Metadata-Version: 2.4
Name: ophanix-python-sdk
Version: 0.1.0
Summary: Python SDK for Ophanix, including the Tool Gateway client.
Project-URL: Documentation, https://github.com/ophanix/ophanix-python-sdk
Project-URL: Source, https://github.com/ophanix/ophanix-python-sdk
Project-URL: Issues, https://github.com/ophanix/ophanix-python-sdk/issues
Author: Ophanix Maintainers
Maintainer: Ophanix Maintainers
License: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1.0,>=0.27.0
Provides-Extra: dev
Requires-Dist: mypy<2,>=1.10; extra == 'dev'
Requires-Dist: pytest==8.4.1; extra == 'dev'
Requires-Dist: ruff<1,>=0.12; extra == 'dev'
Provides-Extra: release
Requires-Dist: build<2,>=1.2.2; extra == 'release'
Requires-Dist: twine<7,>=5.1; extra == 'release'
Provides-Extra: security
Requires-Dist: pip-audit<3,>=2.7; extra == 'security'
Description-Content-Type: text/markdown

# Ophanix Python SDK

Python SDK for Ophanix. The current package provides the Tool Gateway client for
agents that call governed tools through the Ophanix Tool Gateway.

## Install

When the package is published to your configured Python index:

```bash
pip install ophanix-python-sdk
export OPHANIX_GATEWAY_TOKEN="replace-with-a-gateway-token"
```

If the package has not been published to your configured package index yet,
install from a validated wheel generated by `python3 scripts/validate_release.py`,
or install from the repository path during internal testing:

```bash
pip install .
```

The package distribution is named `ophanix-python-sdk`; the stable import path
for the Tool Gateway client remains `ophanix_tool_gateway`.

Gateway tokens are issued by Ophanix for a specific agent, organization,
environment, scope, and tool resource. Do not hard-code them in application
source or commit them to configuration files. Prefer environment variables,
secret managers, or a custom token provider that reads from your runtime's
credential store. Production tokens must be generated by the Ophanix credential
issuance flow or an equivalent cryptographically random issuer; short,
human-readable fixture tokens are only acceptable in local tests.
Token providers must return the raw token without the `Bearer ` prefix. Raw
tokens may contain only `A-Z`, `a-z`, `0-9`, `.`, `_`, `~`, `+`, `/`, `=`, and
`-`, with no whitespace, and must be 4096 characters or fewer.

Use `https://` gateway URLs outside local development. Plain `http://` is accepted for
`localhost` and loopback addresses only; non-local HTTP requires the explicit
`allow_insecure_http=True` opt-in.

## Sync Usage

```python
from ophanix_tool_gateway import (
    EnvironmentTokenProvider,
    OphanixToolGatewayClient,
    ToolAuthenticationError,
    ToolDeniedError,
    ToolGatewayError,
)

with OphanixToolGatewayClient(
    base_url="https://gateway.example.com",
    token_provider=EnvironmentTokenProvider(),
) as client:
    try:
        compatibility = client.check_compatibility()
        if not compatibility.compatible:
            raise RuntimeError("SDK and gateway contract versions do not match")
        tools = client.list_all_tools()
        result = client.call_tool("claims.lookup", {"claim_id": "claim_123"})
    except ToolDeniedError as exc:
        print(f"Denied by gateway policy: {exc.reason_code}")
    except ToolAuthenticationError:
        print("Gateway token was rejected")
    except ToolGatewayError as exc:
        print(f"Gateway call failed: {exc.code or exc.status_code}")
    else:
        print([tool.name for tool in tools])
        print(result.result)
```

## API Reference

`OphanixToolGatewayClient` is the synchronous client. Use it as a context manager
so the underlying HTTP client is closed when the agent shuts down.

`AsyncOphanixToolGatewayClient` mirrors the synchronous API for async runtimes.
It accepts either a sync token provider or an async token provider.

Use `ToolGatewayClientConfig` with `OphanixToolGatewayClient.from_config(...)`
or `AsyncOphanixToolGatewayClient.from_config(...)` when several workers share
the same timeout, cache, retry, and response-limit settings.

Common constructor options:

- `base_url`: absolute gateway URL. Non-local `http://` is rejected unless
  `allow_insecure_http=True`.
- `token_provider`: object with `get_token()`. Use `EnvironmentTokenProvider()`
  for `OPHANIX_GATEWAY_TOKEN`, or `StaticTokenProvider(token)` for tests and
  short-lived scripts.
- `timeout_seconds`: per-request timeout, default `5.0`.
- `max_payload_bytes`: client-side serialized payload cap, default `1_000_000`.
- `max_response_bytes`: client-side gateway response cap, default `1_000_000`.
  The built-in HTTPX clients enforce this while streaming response bytes and
  before JSON parsing. Custom injected clients must expose `stream()` by
  default. Set `allow_buffered_custom_http_client=True` only when the injected
  client enforces equivalent response-size limits before materializing bodies.
- `cache_tools`: opt-in discovery cache, default `False`.
- `cache_ttl_seconds`: cache TTL when `cache_tools=True`, default `300.0`.
- `max_cache_entries`: maximum process-local discovery cache entries when
  caching is enabled, default `256`.
- `event_hook`: optional callable that receives immutable, token-free telemetry
  events for tool-call start/success/denial/error. Hook exceptions are swallowed
  and debug-logged by default so observability code cannot break tool calls.
- `raise_event_hook_errors`: when `True`, event hook failures are surfaced to
  the caller. Use this in tests or strict observability environments.
- `discovery_max_retries`: retry count for discovery only, default `2`.
- `discovery_retry_backoff_seconds`: base exponential backoff, default `0.2`.
- `discovery_retry_max_sleep_seconds`: cap for retry sleeps and `Retry-After`,
  default `5.0`.
- `discovery_retry_jitter_ratio`: client-side retry jitter ratio, default `0.2`.
- `invocation_max_retries`: retry count for idempotent tool invocations,
  default `2`. The SDK only uses this when `call_tool(..., idempotency_key=...)`
  is provided.
- `invocation_retry_backoff_seconds`: base invocation retry backoff, default
  `0.2`.
- `invocation_retry_max_sleep_seconds`: cap for invocation retry sleeps and
  `Retry-After`, default `5.0`.
- `invocation_retry_jitter_ratio`: invocation retry jitter ratio, default `0.2`.
- `allow_buffered_custom_http_client`: default `False`; set only for injected
  clients that enforce equivalent response-size limits without SDK streaming.

Main methods:

- `call_tool(tool_name, payload, correlation_id=None, idempotency_key=None)`:
  invokes one tool. Payload must be a JSON object with string keys and finite
  numeric values. When `idempotency_key` is set, the SDK sends
  `Idempotency-Key` and may retry transient invocation failures with the same
  key.
- `check_compatibility()`: calls authenticated
  `/api/v1/gateway/capabilities` and returns `GatewayCompatibility`.
- `list_tools(owner_team=None, limit=50, offset=0)`: lists one page of callable
  active tools for the configured credential. The gateway discovery API is
  active-only; the legacy `status="active"` argument remains accepted only for
  compatibility and emits `DeprecationWarning`.
- `list_all_tools(owner_team=None, page_size=200, max_total=None)`: follows
  discovery pagination and can fail closed if discovery exceeds `max_total`.
- `get_tool(tool_name_or_id)`: finds one callable tool by name or id.
- `clear_tool_cache()`: clears cached discovery results when `cache_tools=True`.

`get_tool()` searches only the definitions visible to the current credential. A
missing result raises `ToolGatewayError` with `code="tool_not_visible"` because
the SDK cannot safely distinguish a nonexistent tool from a tool hidden by
authorization.

## Async Usage

