Skip to content

feat(python): uvx-installable Python sealg CLI alongside the Rust client - #22

Merged
Miyamura80 merged 3 commits into
mainfrom
claude/python-sealg-cli
Sep 2, 2026
Merged

feat(python): uvx-installable Python sealg CLI alongside the Rust client#22
Miyamura80 merged 3 commits into
mainfrom
claude/python-sealg-cli

Conversation

@Miyamura80

@Miyamura80 Miyamura80 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a pure-Python sealg client in python/, exposing the same CLI surface as the Rust binary in this repo (doctor | list | call), packaged so it installs via uvx/pip. It's a thin MCP-over-HTTP client that forwards tools/list / tools/call to the per-user gateway endpoint, where all policy and enforcement live — it carries no policy of its own.

It exists alongside the Rust sealg for environments that reach for uvx/pip rather 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 (protocol 2025-06-18), tools/list, tools/call for both isError:true→exit 6 and isError:false→exit 0, the conversation-id session flow, and the HTTPS_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); doctor reports 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.toml hook + .github/workflows/python-cli.yaml — the in-repo drift guard. It parses the Rust constants in crates/engine/src/gateway/{config,client}.rs and crates/cli/src/gateway_cmd.rs, compares them to python/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 tests
  • uv 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 agree
  • uvx --from ./python sealg doctor builds and runs

Related

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 Python sealg CLI alongside the Rust binary, mirroring doctor, list, and call with exit codes 0/1/6. It forwards MCP requests to the gateway, so policy and enforcement stay gateway-side.

  • Supports JSON and SSE responses, session and conversation headers, proxy-injected authentication, additive CA-bundle trust, and redirects capped at 10; credential headers are stripped on cross-origin redirects.
  • Redacts the API key from URLs and non-2xx error bodies; doctor now distinguishes a reachability failure from a failed tools/list probe.
  • Includes package docs, environment config, and tests for config resolution, protocol parsing, HTTP handling, and redaction.
  • Adds pre-commit and CI wire-contract checks against the Rust client; CI pins ruff==0.16.5 and ty==0.0.77.

Written for commit 416f8fa. Summary will update on new commits.

Review in cubic

… 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-code-review

Copy link
Copy Markdown

ⓘ 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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread python/sealg/client.py Outdated
Comment thread python/sealg/cli.py
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}"'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread scripts/check_wire_contract.py
Comment thread python/sealg/client.py Outdated
Comment thread python/sealg/client.py Outdated
Comment thread python/tests/test_wire.py
Comment thread prek.toml Outdated
Comment thread python/sealg/client.py Outdated
Comment thread python/sealg/client.py Outdated
- 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
@Miyamura80
Miyamura80 merged commit 422e182 into main Sep 2, 2026
8 checks passed
@github-actions
github-actions Bot deleted the claude/python-sealg-cli branch September 2, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant