Metadata-Version: 2.4
Name: harness-sdk
Version: 1.0.7b3
Summary: Harness Python SDK with instrumentation and plugin architecture
License-Expression: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.md
License-File: NOTICE.md
Requires-Dist: opentelemetry-api==1.41.1
Requires-Dist: opentelemetry-exporter-otlp==1.41.1
Requires-Dist: opentelemetry-instrumentation==0.62b1
Requires-Dist: opentelemetry-instrumentation-aiohttp-client==0.62b1
Requires-Dist: opentelemetry-instrumentation-botocore==0.62b1
Requires-Dist: opentelemetry-instrumentation-wsgi==0.62b1
Requires-Dist: opentelemetry-instrumentation-fastapi==0.62b1
Requires-Dist: opentelemetry-instrumentation-flask==0.62b1
Requires-Dist: opentelemetry-instrumentation-mysql==0.62b1
Requires-Dist: opentelemetry-instrumentation-psycopg2==0.62b1
Requires-Dist: opentelemetry-instrumentation-requests==0.62b1
Requires-Dist: opentelemetry-instrumentation-httpx==0.62b1
Requires-Dist: opentelemetry-instrumentation-grpc==0.62b1
Requires-Dist: opentelemetry-instrumentation-django==0.62b1
Requires-Dist: opentelemetry-instrumentation-mcp==0.60.0
Requires-Dist: opentelemetry-semantic-conventions-ai<0.6.0,>=0.5.1
Requires-Dist: opentelemetry-propagator-b3==1.41.1
Requires-Dist: opentelemetry-proto==1.41.1
Requires-Dist: opentelemetry-sdk==1.41.1
Requires-Dist: opentelemetry-util-http==0.62b1
Requires-Dist: google>=3.0.0
Requires-Dist: pyyaml
Requires-Dist: protobuf
Requires-Dist: psutil
Requires-Dist: distro
Requires-Dist: setuptools
Requires-Dist: jaraco.text
Requires-Dist: platformdirs
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.34.0; extra == "anthropic"
Provides-Extra: openai
Requires-Dist: openai>=1.40.0; extra == "openai"
Provides-Extra: litellm
Requires-Dist: litellm>=1.60.0; extra == "litellm"
Provides-Extra: google-genai
Requires-Dist: google-genai>=1.0.0; extra == "google-genai"
Provides-Extra: dev
Requires-Dist: pytest==7.4.3; extra == "dev"
Requires-Dist: pytest-cov==3.0.0; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-django==4.12.0; extra == "dev"
Requires-Dist: flask==3.1.3; extra == "dev"
Requires-Dist: Werkzeug==3.1.8; extra == "dev"
Requires-Dist: aiohttp; extra == "dev"
Requires-Dist: fastapi>=0.75.0; extra == "dev"
Requires-Dist: psycopg2-binary; extra == "dev"
Requires-Dist: mysql-connector-python==8.0.26; extra == "dev"
Requires-Dist: boto3==1.35.43; extra == "dev"
Requires-Dist: moto==3.0.3; extra == "dev"
Requires-Dist: docker; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Requires-Dist: Django==5.2.13; extra == "dev"
Requires-Dist: anthropic>=0.34.0; extra == "dev"
Requires-Dist: openai>=1.40.0; extra == "dev"
Requires-Dist: litellm>=1.60.0; extra == "dev"
Requires-Dist: google-genai>=1.0.0; extra == "dev"
Requires-Dist: grpcio; extra == "dev"
Dynamic: license-file
Dynamic: requires-python

# harness-sdk (otel-python-sdk)

Generic Python agent SDK with OpenTelemetry instrumentation and a plugin architecture.

## Local development

```bash
bash scripts/fetch-vendor.sh
pip install -e ".[dev,anthropic,google-genai,openai,litellm]"
./scripts/run-unit-tests.sh
```

Environment variables for SDK configuration use the `HARNESS_` prefix (for example `HARNESS_SERVICE_NAME`, `HARNESS_REPORTING_ENDPOINT`). The legacy `HA_`, `AT_`, and `TA_` prefixes remain supported for backwards compatibility; when the same setting is defined under multiple prefixes, `HARNESS_` wins, then `HA_`, then `AT_`, then `TA_`.

### Integration tests (MySQL / PostgreSQL)

```bash
cd test/externalServices && docker compose up -d --wait
cd ../..
RUN_SDK_INTEGRATION_TESTS=1 ./scripts/run-unit-tests.sh
```

## CI

GitHub Actions workflows (public repo, `ubuntu-latest`):

| Workflow | Trigger | Purpose |
|----------|---------|---------|
| [pr_build.yaml](.github/workflows/pr_build.yaml) | PR + push to `main` | Build manylinux wheels, lint, pytest (with Docker for DB tests) |
| [publish.yaml](.github/workflows/publish.yaml) | Tag `v*.*.*` | Build release artifacts and publish to PyPI / TestPyPI |
| [staticanalysis.yaml](.github/workflows/staticanalysis.yaml) | PR, `main`, weekly | Trivy filesystem scan |

## Publishing to PyPI

1. Configure [trusted publishing](https://docs.pypi.org/trusted-publishers/) on PyPI (and TestPyPI for RCs):
   - **PyPI**: environment `pypi`, workflow `publish.yaml`, job `publish-pypi`
   - **TestPyPI**: environment `testpypi`, workflow `publish.yaml`, job `publish-testpypi`
2. Create and push a release tag:
   ```bash
   git tag v0.1.0
   git push origin v0.1.0
   ```
3. Release candidates (`v1.0.0-rc.1`) publish to **TestPyPI** only; stable tags publish to **PyPI**.

Version is taken from the tag (`v1.2.3` → `1.2.3`) via `scripts/build.sh`, which updates `src/harness_sdk/version.py` and `pyproject.toml`.

## Installing

### From PyPI (stable releases)

```bash
pip install harness-sdk
```

Optional instrumentation extras:

```bash
pip install "harness-sdk[anthropic,google-genai,openai,litellm]"
```

### From TestPyPI (release candidates)

Tags containing `-rc` (for example `v1.0.0-rc.1`) are published only to [TestPyPI](https://test.pypi.org/project/harness-sdk/). Use TestPyPI as the primary index and PyPI as a fallback so dependencies that are not on TestPyPI still resolve:

```bash
pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  "harness-sdk==1.0.0-rc.1"
```

With optional extras:

```bash
pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  "harness-sdk[anthropic,google-genai,openai,litellm]==1.0.0-rc.1"
```

Pin the version to the RC you want (see [published files](https://test.pypi.org/project/harness-sdk/#history) on TestPyPI). The version string matches the tag without the leading `v` (tag `v1.0.0-rc.1` → `1.0.0-rc.1`). In a virtualenv or `requirements.txt`, the same flags apply:

```text
--index-url https://test.pypi.org/simple/
--extra-index-url https://pypi.org/simple/
harness-sdk==1.0.0-rc.1
```

After install, import and auto-instrument work the same as a PyPI release:

```python
from harness_sdk.agent import Agent
```

```bash
harness-instrument python app.py
```

## Usage

```python
from harness_sdk.agent import Agent

agent = Agent()
agent.instrument()
```

Auto-instrumentation:

```bash
export HARNESS_CONFIG_FILE=/path/to/config.yaml
harness-instrument python app.py
```

## Enabling instrumentation (opt-in)

Instrumentation is **opt-in**. By default `agent.instrument()` and
`harness-instrument` instrument nothing; you must explicitly enable the
categories you want. A flag is "on" only when its value is (case-insensitively)
`true`; any other value — or an unset variable — leaves it off.

| Category | Environment variable | Enables |
|----------|----------------------|---------|
| API / HTTP | `HARNESS_ENABLE_API` | All non-AI instrumentation: HTTP servers (Django, Flask, FastAPI), HTTP clients (requests, HTTPX, aiohttp), gRPC, databases (MySQL, PostgreSQL), botocore, and any installed OpenTelemetry contrib instrumentors (generic fallback) |
| AI: OpenAI | `HARNESS_ENABLE_AI_OPENAI` | OpenAI instrumentation |
| AI: Anthropic | `HARNESS_ENABLE_AI_ANTHROPIC` | Anthropic instrumentation |
| AI: LiteLLM | `HARNESS_ENABLE_AI_LITELLM` | LiteLLM instrumentation |
| AI: Google GenAI | `HARNESS_ENABLE_AI_GOOGLE_GENAI` | Google GenAI (Gemini / Vertex AI) instrumentation |
| AI: MCP | `HARNESS_ENABLE_AI_MCP` | Model Context Protocol instrumentation |

Each AI provider is enabled independently. The `HARNESS_ENABLE_*` opt-in flags
are **only** read under the `HARNESS_` prefix — they have no `HA_`/`AT_`/`TA_`
aliases. The legacy `HA_GEN_AI_ENABLED` master switch no longer enables or
disables AI instrumentation; use the per-provider flags instead.

```bash
# Enable HTTP/API + OpenAI only
export HARNESS_ENABLE_API=true
export HARNESS_ENABLE_AI_OPENAI=true
harness-instrument python app.py
```

`agent.instrument(skip_libraries=[...])` still works and takes precedence: a
library listed in `skip_libraries` is never instrumented even if its category
is enabled.

### Enrich the current span

Add runtime attributes while the target instrumented span is active:

```python
from harness_sdk import set_span_attribute, set_span_attributes

set_span_attribute("request.client.name", client_name)
set_span_attributes({
    "agent.action.type": action_type,
    "custom.retry.count": retry_count,
})
```

These helpers update only the current recording span. They silently do nothing when no
recording span is active. Attributes do not propagate to child spans or downstream
services.

Keys may use any customer-defined name. Values must be valid OpenTelemetry attribute
values: strings, booleans, integers, floats, or homogeneous sequences of those types.
For duplicate keys, the last write wins; instrumentation can overwrite a customer value
if it writes the same key later. Because the span already exists, these attributes
cannot influence head sampling.

## Plugins

The SDK loads extensions via [setuptools entry points](https://setuptools.pypa.io/en/latest/userguide/entry_point.html). Each plugin has a **name** (the entry-point key). Names are listed in config or environment variables; only installed plugins are loaded, in the order you configure.

| Type | Entry-point group | Config key / env |
|------|-------------------|------------------|
| Control | `harness_sdk_control_plugin` | `plugins.control` / `HA_CONTROL_PLUGINS` |
| Observability | `harness_sdk_observability_plugin` | `plugins.observability` / `HA_OBSERVABILITY_PLUGINS` |

Built-in observability plugins (shipped with `harness-sdk`):

- `builtin_pipeline` — OTLP export, sampling, exclusion processors
- `builtin_span_attributes` — service name and configured span attributes

Example `agent-config.yaml`:

```yaml
service_name: my-service
reporting:
  endpoint: http://localhost:4318
plugins:
  control:
    - my_policy          # order matters: first plugin runs first
  observability:
    - builtin_pipeline
    - builtin_span_attributes
    - my_exporter        # custom plugin after builtins
```

Or via environment (comma-separated, same order semantics):

```bash
export HA_CONTROL_PLUGINS=my_policy
export HA_OBSERVABILITY_PLUGINS=builtin_pipeline,builtin_span_attributes,my_exporter
```

### Create a control plugin

Control plugins evaluate HTTP/gRPC ingress and GenAI spans. They return a `ControlResult` (block, headers, span attributes, etc.). Plugins run in config order; the chain stops when one returns `block=True`.

1. **Implement the plugin** in your package (see `harness_sdk.plugins.control.ControlPlugin`):

```python
# my_company_policy/plugin.py
from typing import Any
from opentelemetry.trace import Span
from harness_sdk.plugins.control import ControlResult, ControlPlugin


class MyPolicyPlugin:
    name = "my_policy"
    provides_blocking = True  # set True if this plugin can block requests

    def on_init(self, config: Any) -> None:
        self._config = config

    def evaluate(
        self, span: Span, url: str, headers: dict, body, is_grpc: bool
    ) -> ControlResult:
        result = ControlResult()
        # result.block = True
        # result.response_status_code = 403
        return result

    def evaluate_agent_span(self, span: Span, body: str = "") -> ControlResult:
        return ControlResult()

    def shutdown(self) -> None:
        pass


def factory(config: Any) -> ControlPlugin:
    return MyPolicyPlugin()
```

2. **Register the entry point** in your package `pyproject.toml`:

```toml
[project.entry-points.harness_sdk_control_plugin]
my_policy = "my_company_policy.plugin:factory"
```

(Equivalent in `setup.py`: `entry_points={'harness_sdk_control_plugin': ['my_policy = ...']}`.)

3. **Install** your package in the same environment as the app (`pip install my-company-policy`).

4. **Enable** the plugin by name in config or `HA_CONTROL_PLUGINS` (see above).

For tests or one-off wiring you can also call `agent.register_control_plugin(plugin)` after `Agent()` is constructed.

Reference implementation: the Traceable agent ships a control plugin as `traceable` (`traceableai.plugins.traceable_control:factory`).

### Create an observability plugin

Observability plugins contribute OpenTelemetry `SpanProcessor` instances to the tracer provider. Processors are registered in config order (each plugin’s `create_span_processors` may return multiple processors).

1. **Implement the plugin** (see `harness_sdk.plugins.observability.ObservabilityPlugin`):

```python
# my_company_telemetry/plugin.py
from typing import Any, List
from opentelemetry.sdk.trace import SpanProcessor
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor


class MyExporterPlugin:
    name = "my_exporter"
    priority = 300  # informational; ordering is driven by config list

    def on_init(self, config: Any) -> None:
        self._config = config

    def create_span_processors(self, config: Any) -> List[SpanProcessor]:
        return [SimpleSpanProcessor(ConsoleSpanExporter())]

    def shutdown(self) -> None:
        pass


def factory(config: Any) -> MyExporterPlugin:
    return MyExporterPlugin()
```

2. **Register the entry point**:

```toml
[project.entry-points.harness_sdk_observability_plugin]
my_exporter = "my_company_telemetry.plugin:factory"
```

3. **Install** your package and **enable** it under `plugins.observability` or `HA_OBSERVABILITY_PLUGINS`.

If you omit observability plugins entirely, the SDK defaults to `builtin_pipeline` and `builtin_span_attributes`. Custom plugins typically keep those builtins and append your entry after them.

Reference implementations in this repo:

- `harness_sdk.plugins.builtin.pipeline`
- `harness_sdk.plugins.builtin.span_attributes`