```python
from ophanix_tool_gateway import AsyncOphanixToolGatewayClient, EnvironmentTokenProvider

async with AsyncOphanixToolGatewayClient(
    base_url="https://gateway.example.com",
    token_provider=EnvironmentTokenProvider(),
) as client:
    tool = await client.get_tool("claims.lookup")
    result = await client.call_tool(tool.name, {"claim_id": "claim_123"})
    print(result.request_id, result.result)
```

## Error Handling

The SDK raises `ToolGatewayValidationError` for invalid local configuration or
caller input, `ToolAuthenticationError` for rejected gateway credentials,
`ToolDeniedError` for gateway policy denials, and `ToolGatewayError` for
transport, gateway, upstream, and response-contract failures.
`ToolGatewayValidationError` is a `ValueError` subclass for backward-compatible
callers that already catch `ValueError`. Exception messages are intentionally
generic so application logs do not inherit arbitrary upstream text. Use
`status_code`, `code`, `reason_code`, `request_id`, `correlation_id`,
`retry_after_seconds`, and the sanitized `response_body` for debugging.

```python
try:
    result = client.call_tool("claims.lookup", {"claim_id": "claim_123"})
except ToolAuthenticationError as exc:
    refresh_or_rotate_token = exc.code
except ToolDeniedError as exc:
    audit_reason = exc.reason_code
except ToolGatewayError as exc:
    request_id = exc.request_id
    diagnostic_code = exc.code or exc.status_code
    retry_after = exc.retry_after_seconds
```

## Reliability And Safety

Discovery uses the gateway-authenticated `/api/v1/gateway/tools` route and only returns
active tools callable by the configured credential. `list_all_tools()` follows pagination
for you, while `list_tools()` exposes explicit `limit` and `offset` controls.
Pagination is offset-based in `0.1.x`; consumers that need very large or
high-churn catalogs should set `max_total` and re-run discovery after contract
changes until the gateway exposes cursor pagination.
The optional `owner_team` discovery filter is an exact, case-sensitive server
filter; use the owner-team value from the tool definition rather than display
text.

Discovery requests retry transient `408`, `429`, and `5xx` responses by default with
bounded exponential backoff, `Retry-After` support, and jitter. Tool invocation
requests retry transient transport, `408`, `429`, and `5xx` failures only when
the caller supplies `idempotency_key`; the same key is sent on every attempt so
the gateway can replay a completed result or reject a conflicting payload.

The SDK validates payloads as strict JSON objects, rejects non-finite numbers,
non-string object keys, and cyclic Python payloads, rejects header control
characters, rejects raw gateway tokens longer than 4096 characters, redacts
sensitive fields from exception diagnostic bodies, caps
gateway responses while streaming bytes before JSON parsing when using the
built-in HTTPX clients, and keeps static tokens out of generated `repr()` output.

When `cache_tools=True`, discovery cache entries are partitioned by a
process-local HMAC fingerprint of the current bearer token and expire after
`cache_ttl_seconds`.
Cached results do not cross credential rotations, and `list_all_tools()` /
`get_tool()` use one credential context across their paginated requests. Cached
tool definitions are copied before they are returned so caller mutation cannot
poison later cache reads. The cache is process-local and bounded by
`max_cache_entries`; call `clear_tool_cache()` after urgent permission or
tool-contract changes instead of waiting for TTL expiry.

Client cache mutation is internally guarded, but applications should still prefer
one client per worker process or event loop so timeout, token-provider, and
lifecycle behavior remains easy to reason about.
Async calls follow normal `asyncio` cancellation semantics. If a task awaiting
`call_tool()` is cancelled, treat the invocation outcome as unknown unless your
tool contract is idempotent or your application can reconcile by `request_id` or
`correlation_id`.

Use a unique idempotency key per logical invocation when retrying or when the
caller cannot tolerate duplicate upstream side effects:

```python
result = client.call_tool(
    "claims.lookup",
    {"claim_id": "claim_123"},
    idempotency_key="claim-123-refresh-2026-05-11T09:00Z",
)
```

