feat(engine): gateway transport (config + hand-rolled MCP client)

Merged
MI
2mo ago
+541-0

Summary

First slice of the thin-engine re-scope: sealg becomes a thin transport that forwards tool calls to the SealGate gateway's per-user /mcp/{api_key}/ endpoint, where all policy / lethal-trifecta / PII enforcement lives. The binary carries no policy of its own.

This PR is additive — it adds the gateway transport module but does not yet wire it into the CLI surface, so nothing existing breaks. The follow-up slice rewires sealg call → the gateway, adds sealg list, and deletes the template's local demo commands.

Design context: dev-docs/architecture/multi-interface-design.md in edison-watch/edison-watch (§5A thin engine, §5B Centaur, B3 error mirroring).

Changes

  • crates/engine/src/gateway/config.rs — GatewayConfig::from_env() resolves gateway coordinates from the environment (so the binary stays stateless and drops into any sandbox):
    • gateway URL (SEALGATE_URL, defaults to the dev endpoint — never a guessed prod host)
    • optional API key: present → embedded in the /mcp/{key}/ path; absent → keyless path, auth injected upstream (Centaur iron-proxy). Supports both credential models with no fork.
    • sealgate_secret_key header value, conversation id (SEALGATE_CONVERSATION_ID, falling back to Centaur's CENTAUR_THREAD_KEY), and a MITM CA bundle (SSL_CERT_FILE/REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS) — reqwest+rustls does not read those automatically, so we load them or TLS fails behind an intercepting proxy.
    • written against a getter seam for deterministic tests.
  • crates/engine/src/gateway/client.rs — hand-rolled MCP Streamable-HTTP client (spec 2026-07-28), no rmcp dependency so the binary stays thin: initialize / tools_list / tools_call, Mcp-Session-Id capture, JSON + SSE response parsing, and JSON-RPC errors mapped to a variant that mirrors the MCP error shape verbatim (no bespoke taxonomy).
  • crates/engine/src/gateway/mod.rs + lib.rs — module wiring and re-exports.

Testing

  • Tests pass — cargo test -p engine gateway: 10 passed (env resolution, URL building, JSON/SSE/error parsing). The client's transport methods need a live gateway, so this slice unit-tests the pure pieces (config resolution, the response parser).
  • Linting — cargo fmt --check and cargo clippy -p engine clean locally. Full make ci runs here on CI.

Related Issues

None — tracked by the multi-interface design doc.


Generated by Claude Code


Summary by cubic

Turns sealg into a thin transport that forwards tool calls to the SealGate gateway's per-user /mcp/{api_key}/ endpoint, where all policy and PII enforcement lives. Additive only — the CLI surface is unchanged and nothing existing breaks.

  • GatewayConfig::from_env() reads gateway URL, API key, secret key, conversation id, and MITM CA bundle from the environment.
  • With an API key the path becomes /mcp/{key}/; without one, auth is injected upstream and the keyless path is used; conversation id falls back to CENTAUR_THREAD_KEY.
  • Adds a hand-rolled MCP Streamable HTTP client (initialize, tools/list, tools/call) with JSON and SSE parsing and no rmcp dependency.
  • CA bundle env vars are honored explicitly because reqwest+rustls does not read them, so TLS keeps working behind an intercepting proxy.
  • JSON-RPC errors mirror the MCP error shape so callers can map them to exit codes directly; 10 unit tests cover config resolution, URL building, and response parsing.