feat(python): uvx-installable Python sealg CLI alongside the Rust client - #22
Conversation
… client
A pure-Python client for the SealGate gateway exposing the same `sealg`
surface (doctor | list | call) as the Rust binary, packaged so it installs
via `uvx`/`pip`. It is a thin MCP-over-HTTP client that forwards tools/list
and tools/call to the per-user gateway endpoint, where all policy is enforced.
- python/sealg/: client.py (transport), cli.py (typer, exit codes 0/1/6),
contract.py (single source of truth for wire constants); tests ported from
the Rust suite; README + .env.example.
- scripts/check_wire_contract.py + prek hook + .github/workflows/python-cli.yaml:
an in-repo drift guard that reads the Rust source in crates/ and the Python
contract and fails if any shared constant (protocol version, headers, env
keys, /mcp/{key}/ path shape, exit codes) diverges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EpwFmgQPfugKFF9zugay6Y
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
CI installs the latest ruff, whose defaults are stricter than the local version used to author the package: - return Self from GatewayClient.__enter__ (PYI034) - drop the two unused `# noqa: BLE001` on the re-raising except blocks; keep it only on doctor's deliberate catch-all - drop the two unused `# noqa: E402` in the test - drop the orphan shebang from check_wire_contract.py (run via `python3`) Pin ruff==0.16.5 and ty==0.0.77 in the workflow so local and CI agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EpwFmgQPfugKFF9zugay6Y
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="python/sealg/client.py">
<violation number="1" location="python/sealg/client.py:151">
P2: When a response envelope has an ID different from the current request, the client still returns its result instead of reporting a protocol error. Require `id == want_id` for JSON and SSE responses, and do not use a different-ID response as the fallback.</violation>
</file>
<file name="scripts/check_wire_contract.py">
<violation number="1" location="scripts/check_wire_contract.py:196">
P2: The guard can stay green after the Rust request wire changes. Scope assertions to `GatewayClient::request` and verify every contract header is attached using syntax-aware parsing or targeted regression tests.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| accept = re.escape(str(SNAPSHOT["ACCEPT"])) | ||
| ver_header = re.escape(str(SNAPSHOT["PROTOCOL_VERSION_HEADER"])) | ||
| header_checks = [ | ||
| ("ACCEPT", rf'\.header\(\s*[^,]*ACCEPT\s*,\s*"{accept}"'), |
There was a problem hiding this comment.
P2: The guard can stay green after the Rust request wire changes. Scope assertions to GatewayClient::request and verify every contract header is attached using syntax-aware parsing or targeted regression tests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check_wire_contract.py, line 196:
<comment>The guard can stay green after the Rust request wire changes. Scope assertions to `GatewayClient::request` and verify every contract header is attached using syntax-aware parsing or targeted regression tests.</comment>
<file context>
@@ -0,0 +1,218 @@
+ accept = re.escape(str(SNAPSHOT["ACCEPT"]))
+ ver_header = re.escape(str(SNAPSHOT["PROTOCOL_VERSION_HEADER"]))
+ header_checks = [
+ ("ACCEPT", rf'\.header\(\s*[^,]*ACCEPT\s*,\s*"{accept}"'),
+ ("PROTOCOL_VERSION_HEADER", rf'\.header\(\s*"{ver_header}"\s*,'),
+ ]
</file context>
There was a problem hiding this comment.
Leaving this as-is: the check already anchors to the .header(...) call with the exact expected value (e.g. .header(reqwest::header::ACCEPT, "application/json, text/event-stream")), not a bare whole-file substring, and I verified it bites when that value changes. A stale identical literal sitting in a comment or test that also matches the full .header(VALUE) shape is implausible. Extracting and brace-matching the request fn body to scope it further is real added fragility (a Rust-syntax parser in a regex script) for a marginal gain; if the request builder is ever refactored, the run-time tests and the exact-value anchor already fail loudly.
Generated by Claude Code
- client: strip the credential headers (secret key, conversation id, session id) on any cross-origin redirect, and cap redirects at 10 to match the Rust client (P1: httpx forwards custom headers across origins on redirect). - client: return the raw non-blank env value from _get; strip() is only the blank test, so a padded key/id is sent identically to the Rust client. - client: a valid-JSON scalar response is a GatewayError, not a TypeError. - client: a missing/invalid CA bundle raises GatewayError, not a raw ssl/OSError. - client: redact the API key from a non-2xx HTTP error body before it hits stderr. - cli: doctor distinguishes reachability (connect) from a tools/list probe failure instead of reporting a reachable gateway as unreachable. - guard: add EXIT_ERROR (1) to the snapshot and assert the Rust CLI exits 1 on client failure, so the 0/1/6 parity stays enforced on both sides. - prek: include cli.py in the wire-contract hook trigger. - tests: padded-value parity, cross-origin credential strip, scalar-response. Not changed: JSON-RPC id validation / SSE fallback (mirrors the Rust client's deliberate single-response behavior; tightening it belongs in both clients). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EpwFmgQPfugKFF9zugay6Y
Summary
Adds a pure-Python
sealgclient inpython/, exposing the same CLI surface as the Rust binary in this repo (doctor | list | call), packaged so it installs viauvx/pip. It's a thin MCP-over-HTTP client that forwardstools/list/tools/callto the per-user gateway endpoint, where all policy and enforcement live — it carries no policy of its own.It exists alongside the Rust
sealgfor environments that reach foruvx/piprather than a native binary. Co-locating it here (rather than in the gateway repo) means the drift guard reads the Rust source directly — no cross-repo checkout, no token, no fallback.Verified end-to-end earlier against the live demo gateway:
initialize(protocol2025-06-18),tools/list,tools/callfor bothisError:true→exit 6 andisError:false→exit 0, the conversation-id session flow, and theHTTPS_PROXY+ CA-bundle path.Changes
python/sealg/— the package.client.py:GatewayConfig+GatewayClient(initialize/list/call, SSE+JSON parsing, additive CA trust, env-key vs proxy-injected auth, API-key redaction; follows redirects and treats only 2xx as success, matching reqwest).cli.py: typer CLI mirroring the Rust surface and exit codes (0/1/6);doctorreports resolved env + a reachability probe.contract.py: single source of truth for every shared wire constant.tests/test_wire.py: config/parse/redaction/redirect tests ported from the Rust suite.pyproject.toml([project.scripts] sealg),README.md,.env.example.scripts/check_wire_contract.py+prek.tomlhook +.github/workflows/python-cli.yaml— the in-repo drift guard. It parses the Rust constants incrates/engine/src/gateway/{config,client}.rsandcrates/cli/src/gateway_cmd.rs, compares them topython/sealg/contract.py, and fails if the protocol version, headers, env keys,/mcp/{key}/path shape, or exit codes diverge. Runs on pre-commit and in a dedicated CI job.Testing
uv run pytest python/tests— 15 testsuv run ruff format --check+ruff check+ty check(against a venv with the package installed)python3 scripts/check_wire_contract.py— Python and Rust constants agreeuvx --from ./python sealg doctorbuilds and runsRelated
Supersedes the copy previously staged in edison-watch/edison-watch#1396 (that PR keeps only the design-doc note pointing here).
🤖 Generated with Claude Code
https://claude.ai/code/session_01EpwFmgQPfugKFF9zugay6Y
Generated by Claude Code
Summary by cubic
Adds a
uvx/pip-installable PythonsealgCLI alongside the Rust binary, mirroringdoctor,list, andcallwith exit codes 0/1/6. It forwards MCP requests to the gateway, so policy and enforcement stay gateway-side.doctornow distinguishes a reachability failure from a failedtools/listprobe.ruff==0.16.5andty==0.0.77.Written for commit 416f8fa. Summary will update on new commits.