Reusing a key with a different payload returns `idempotency_conflict`. Reusing a
key while the original call is still running returns `idempotency_in_progress`.

`EnvironmentTokenProvider` reads the configured environment variable on each
request. For high-throughput agents, prefer a custom provider backed by your
runtime secret manager or refresh cache:

```python
class CachedTokenProvider:
    def __init__(self, loader):
        self._loader = loader
        self._token = None

    def get_token(self) -> str:
        if self._token is None:
            self._token = self._loader()
        return self._token
```

SDK event hooks receive immutable mappings with no bearer tokens:

| Event | Fields |
| --- | --- |
| `tool_call.start` | `tool_name`, `correlation_id`, `idempotent` |
| `tool_call.success` | `tool_name`, `request_id`, `correlation_id`, `reason_code`, `elapsed_ms` |
| `tool_call.denied` | `tool_name`, `status_code`, `elapsed_ms` |
| `tool_call.error` | `tool_name`, `status_code` or `code`, `elapsed_ms` |
| `tool_call.retry` | `tool_name`, `attempt`, `delay_seconds`, `status_code`, `code` |
| `tool_discovery.retry` | `attempt`, `delay_seconds`, `status_code` |

Sanitized exception `response_body` values redact common credential keys and
PII-like keys including `authorization`, `api_key`, `credential`, `password`,
`secret`, `token`, `key`, `email`, `phone`, `address`, and `ssn`.

## Credential Issuance

The SDK consumes gateway bearer tokens; it does not mint them. Production tokens
must be issued through Product Platform agent credential issuance:

1. Register or select an active agent identity.
2. Approve the capability scope the credential needs.
3. Bind the scope to the exact tool resource where possible.
4. Issue a short-lived bearer credential from the Product Platform API or
   operator workflow.
5. Store the raw token in a runtime secret store and expose it to the SDK through
   `EnvironmentTokenProvider` or a custom provider.
6. Rotate before expiry and clear discovery caches after emergency permission
   changes.

Local fixture tokens in direct HTTP examples are deterministic demo data and are
not production credentials.

Minimal local issuance flow for demo data:

```bash
cd packages/product-platform
python3 -m product_platform.cli db migrate
python3 -m product_platform.cli db seed
python3 -m product_platform.cli serve --host 127.0.0.1 --port 8088

export OPHANIX_BASE_URL="http://127.0.0.1:8088"
export OPHANIX_OPERATOR_TOKEN="<operator-api-token-from-your-local-login-or-admin-flow>"

curl -sS -X POST "$OPHANIX_BASE_URL/api/v1/agents/agent_claims_demo/credentials" \
  -H "Authorization: Bearer $OPHANIX_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Environment-ID: env_default" \
  --data '{
    "credential_type": "bearer",
    "issuer": "local-demo",
    "ttl_seconds": 900,
    "scopes": [
      {"scope": "claims.lookup:read", "resource_type": "tool", "resource_id": "claims.lookup"}
    ]
  }'
```

The response includes the raw token once. Store it in a local secret store or
`OPHANIX_GATEWAY_TOKEN`; do not commit it. Exact endpoint names may differ in
private operator builds, so verify the local API shape against your deployment
before writing automation.

## Stability

The package is still `0.1.0` and classified as Beta. Constructor names, typed
error classes, and the main sync/async methods are treated as the supported
surface for this line; compatibility shims such as `list_tools(status="active")`
may emit deprecation warnings before removal in a future pre-1.0 release.

Compatibility matrix:

| SDK version | Expected gateway contract | Python versions | Notes |
| --- | --- | --- | --- |
| `0.1.x` | `/api/v1/gateway/capabilities`, `/api/v1/gateway/tools`, and `/api/v1/tools/{name}/invoke` | 3.11, 3.12, 3.13 | Beta MVP. `product_platform.tool_gateway` imports are compatibility shims; prefer `ophanix_tool_gateway`. |

