sphyr-sdk 2.0.0b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. sphyr_sdk-2.0.0b1/PKG-INFO +152 -0
  2. sphyr_sdk-2.0.0b1/README.md +134 -0
  3. sphyr_sdk-2.0.0b1/pyproject.toml +53 -0
  4. sphyr_sdk-2.0.0b1/setup.cfg +4 -0
  5. sphyr_sdk-2.0.0b1/sphyr_sdk/__init__.py +150 -0
  6. sphyr_sdk-2.0.0b1/sphyr_sdk/__main__.py +70 -0
  7. sphyr_sdk-2.0.0b1/sphyr_sdk/_connectivity.py +207 -0
  8. sphyr_sdk-2.0.0b1/sphyr_sdk/_instrument.py +476 -0
  9. sphyr_sdk-2.0.0b1/sphyr_sdk/_login.py +191 -0
  10. sphyr_sdk-2.0.0b1/sphyr_sdk/_session_models.py +46 -0
  11. sphyr_sdk-2.0.0b1/sphyr_sdk/_signing.py +97 -0
  12. sphyr_sdk-2.0.0b1/sphyr_sdk/_tool_drift.py +329 -0
  13. sphyr_sdk-2.0.0b1/sphyr_sdk/_url_guard.py +91 -0
  14. sphyr_sdk-2.0.0b1/sphyr_sdk/client.py +541 -0
  15. sphyr_sdk-2.0.0b1/sphyr_sdk/errors.py +316 -0
  16. sphyr_sdk-2.0.0b1/sphyr_sdk/generated_models.py +71 -0
  17. sphyr_sdk-2.0.0b1/sphyr_sdk/idempotency_fingerprint.py +75 -0
  18. sphyr_sdk-2.0.0b1/sphyr_sdk/retry_policy.py +172 -0
  19. sphyr_sdk-2.0.0b1/sphyr_sdk/verify.py +97 -0
  20. sphyr_sdk-2.0.0b1/sphyr_sdk.egg-info/PKG-INFO +152 -0
  21. sphyr_sdk-2.0.0b1/sphyr_sdk.egg-info/SOURCES.txt +37 -0
  22. sphyr_sdk-2.0.0b1/sphyr_sdk.egg-info/dependency_links.txt +1 -0
  23. sphyr_sdk-2.0.0b1/sphyr_sdk.egg-info/requires.txt +13 -0
  24. sphyr_sdk-2.0.0b1/sphyr_sdk.egg-info/top_level.txt +1 -0
  25. sphyr_sdk-2.0.0b1/tests/test_client.py +247 -0
  26. sphyr_sdk-2.0.0b1/tests/test_connectivity.py +160 -0
  27. sphyr_sdk-2.0.0b1/tests/test_credentials.py +122 -0
  28. sphyr_sdk-2.0.0b1/tests/test_degraded.py +235 -0
  29. sphyr_sdk-2.0.0b1/tests/test_errors.py +357 -0
  30. sphyr_sdk-2.0.0b1/tests/test_handshake_parity.py +74 -0
  31. sphyr_sdk-2.0.0b1/tests/test_idempotency_fingerprint.py +135 -0
  32. sphyr_sdk-2.0.0b1/tests/test_instrument.py +511 -0
  33. sphyr_sdk-2.0.0b1/tests/test_login.py +345 -0
  34. sphyr_sdk-2.0.0b1/tests/test_main.py +58 -0
  35. sphyr_sdk-2.0.0b1/tests/test_parity.py +246 -0
  36. sphyr_sdk-2.0.0b1/tests/test_session_models.py +96 -0
  37. sphyr_sdk-2.0.0b1/tests/test_tool_drift.py +742 -0
  38. sphyr_sdk-2.0.0b1/tests/test_url_guard.py +137 -0
  39. sphyr_sdk-2.0.0b1/tests/test_verify.py +167 -0
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: sphyr-sdk
3
+ Version: 2.0.0b1
4
+ Summary: Python SDK for Sphyr Agent Guard
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx<1.0,>=0.28.0
8
+ Requires-Dist: pydantic<3.0,>=2.13.0
9
+ Requires-Dist: cryptography<49.0,>=42.0
10
+ Requires-Dist: requests<3.0,>=2.32
11
+ Provides-Extra: instrument
12
+ Requires-Dist: requests<3.0,>=2.32; extra == "instrument"
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-cov<8.0,>=7.0; extra == "dev"
15
+ Requires-Dist: freezegun<2.0,>=1.5; extra == "dev"
16
+ Requires-Dist: pytest-asyncio<2.0,>=0.25; extra == "dev"
17
+ Requires-Dist: requests<3.0,>=2.32; extra == "dev"
18
+
19
+ # sphyr-sdk
20
+
21
+ Python SDK for [Sphyr Agent Guard](https://sphyr.io). Adds verifiable, auditable security checks to every outbound agent request.
22
+
23
+ ## Quick-Start
24
+
25
+ ### 1. Install
26
+
27
+ ```bash
28
+ pip install sphyr-sdk
29
+ ```
30
+
31
+ ### 2. Sign in (no key copy-paste)
32
+
33
+ ```bash
34
+ npx @sphyr/cli login
35
+ ```
36
+
37
+ Opens your browser, signs you in via the device flow, and writes your credential to
38
+ `~/.sphyr/config.json` (owner-only `0600`). After this, the SDK picks it up automatically.
39
+
40
+ > No Node/npx on your machine? Run `python -m sphyr_sdk login` for the same device-flow sign-in.
41
+ >
42
+ > Setting up an IDE-based agent (Claude Desktop, Cursor, etc.) instead? Use
43
+ > `npx @sphyr/cli guard init` — it signs you in **and** writes the MCP server config for each
44
+ > detected IDE.
45
+
46
+ ### 3. Protect every outbound call (recommended — install and forget)
47
+
48
+ ```python
49
+ from sphyr_sdk import auto_instrument
50
+
51
+ # One line at startup. After this, every requests / httpx call is signed, scanned,
52
+ # and audited through Sphyr automatically — no per-call wrapping.
53
+ report = auto_instrument()
54
+ print(report.requests.status) # 'patched' or 'not_installed'
55
+ print(report.httpx.status) # 'patched' or 'not_installed'
56
+ ```
57
+
58
+ > **Breaking change (v0.27.0):** `auto_instrument()` now returns a `PatchReport` (per-lib
59
+ > instrumentation status) instead of a `SphyrClient`. Callers that did
60
+ > `client = auto_instrument(...)` must update — use `SphyrClient(...).instrument()` directly
61
+ > if you need a client reference for later teardown.
62
+ >
63
+ > **The SDK does nothing until you call `auto_instrument()`** — importing the package alone is not protective.
64
+ >
65
+ > **Fail-closed by default:** if the Sphyr gateway is unreachable, instrumented requests raise
66
+ > `SphyrNetworkError` rather than leaving unscreened. Pass `auto_instrument(fail_closed=False)` to
67
+ > prioritize app uptime over strict enforcement.
68
+
69
+ Credentials resolve in order: **explicit args → `SPHYR_CREDENTIAL` env var →
70
+ `~/.sphyr/config.json`**. So `auto_instrument()` with no arguments works after `npx @sphyr/cli login`.
71
+
72
+ ### Works with your agent framework — automatically
73
+
74
+ `auto_instrument()` patches the underlying HTTP layer (`requests` and `httpx`), so any library or
75
+ framework that makes outbound calls through them is signed, scanned, and audited the moment you call
76
+ it — **no adapters, no per-framework wiring**. That includes the OpenAI SDK, the Anthropic SDK,
77
+ LangChain / LangGraph, CrewAI, AutoGen, and LlamaIndex (all of which use `requests`/`httpx` under the
78
+ hood). Call `auto_instrument()` once at process start, before your agent makes its first request.
79
+
80
+ ### Manual / advanced — wrap calls yourself
81
+
82
+ ```python
83
+ import asyncio
84
+
85
+ from sphyr_sdk.client import AsyncSphyrClient
86
+ from sphyr_sdk.errors import SphyrError
87
+
88
+
89
+ async def main() -> None:
90
+ client = AsyncSphyrClient() # resolves creds from env / ~/.sphyr/config.json
91
+ try:
92
+ result = await client.call({"url": "https://api.example.com/data", "mthd": "GET"})
93
+ print(result)
94
+ except SphyrError as err:
95
+ print(err.code, err.retryable, err.docs_url)
96
+
97
+
98
+ asyncio.run(main())
99
+ ```
100
+
101
+ ### 4. Add the MCP Server (Claude Desktop, Cursor, etc.)
102
+
103
+ **Recommended — one command:**
104
+
105
+ ```bash
106
+ npx @sphyr/cli guard init
107
+ ```
108
+
109
+ 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`.
110
+
111
+ See [sphyr.io/docs/mcp](https://sphyr.io/docs/mcp) for per-client setup (Claude Desktop, Claude Code CLI, Cursor, Antigravity, OpenAI Codex).
112
+
113
+ Full reference (retry policy, custom transport, timeout config, error catalog): [sphyr.io/docs](https://sphyr.io/docs)
114
+
115
+ ## What traffic is intercepted (and what is not)
116
+
117
+ `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.
118
+
119
+ | Transport | Intercepted? |
120
+ |---|---|
121
+ | `requests` (Session-based and module-level) | ✅ yes |
122
+ | `httpx` (`Client` and `AsyncClient`) | ✅ yes |
123
+ | `aiohttp` | ❌ no — flows unguarded |
124
+ | `urllib` / `urllib.request` / `http.client` | ❌ no — flows unguarded |
125
+ | `urllib3` (used directly, not via requests) | ❌ no — flows unguarded |
126
+ | `pycurl` / subprocess `curl` | ❌ no — flows unguarded |
127
+
128
+ If your agent uses an unsupported transport, route those calls through `SphyrClient.call()` explicitly, or switch the agent's HTTP layer to `requests`/`httpx`.
129
+
130
+ **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.
131
+
132
+ ## Tool Schema Drift Detection
133
+
134
+ 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).
135
+
136
+ ```python
137
+ from sphyr_sdk import SphyrClient, SphyrDriftError
138
+
139
+ try:
140
+ result = await client.call({"url": "https://mcp.example.com/...", "mthd": "GET"})
141
+ except SphyrDriftError as err:
142
+ # Tool schema changed unexpectedly — halt and alert
143
+ print(err.code) # "TOOL_SCHEMA_DRIFT"
144
+ ```
145
+
146
+ `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.
147
+
148
+ ## User-Agent header (custom integrations)
149
+
150
+ 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.
151
+
152
+ 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.
@@ -0,0 +1,134 @@
1
+ # sphyr-sdk
2
+
3
+ Python SDK for [Sphyr Agent Guard](https://sphyr.io). Adds verifiable, auditable security checks to every outbound agent request.
4
+
5
+ ## Quick-Start
6
+
7
+ ### 1. Install
8
+
9
+ ```bash
10
+ pip install sphyr-sdk
11
+ ```
12
+
13
+ ### 2. Sign in (no key copy-paste)
14
+
15
+ ```bash
16
+ npx @sphyr/cli login
17
+ ```
18
+
19
+ Opens your browser, signs you in via the device flow, and writes your credential to
20
+ `~/.sphyr/config.json` (owner-only `0600`). After this, the SDK picks it up automatically.
21
+
22
+ > No Node/npx on your machine? Run `python -m sphyr_sdk login` for the same device-flow sign-in.
23
+ >
24
+ > Setting up an IDE-based agent (Claude Desktop, Cursor, etc.) instead? Use
25
+ > `npx @sphyr/cli guard init` — it signs you in **and** writes the MCP server config for each
26
+ > detected IDE.
27
+
28
+ ### 3. Protect every outbound call (recommended — install and forget)
29
+
30
+ ```python
31
+ from sphyr_sdk import auto_instrument
32
+
33
+ # One line at startup. After this, every requests / httpx call is signed, scanned,
34
+ # and audited through Sphyr automatically — no per-call wrapping.
35
+ report = auto_instrument()
36
+ print(report.requests.status) # 'patched' or 'not_installed'
37
+ print(report.httpx.status) # 'patched' or 'not_installed'
38
+ ```
39
+
40
+ > **Breaking change (v0.27.0):** `auto_instrument()` now returns a `PatchReport` (per-lib
41
+ > instrumentation status) instead of a `SphyrClient`. Callers that did
42
+ > `client = auto_instrument(...)` must update — use `SphyrClient(...).instrument()` directly
43
+ > if you need a client reference for later teardown.
44
+ >
45
+ > **The SDK does nothing until you call `auto_instrument()`** — importing the package alone is not protective.
46
+ >
47
+ > **Fail-closed by default:** if the Sphyr gateway is unreachable, instrumented requests raise
48
+ > `SphyrNetworkError` rather than leaving unscreened. Pass `auto_instrument(fail_closed=False)` to
49
+ > prioritize app uptime over strict enforcement.
50
+
51
+ Credentials resolve in order: **explicit args → `SPHYR_CREDENTIAL` env var →
52
+ `~/.sphyr/config.json`**. So `auto_instrument()` with no arguments works after `npx @sphyr/cli login`.
53
+
54
+ ### Works with your agent framework — automatically
55
+
56
+ `auto_instrument()` patches the underlying HTTP layer (`requests` and `httpx`), so any library or
57
+ framework that makes outbound calls through them is signed, scanned, and audited the moment you call
58
+ it — **no adapters, no per-framework wiring**. That includes the OpenAI SDK, the Anthropic SDK,
59
+ LangChain / LangGraph, CrewAI, AutoGen, and LlamaIndex (all of which use `requests`/`httpx` under the
60
+ hood). Call `auto_instrument()` once at process start, before your agent makes its first request.
61
+
62
+ ### Manual / advanced — wrap calls yourself
63
+
64
+ ```python
65
+ import asyncio
66
+
67
+ from sphyr_sdk.client import AsyncSphyrClient
68
+ from sphyr_sdk.errors import SphyrError
69
+
70
+
71
+ async def main() -> None:
72
+ client = AsyncSphyrClient() # resolves creds from env / ~/.sphyr/config.json
73
+ try:
74
+ result = await client.call({"url": "https://api.example.com/data", "mthd": "GET"})
75
+ print(result)
76
+ except SphyrError as err:
77
+ print(err.code, err.retryable, err.docs_url)
78
+
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ### 4. Add the MCP Server (Claude Desktop, Cursor, etc.)
84
+
85
+ **Recommended — one command:**
86
+
87
+ ```bash
88
+ npx @sphyr/cli guard init
89
+ ```
90
+
91
+ 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`.
92
+
93
+ See [sphyr.io/docs/mcp](https://sphyr.io/docs/mcp) for per-client setup (Claude Desktop, Claude Code CLI, Cursor, Antigravity, OpenAI Codex).
94
+
95
+ Full reference (retry policy, custom transport, timeout config, error catalog): [sphyr.io/docs](https://sphyr.io/docs)
96
+
97
+ ## What traffic is intercepted (and what is not)
98
+
99
+ `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.
100
+
101
+ | Transport | Intercepted? |
102
+ |---|---|
103
+ | `requests` (Session-based and module-level) | ✅ yes |
104
+ | `httpx` (`Client` and `AsyncClient`) | ✅ yes |
105
+ | `aiohttp` | ❌ no — flows unguarded |
106
+ | `urllib` / `urllib.request` / `http.client` | ❌ no — flows unguarded |
107
+ | `urllib3` (used directly, not via requests) | ❌ no — flows unguarded |
108
+ | `pycurl` / subprocess `curl` | ❌ no — flows unguarded |
109
+
110
+ If your agent uses an unsupported transport, route those calls through `SphyrClient.call()` explicitly, or switch the agent's HTTP layer to `requests`/`httpx`.
111
+
112
+ **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.
113
+
114
+ ## Tool Schema Drift Detection
115
+
116
+ 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).
117
+
118
+ ```python
119
+ from sphyr_sdk import SphyrClient, SphyrDriftError
120
+
121
+ try:
122
+ result = await client.call({"url": "https://mcp.example.com/...", "mthd": "GET"})
123
+ except SphyrDriftError as err:
124
+ # Tool schema changed unexpectedly — halt and alert
125
+ print(err.code) # "TOOL_SCHEMA_DRIFT"
126
+ ```
127
+
128
+ `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.
129
+
130
+ ## User-Agent header (custom integrations)
131
+
132
+ 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.
133
+
134
+ 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.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sphyr-sdk"
7
+ version = "2.0.0-beta.1"
8
+ description = "Python SDK for Sphyr Agent Guard"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "httpx>=0.28.0,<1.0",
13
+ "pydantic>=2.13.0,<3.0",
14
+ # Phase 134 D-12: Ed25519 verification for offline denial-proof verify helper.
15
+ "cryptography>=42.0,<49.0",
16
+ # requests is mandatory (not an optional extra) so the default `pip install sphyr-sdk`
17
+ # guarantees auto_instrument() can patch requests — the dominant agent HTTP library.
18
+ # Previously optional ([instrument] extra); a user who installed without the extra and
19
+ # relied on the SDK to guard requests calls got a silent no-op. Making it mandatory closes
20
+ # that gap at the source without a runtime warning. The [instrument] extra is retained below
21
+ # for backward-compatibility of `pip install sphyr-sdk[instrument]`.
22
+ "requests>=2.32,<3.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ # Retained for backward-compat — requests is now a mandatory dependency (see above).
27
+ instrument = ["requests>=2.32,<3.0"]
28
+ # Dev tooling for coverage (F-009/F-019/F-020) and freezegun timing tests (F-046, plan 92-04 handoff).
29
+ dev = ["pytest-cov>=7.0,<8.0", "freezegun>=1.5,<2.0", "pytest-asyncio>=0.25,<2.0", "requests>=2.32,<3.0"]
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["."]
33
+ include = ["sphyr_sdk*"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ python_files = ["test_*.py"]
38
+ # Coverage measurement on every pytest run (F-019 enables branch coverage; fail_under=79 from [tool.coverage.report]).
39
+ addopts = "-q --cov --cov-report=term-missing"
40
+ asyncio_mode = "auto"
41
+ pythonpath = [".", "../.."]
42
+
43
+ # F-019: enable branch coverage for sdk/python (previously UNMEASURED).
44
+ [tool.coverage.run]
45
+ branch = true
46
+ source = ["sphyr_sdk"]
47
+
48
+ # F-009: enforce coverage floor per 92-RESEARCH §3 ratchet plan.
49
+ # Floor stays at 79% — proxy.py removal removes the excluded file but the
50
+ # measured core coverage has not been re-baselined yet. Raise after next CI run.
51
+ [tool.coverage.report]
52
+ fail_under = 79
53
+ show_missing = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,150 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Sphyr Agent Guard Contributors
3
+
4
+ """Python SDK package for Sphyr Agent Guard."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Literal, Optional
10
+
11
+ from sphyr_sdk.client import SphyrClient, AsyncSphyrClient
12
+ from sphyr_sdk._session_models import AuditSessResponse
13
+ from sphyr_sdk.errors import (
14
+ SphyrError,
15
+ SphyrAuthError,
16
+ SphyrRateLimitError,
17
+ SphyrSecurityError,
18
+ SphyrDriftError,
19
+ SphyrNetworkError,
20
+ SphyrBillingError,
21
+ SphyrConfigError,
22
+ SphyrRequestError,
23
+ DegradedModeError,
24
+ make_sphyr_error,
25
+ )
26
+ from sphyr_sdk.verify import verify_denial_proof_locally, VerifyResult
27
+
28
+ # _signing is intentionally excluded — it is an internal primitive, not public API.
29
+
30
+
31
+ @dataclass
32
+ class LibStatus:
33
+ """Per-library instrumentation status returned by auto_instrument().
34
+
35
+ status:
36
+ 'patched' — the HTTP library was found and successfully patched.
37
+ 'not_installed' — the library is not installed in this environment.
38
+ reason:
39
+ Optional human-readable note (e.g. an edge-case explanation). None in
40
+ the common cases — 'not_installed' is self-explanatory.
41
+ """
42
+
43
+ status: Literal["patched", "not_installed"]
44
+ reason: Optional[str] = None
45
+
46
+
47
+ @dataclass
48
+ class PatchReport:
49
+ """Structured report of which HTTP libraries were patched by auto_instrument().
50
+
51
+ Breaking change (v0.27.0): auto_instrument() now returns a PatchReport
52
+ instead of a SphyrClient. Callers that did `client = auto_instrument(...)`
53
+ must update — uninstrumentation is via the module-level sentinels in
54
+ sphyr_sdk._instrument, or by constructing a SphyrClient and calling
55
+ .uninstrument() directly.
56
+
57
+ Attributes:
58
+ requests: Status of the `requests` library patch.
59
+ httpx: Status of the `httpx` library patch (covers both sync and async clients).
60
+ """
61
+
62
+ requests: LibStatus
63
+ httpx: LibStatus
64
+
65
+
66
+ def auto_instrument(
67
+ *,
68
+ credential: Optional[str] = None,
69
+ mcp_url: Optional[str] = None,
70
+ fail_closed: bool = True,
71
+ ) -> PatchReport:
72
+ """One-line Sphyr setup: patches all HTTP libraries and returns a PatchReport.
73
+
74
+ Mirrors the sentry_sdk.init() pattern. After this call, every outbound
75
+ request via `requests` or `httpx` is automatically signed, scanned, and
76
+ audited through Sphyr.
77
+
78
+ Credential resolves inside SphyrClient with precedence: explicit credential >
79
+ SPHYR_CREDENTIAL env > ~/.sphyr/config.json. So `auto_instrument()` with no
80
+ arguments works after `npx sphyr login`.
81
+
82
+ Args:
83
+ credential: Single sphyr_v1_<keyId>.<signingSecret> credential string.
84
+ Optional — falls back to SPHYR_CREDENTIAL env, then config file.
85
+ mcp_url: Sphyr MCP gateway URL. Optional — falls back to SPHYR_MCP_URL, config file, then public endpoint.
86
+ fail_closed: If True (default), raise SphyrNetworkError when the gateway is
87
+ unreachable so nothing leaves unscreened. Set False to log an
88
+ UNGUARDED warning and pass through (uptime over strict enforcement).
89
+
90
+ Returns:
91
+ PatchReport — per-library instrumentation status. Use to confirm which
92
+ HTTP libraries were patched. The internal SphyrClient is NOT returned;
93
+ use SphyrClient(...).instrument() directly if you need a client reference.
94
+
95
+ Example:
96
+ >>> from sphyr_sdk import auto_instrument
97
+ >>> report = auto_instrument(
98
+ ... credential=os.environ["SPHYR_CREDENTIAL"],
99
+ ... )
100
+ >>> print(report.requests.status) # 'patched' or 'not_installed'
101
+ >>> # Every requests.get() / httpx.get() call is now signed automatically.
102
+ """
103
+ # WR-05: warn if instrumentation is already active so silent credential-rotation
104
+ # bugs are detectable. Lazy import mirrors the pattern below.
105
+ import warnings # noqa: PLC0415
106
+ from sphyr_sdk import _instrument as _instr_check # noqa: PLC0415
107
+ if _instr_check._orig_session_init is not None or _instr_check._orig_client_init is not None:
108
+ warnings.warn(
109
+ "[sphyr] auto_instrument() called while instrumentation is already active. "
110
+ "New credentials ignored — call uninstrument() first if you need to re-instrument.",
111
+ stacklevel=2,
112
+ )
113
+ client = SphyrClient(credential=credential, mcp_url=mcp_url)
114
+ client.instrument(fail_closed=fail_closed)
115
+ # Query actual module-level sentinels to determine which libs were patched.
116
+ # Lazy import avoids top-level import cycle (client.py also imports _instrument lazily).
117
+ from sphyr_sdk import _instrument # noqa: PLC0415
118
+ req_status: Literal["patched", "not_installed"] = (
119
+ "patched" if _instrument._orig_session_init is not None else "not_installed"
120
+ )
121
+ httpx_status: Literal["patched", "not_installed"] = (
122
+ "patched" if _instrument._orig_client_init is not None else "not_installed"
123
+ )
124
+ return PatchReport(
125
+ requests=LibStatus(status=req_status),
126
+ httpx=LibStatus(status=httpx_status),
127
+ )
128
+
129
+
130
+ __all__ = [
131
+ "SphyrClient",
132
+ "AsyncSphyrClient",
133
+ "AuditSessResponse",
134
+ "SphyrError",
135
+ "SphyrAuthError",
136
+ "SphyrRateLimitError",
137
+ "SphyrSecurityError",
138
+ "SphyrDriftError",
139
+ "SphyrNetworkError",
140
+ "SphyrBillingError",
141
+ "SphyrConfigError",
142
+ "SphyrRequestError",
143
+ "DegradedModeError",
144
+ "make_sphyr_error",
145
+ "auto_instrument",
146
+ "verify_denial_proof_locally",
147
+ "VerifyResult",
148
+ "LibStatus",
149
+ "PatchReport",
150
+ ]
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 Sphyr Agent Guard Contributors
3
+
4
+ """Argparse dispatcher for `python -m sphyr_sdk`.
5
+
6
+ Subcommands (D-02):
7
+ login — device-flow login via RFC 8628, writes ~/.sphyr/config.json
8
+ verify — connectivity verify: audit_sess handshake + agent_guard_up round-trip
9
+
10
+ Usage:
11
+ python -m sphyr_sdk login
12
+ python -m sphyr_sdk verify [--json]
13
+
14
+ No subcommand or unknown subcommand prints help and exits 1 (Pitfall 8).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import sys
21
+
22
+
23
+ def main() -> None:
24
+ """Parse argv and dispatch to the appropriate subcommand."""
25
+ parser = argparse.ArgumentParser(
26
+ prog="python -m sphyr_sdk",
27
+ description="Sphyr Agent Guard SDK CLI",
28
+ )
29
+ subparsers = parser.add_subparsers(dest="command")
30
+
31
+ # login subcommand
32
+ subparsers.add_parser(
33
+ "login",
34
+ help="Device-flow login — opens a browser and writes ~/.sphyr/config.json",
35
+ )
36
+
37
+ # verify subcommand
38
+ verify_parser = subparsers.add_parser(
39
+ "verify",
40
+ help="Verify gateway connectivity (audit_sess handshake + agent_guard_up)",
41
+ )
42
+ verify_parser.add_argument(
43
+ "--json",
44
+ action="store_true",
45
+ default=False,
46
+ help="Emit machine-readable JSON output matching the TS sphyr guard verify schema",
47
+ )
48
+
49
+ args = parser.parse_args()
50
+
51
+ # Pitfall 8: no subcommand → print help and exit 1
52
+ if not args.command:
53
+ parser.print_help()
54
+ sys.exit(1)
55
+
56
+ if args.command == "login":
57
+ from sphyr_sdk._login import run_login_command # noqa: PLC0415
58
+ sys.exit(run_login_command())
59
+
60
+ if args.command == "verify":
61
+ from sphyr_sdk._connectivity import run_verify_command # noqa: PLC0415
62
+ sys.exit(run_verify_command(json_mode=getattr(args, "json", False)))
63
+
64
+ # Unknown command (defensive — argparse should handle most cases above)
65
+ parser.print_help()
66
+ sys.exit(1)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()