Metadata-Version: 2.4
Name: sphyr-sdk
Version: 2.0.0b2
Summary: Python SDK for Sphyr Agent Guard
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: httpx<1.0,>=0.28.0
Requires-Dist: pydantic<3.0,>=2.13.0
Requires-Dist: cryptography<49.0,>=42.0
Requires-Dist: requests<3.0,>=2.32
Provides-Extra: instrument
Requires-Dist: requests<3.0,>=2.32; extra == "instrument"
Provides-Extra: dev
Requires-Dist: pytest-cov<8.0,>=7.0; extra == "dev"
Requires-Dist: freezegun<2.0,>=1.5; extra == "dev"
Requires-Dist: pytest-asyncio<2.0,>=0.25; extra == "dev"
Requires-Dist: requests<3.0,>=2.32; extra == "dev"

# sphyr-sdk

Python SDK for [Sphyr Agent Guard](https://sphyr.io). Adds verifiable, auditable security checks to every outbound agent request.

## Quick-Start

### 1. Install

```bash
pip install sphyr-sdk
```

### 2. Sign in (no key copy-paste)

```bash
npx @sphyr/cli login
```

Opens your browser, signs you in via the device flow, and writes your credential to
`~/.sphyr/config.json` (owner-only `0600`). After this, the SDK picks it up automatically.

> No Node/npx on your machine? Run `python -m sphyr_sdk login` for the same device-flow sign-in.
>
> Setting up an IDE-based agent (Claude Desktop, Cursor, etc.) instead? Use
> `npx @sphyr/cli guard init` — it signs you in **and** writes the MCP server config for each
> detected IDE.

### 3. Protect every outbound call (recommended — install and forget)

```python
from sphyr_sdk import auto_instrument

# One line at startup. After this, every requests / httpx call is signed, scanned,
# and audited through Sphyr automatically — no per-call wrapping.
report = auto_instrument()
print(report.requests.status)  # 'patched' or 'not_installed'
print(report.httpx.status)     # 'patched' or 'not_installed'
```

> **Note:** `auto_instrument()` returns a `PatchReport` (per-lib instrumentation status), not a
> `SphyrClient`. If you need a client reference for later teardown, use
> `SphyrClient(...).instrument()` directly.
>
> **The SDK does nothing until you call `auto_instrument()`** — importing the package alone is not protective.
>
> **Fail-closed by default:** if the Sphyr gateway is unreachable, instrumented requests raise
> `SphyrNetworkError` rather than leaving unscreened. Pass `auto_instrument(fail_closed=False)` to
> prioritize app uptime over strict enforcement.

Credentials resolve in order: **explicit args → `SPHYR_CREDENTIAL` env var →
`~/.sphyr/config.json`**. So `auto_instrument()` with no arguments works after `npx @sphyr/cli login`.

### Works with your agent framework — automatically

`auto_instrument()` patches the underlying HTTP layer (`requests` and `httpx`), so any library or
framework that makes outbound calls through them is signed, scanned, and audited the moment you call
it — **no adapters, no per-framework wiring**. That includes the OpenAI SDK, the Anthropic SDK,
LangChain / LangGraph, CrewAI, AutoGen, and LlamaIndex (all of which use `requests`/`httpx` under the
hood). Call `auto_instrument()` once at process start, before your agent makes its first request.

### Manual / advanced — wrap calls yourself

```python
import asyncio

from sphyr_sdk.client import AsyncSphyrClient
from sphyr_sdk.errors import SphyrError


async def main() -> None:
    client = AsyncSphyrClient()  # resolves creds from env / ~/.sphyr/config.json
    try:
        result = await client.call({"url": "https://api.example.com/data", "mthd": "GET"})
        print(result)
    except SphyrError as err:
        print(err.code, err.retryable, err.docs_url)


asyncio.run(main())
```

### 4. Add the MCP Server (Claude Desktop, Cursor, etc.)

**Recommended — one command:**

```bash
npx @sphyr/cli guard init
```

This opens your browser, signs you in, and writes per-IDE configs automatically — including the `SPHYR_CREDENTIAL` env var. MCP server setup uses the Node-based `sphyr-mcp` binary from `@sphyr/sdk`.

See [sphyr.io/docs/mcp](https://sphyr.io/docs/mcp) for per-client setup (Claude Desktop, Claude Code CLI, Cursor, Antigravity, OpenAI Codex).

Full reference (retry policy, custom transport, timeout config, error catalog): [sphyr.io/docs](https://sphyr.io/docs)

## What traffic is intercepted (and what is not)

`instrument()` / `auto_instrument()` work by patching specific HTTP client libraries. Traffic from anything else flows **directly to the network, unguarded** — there is no OS-level interception.

| Transport | Intercepted? |
|---|---|
| `requests` (Session-based and module-level) | ✅ yes |
| `httpx` (`Client` and `AsyncClient`) | ✅ yes |
| `aiohttp` | ❌ no — flows unguarded |
| `urllib` / `urllib.request` / `http.client` | ❌ no — flows unguarded |
| `urllib3` (used directly, not via requests) | ❌ no — flows unguarded |
| `pycurl` / subprocess `curl` | ❌ no — flows unguarded |

If your agent uses an unsupported transport, route those calls through `SphyrClient.call()` explicitly, or switch the agent's HTTP layer to `requests`/`httpx`.

**Local/private traffic exclusions.** To prevent gateway loops and keep local development working, intercepted clients pass traffic to `localhost`, RFC 1918 ranges (`10/8`, `172.16/12`, `192.168/16`), generic link-local (`169.254/16`), and IPv6 ULA/link-local **directly to the real transport, unguarded**. One deliberate exception: the cloud metadata endpoints (`169.254.169.254`, `fd00:ec2::254`) are always routed through the gateway, which blocks and audits the attempt — these are the primary SSRF targets a prompt-injected agent probes.

## Tool Schema Drift Detection

When `SphyrClient.instrument()` is active, the SDK automatically detects changes to an MCP server's `tools/list` response between sessions. The first response per origin sets the baseline; subsequent changes raise `SphyrDriftError` (in `BLOCK` mode) or emit a telemetry event and continue (in `LOG` mode, the default).

```python
from sphyr_sdk import SphyrClient, SphyrDriftError

try:
    result = await client.call({"url": "https://mcp.example.com/...", "mthd": "GET"})
except SphyrDriftError as err:
    # Tool schema changed unexpectedly — halt and alert
    print(err.code)  # "TOOL_SCHEMA_DRIFT"
```

`SphyrDriftError` is also importable from `sdk.agent_guard_utils` for legacy integrations. The enforcement mode (`LOG` or `BLOCK`) is set per API key via `key_flags.tool_drift_mode` in the admin console.

## User-Agent header (custom integrations)

This SDK sends `User-Agent: sphyr-sdk-python/<version>` on every request to the Sphyr gateway. You do not need to do anything to enable this — it is set automatically.

If you are **building a custom MCP client** that talks to the Sphyr gateway directly (i.e. not using this SDK), you must set a `User-Agent` header on your requests. Cloudflare's WAF blocks Python `urllib`'s default `Python-urllib/3.X` User-Agent as a bot — requests fail with HTTP 403 / Error 1010 (`browser_signature_banned`) before they reach the Worker. Any non-empty descriptive value (e.g. `your-app/1.0`) is sufficient.
