sphyr-sdk 2.0.0b1__py3-none-any.whl
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.
- sphyr_sdk/__init__.py +150 -0
- sphyr_sdk/__main__.py +70 -0
- sphyr_sdk/_connectivity.py +207 -0
- sphyr_sdk/_instrument.py +476 -0
- sphyr_sdk/_login.py +191 -0
- sphyr_sdk/_session_models.py +46 -0
- sphyr_sdk/_signing.py +97 -0
- sphyr_sdk/_tool_drift.py +329 -0
- sphyr_sdk/_url_guard.py +91 -0
- sphyr_sdk/client.py +541 -0
- sphyr_sdk/errors.py +316 -0
- sphyr_sdk/generated_models.py +71 -0
- sphyr_sdk/idempotency_fingerprint.py +75 -0
- sphyr_sdk/retry_policy.py +172 -0
- sphyr_sdk/verify.py +97 -0
- sphyr_sdk-2.0.0b1.dist-info/METADATA +152 -0
- sphyr_sdk-2.0.0b1.dist-info/RECORD +19 -0
- sphyr_sdk-2.0.0b1.dist-info/WHEEL +5 -0
- sphyr_sdk-2.0.0b1.dist-info/top_level.txt +1 -0
sphyr_sdk/__init__.py
ADDED
|
@@ -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
|
+
]
|
sphyr_sdk/__main__.py
ADDED
|
@@ -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()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Sphyr Agent Guard Contributors
|
|
3
|
+
|
|
4
|
+
"""Gateway connectivity verify for `python -m sphyr_sdk verify`.
|
|
5
|
+
|
|
6
|
+
This module implements the `verify` subcommand — a port of sdk/ts/guard-verify.ts.
|
|
7
|
+
It performs a real AUTH-08 audit_sess handshake followed by an agent_guard_up
|
|
8
|
+
round-trip and reports per-library instrumentation status.
|
|
9
|
+
|
|
10
|
+
DISTINCT from verify.py (sphyr_sdk.verify), which is the offline Ed25519
|
|
11
|
+
denial-receipt verifier and has no network dependency.
|
|
12
|
+
|
|
13
|
+
Security:
|
|
14
|
+
- T-137-16: Calls agent_guard_up by literal tool name via JSON-RPC — never the
|
|
15
|
+
legacy tool name, and never via AgentGuardClient.agent_guard_up()
|
|
16
|
+
(which calls an unregistered tool name internally)
|
|
17
|
+
- T-137-17: Credential never printed — only pass/fail status is emitted
|
|
18
|
+
- T-137-18: --json output schema is byte-for-byte compatible with TS verify schema
|
|
19
|
+
(only instrumentation keys differ: requests/httpx vs fetch/axios)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import hmac
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
import uuid
|
|
30
|
+
from typing import Any, Dict
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
from sphyr_sdk import auto_instrument
|
|
35
|
+
from sphyr_sdk.client import _resolve_credentials, _parse_credential
|
|
36
|
+
from sphyr_sdk._instrument import _unpatch_requests, _unpatch_httpx
|
|
37
|
+
|
|
38
|
+
_DEFAULT_MCP_URL = "https://api.sphyr.io/mcp"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _call_tool_once(mcp_url: str, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
+
"""POST a JSON-RPC tools/call envelope and return the parsed result dict.
|
|
43
|
+
|
|
44
|
+
Raises on HTTP error, envelope-level error, or malformed response.
|
|
45
|
+
"""
|
|
46
|
+
envelope = {
|
|
47
|
+
"jsonrpc": "2.0",
|
|
48
|
+
"method": "tools/call",
|
|
49
|
+
"params": {"name": name, "arguments": args},
|
|
50
|
+
"id": "1",
|
|
51
|
+
}
|
|
52
|
+
response = httpx.post(
|
|
53
|
+
mcp_url,
|
|
54
|
+
json=envelope,
|
|
55
|
+
headers={
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Accept": "application/json, text/event-stream",
|
|
58
|
+
},
|
|
59
|
+
timeout=10.0,
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
body: Dict[str, Any] = response.json()
|
|
63
|
+
if "error" in body:
|
|
64
|
+
raise RuntimeError(f"MCP error: {body['error']}")
|
|
65
|
+
result_text: str = body["result"]["content"][0]["text"]
|
|
66
|
+
return json.loads(result_text)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_audit_sess_args(key_id: str, signing_secret: str) -> Dict[str, str]:
|
|
70
|
+
"""Build the AUTH-08 audit_sess argument dict.
|
|
71
|
+
|
|
72
|
+
Payload: key_id:agent_id:ts:client_nonce (colon-joined, HMAC-SHA256 hex).
|
|
73
|
+
agent_id is a fresh UUID per verification run (non-persistent, per D-03).
|
|
74
|
+
"""
|
|
75
|
+
agent_id = str(uuid.uuid4())
|
|
76
|
+
ts = str(int(time.time()))
|
|
77
|
+
client_nonce = str(uuid.uuid4())
|
|
78
|
+
payload = f"{key_id}:{agent_id}:{ts}:{client_nonce}"
|
|
79
|
+
sig = hmac.new(
|
|
80
|
+
signing_secret.encode("utf-8"),
|
|
81
|
+
payload.encode("utf-8"),
|
|
82
|
+
hashlib.sha256,
|
|
83
|
+
).hexdigest()
|
|
84
|
+
return {
|
|
85
|
+
"key_id": key_id,
|
|
86
|
+
"agent_id": agent_id,
|
|
87
|
+
"ts": ts,
|
|
88
|
+
"client_nonce": client_nonce,
|
|
89
|
+
"sig": sig,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_verify_command(json_mode: bool = False) -> int: # noqa: PLR0912 -- verify sequence: credential → probe → teardown → handshake → agent_guard_up → render; ordering invariant breaks if split (teardown must precede network calls)
|
|
94
|
+
"""Run the connectivity verify command.
|
|
95
|
+
|
|
96
|
+
Steps mirror guard-verify.ts:
|
|
97
|
+
1. Resolve credential (env > config file)
|
|
98
|
+
2. Probe instrumentation, then tear it down BEFORE any MCP network call
|
|
99
|
+
3. AUTH-08 audit_sess handshake
|
|
100
|
+
4. agent_guard_up round-trip (direct JSON-RPC by name — T-137-16)
|
|
101
|
+
5. Render output (--json or human-readable)
|
|
102
|
+
6. Return 0 on full pass, 1 on any failure
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
0 if gateway reachable + handshake ok + agent_guard_up ok
|
|
106
|
+
1 otherwise
|
|
107
|
+
"""
|
|
108
|
+
# ── Step 1: Resolve credential ──────────────────────────────────────────────
|
|
109
|
+
credential, mcp_url = _resolve_credentials(None, None)
|
|
110
|
+
if not credential:
|
|
111
|
+
print(
|
|
112
|
+
"No credential found. Run 'python -m sphyr_sdk login' or set SPHYR_CREDENTIAL.",
|
|
113
|
+
file=sys.stderr,
|
|
114
|
+
)
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
key_id, signing_secret = _parse_credential(credential)
|
|
118
|
+
|
|
119
|
+
# ── Step 2: Probe instrumentation, then uninstrument BEFORE any network call ─
|
|
120
|
+
# Mirror guard-verify.ts ordering (T-136-10 / T-137-18): probe so we can report
|
|
121
|
+
# status, then tear down immediately so MCP traffic doesn't route through Sphyr.
|
|
122
|
+
try:
|
|
123
|
+
report = auto_instrument(credential=credential, mcp_url=mcp_url)
|
|
124
|
+
finally:
|
|
125
|
+
_unpatch_requests()
|
|
126
|
+
_unpatch_httpx()
|
|
127
|
+
|
|
128
|
+
# ── Step 3 + 4: handshake + agent_guard_up ─────────────────────────────────
|
|
129
|
+
gateway_reachable = False
|
|
130
|
+
handshake_ok = False
|
|
131
|
+
agent_guard_up_ok = False
|
|
132
|
+
sess_balance: float | None = None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
audit_sess_args = _build_audit_sess_args(key_id, signing_secret)
|
|
136
|
+
audit_sess_data = _call_tool_once(mcp_url, "audit_sess", audit_sess_args)
|
|
137
|
+
|
|
138
|
+
gateway_reachable = True # audit_sess responded (even with an error body)
|
|
139
|
+
|
|
140
|
+
sess_token = audit_sess_data.get("sess_token")
|
|
141
|
+
sess_balance = audit_sess_data.get("balance")
|
|
142
|
+
if sess_token:
|
|
143
|
+
handshake_ok = True
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
# T-137-16: MUST call "agent_guard_up" by name via JSON-RPC.
|
|
147
|
+
# Direct JSON-RPC only — no legacy client wrappers.
|
|
148
|
+
guard_up_data = _call_tool_once(
|
|
149
|
+
mcp_url, "agent_guard_up", {"sess_token": sess_token}
|
|
150
|
+
)
|
|
151
|
+
status = guard_up_data.get("status")
|
|
152
|
+
if status in ("ACTIVE", "DEGRADED"):
|
|
153
|
+
agent_guard_up_ok = True
|
|
154
|
+
except Exception:
|
|
155
|
+
agent_guard_up_ok = False
|
|
156
|
+
|
|
157
|
+
except Exception:
|
|
158
|
+
# fetch error, HTTP error, or JSON parse failure — gateway unreachable / handshake failed
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
# ── Step 5: Compute overall pass/fail ───────────────────────────────────────
|
|
162
|
+
ok = gateway_reachable and handshake_ok and agent_guard_up_ok
|
|
163
|
+
|
|
164
|
+
# ── Step 6: Render output ───────────────────────────────────────────────────
|
|
165
|
+
if json_mode:
|
|
166
|
+
# T-137-18: schema must match TS verify --json exactly (instrumentation keys differ)
|
|
167
|
+
output = {
|
|
168
|
+
"ok": ok,
|
|
169
|
+
"gateway": {"reachable": gateway_reachable, "url": mcp_url},
|
|
170
|
+
"handshake": {"ok": handshake_ok, "balance": sess_balance},
|
|
171
|
+
"agent_guard_up": {"ok": agent_guard_up_ok},
|
|
172
|
+
"instrumentation": {
|
|
173
|
+
"requests": {"status": report.requests.status},
|
|
174
|
+
"httpx": {"status": report.httpx.status},
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
print(json.dumps(output))
|
|
178
|
+
else:
|
|
179
|
+
# Human-readable summary (T-137-17: credential never printed)
|
|
180
|
+
print("✓ Credential loaded")
|
|
181
|
+
if gateway_reachable:
|
|
182
|
+
try:
|
|
183
|
+
from urllib.parse import urlparse as _urlparse # noqa: PLC0415
|
|
184
|
+
host = _urlparse(mcp_url).hostname or mcp_url
|
|
185
|
+
except Exception:
|
|
186
|
+
host = mcp_url
|
|
187
|
+
print(f"✓ Gateway reachable ({host})")
|
|
188
|
+
else:
|
|
189
|
+
print("✗ Gateway unreachable")
|
|
190
|
+
|
|
191
|
+
print("✓ Handshake succeeded" if handshake_ok else "✗ Handshake failed")
|
|
192
|
+
print(
|
|
193
|
+
"✓ agent_guard_up responded"
|
|
194
|
+
if agent_guard_up_ok
|
|
195
|
+
else "✗ agent_guard_up did not respond"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
for lib_name, lib_status in [("requests", report.requests), ("httpx", report.httpx)]:
|
|
199
|
+
if lib_status.status == "patched":
|
|
200
|
+
print(f"✓ {lib_name}: patched")
|
|
201
|
+
else:
|
|
202
|
+
print(f"- {lib_name}: not installed")
|
|
203
|
+
|
|
204
|
+
print()
|
|
205
|
+
print("sphyr-mcp: OK" if ok else "sphyr-mcp: FAILED")
|
|
206
|
+
|
|
207
|
+
return 0 if ok else 1
|