MVP support boundary: the SDK is suitable for controlled internal or design
partner pilots where gateway operators control tokens, upstream targets,
ingress limits, and rollout scope. It is not an enterprise production
certification. Idempotent invocation retries are implemented for callers that
provide `Idempotency-Key`; cursor pagination, externally enforced ingress
limits, and broader release-governance evidence remain post-MVP hardening.

## Troubleshooting

- `ToolGatewayValidationError: base_url must use https`: use an `https://`
  gateway URL outside localhost development, or explicitly opt into non-local
  HTTP for a trusted test network.
- `ToolGatewayValidationError: token is required`: check that
  `OPHANIX_GATEWAY_TOKEN` is present and not blank.
- `ToolGatewayValidationError: token must be the raw gateway token without the
  Bearer prefix`: remove `Bearer ` from the value returned by your token
  provider. The SDK adds the authorization scheme itself.
- `ToolDeniedError`: the credential authenticated successfully, but policy,
  permission, scope, or resource binding denied the call.
- `ToolGatewayError(code="invalid_response")`: the gateway returned a successful
  HTTP response that did not match the SDK contract. Capture the `request_id`
  and sanitized `response_body` when reporting the issue.
- `ToolGatewayError(code="transport_error")`: the SDK could not reach the
  gateway. Check DNS, TLS, connectivity, proxy settings, and `timeout_seconds`.
- `ToolGatewayError(code="response_too_large")`: the gateway response exceeded
  `max_response_bytes` before SDK JSON parsing.
- `ToolGatewayError(code="client_closed")`: the SDK client was used after
  `close()` or context-manager exit. Create a new client for further calls.

## Production Adoption Checklist

- Use an `https://` gateway URL outside localhost development.
- Use the SDK instead of direct HTTP examples for production agents.
- Issue high-entropy gateway tokens through the Product Platform credential
  workflow, store them in a secret manager, and rotate them before expiry.
- Scope credentials to the minimum tool resource and operation required.
- Set `timeout_seconds` and `max_payload_bytes` for the caller's workload.
- Use `idempotency_key` for any invocation that may be retried or reconciled
  after an unknown outcome.
- Enable discovery caching only when stale contract/permission data is acceptable
  for up to `cache_ttl_seconds`.
- Capture `request_id`, `correlation_id`, `code`, and sanitized `response_body`
  in application diagnostics.
- Use custom HTTP clients only when they support streaming response limits, or
  set `allow_buffered_custom_http_client=True` with an equivalent safety control.

## Release Validation

Before publishing a standalone SDK release, install release tooling and validate
both artifact shapes:

```bash
python3 -m pip install '.[release]'
python3 scripts/validate_release.py
```

The release check builds a wheel and sdist, verifies that both artifacts contain
`ophanix_tool_gateway/__init__.py`, `ophanix_tool_gateway/sdk.py`, and
`ophanix_tool_gateway/py.typed`, rejects local/generated files such as SQLite
databases and `__pycache__`, verifies parity with the product-platform vendored
SDK copy, installs the built wheel with runtime dependencies into a temporary
target, writes a local CycloneDX SBOM with artifact hashes, and runs
`twine check` over the generated artifacts. Dependency audit can be enforced in
CI with:

```bash
python3 -m pip install '.[security]'
python3 scripts/validate_release.py --require-dependency-audit
python3 scripts/validate_release.py --strict-git
```

The SDK depends only on `httpx`. It is also re-exported from
`product_platform.tool_gateway` for compatibility with earlier internal imports.
CI exercises the SDK on Python 3.11, 3.12, and 3.13. Dependency range changes
must keep that matrix green before release.

`--strict-git` additionally requires a clean SDK package worktree and an exact
release tag matching `v<project.version>` unless `--expected-tag` is supplied.

Publishing is intentionally separate from local artifact validation. A release
owner must publish only artifacts produced by the validated workflow, preserve
the build logs, artifact checksums, provenance attestations, and SBOM, and
document whether the final upload is performed through GitHub trusted
publishing, Azure DevOps, or another approved internal release pipeline.
