Skip to content

Agent gateway (Slice 1): host-served MCP read/coordination tools#140

Open
yaacovcorcos wants to merge 2 commits into
mainfrom
feat/agent-gateway-slice1-read
Open

Agent gateway (Slice 1): host-served MCP read/coordination tools#140
yaacovcorcos wants to merge 2 commits into
mainfrom
feat/agent-gateway-slice1-read

Conversation

@yaacovcorcos

Copy link
Copy Markdown
Contributor

Slice 1 — Agent Gateway: host-served MCP read/coordination tools

First slice of the agent gateway port: a thread-scoped, host-served MCP endpoint that lets an agent in one thread observe and coordinate sibling top-level threads ("chats talking to each other"). This slice ships read/coordination only — no thread-driving (send/interrupt) and no creation/fork. It is behind a feature flag, disabled by default.

Lift-and-reconcile of Synara's MIT-licensed agent gateway (upstream/feat/agent-gateway-mcp) onto a Scient-owned authorization + credential spine. synara_* tool names and synara/ namespaces are kept intentionally (the rebrand is a later coordinated pass).

What ships

New subsystem apps/server/src/agentGateway/:

  • Transport / ingress auth (mcpTransport.ts, httpRoute.ts, protocol.ts, toolInput.ts, toolRuntime.ts) — POST /mcp streamable-HTTP endpoint that bypasses the global cookie-auth stack (provider child processes carry no session cookie) and runs its own per-request pipeline: extractBearerToken → verifySession → thread-existence recheck → provider-ownership recheck → central authorization. Every request re-checks authorization (no cached grant). Body cap (1 MiB) and batch cap (50) preserved. GET/DELETE mirror POST's disabled-state 404 so a disabled instance is indistinguishable from one that never registered the route, and return 405 only once the feature is enabled. A top-level defect net guarantees the documented "never fails" contract on handleMcpPost, returning a generic 500 (detail logged server-side, never in the body).
  • Credential spine (bearerToken.ts, Services/ + Layers/AgentGatewayCredentials.ts, AgentGatewaySessionRegistry.ts) — opaque per-thread bearer tokens, constant-time verification, in-memory registry (dies on restart), revoke-on-teardown. Least-privilege: read tokens carry only thread:read.
  • Central default-deny authorization (authorization.ts) — one policy every request funnels through: capability match → project scope → thread-type. Cross-project reads are denied as thread_not_found without disclosing the target project's existence.
  • Read tools (threadReadTools.ts, threadSummary.ts) — synara_context, synara_list_projects, synara_list_threads, synara_read_thread, synara_wait_for_threads. Backed by ProjectionSnapshotQuery. Token-friendly summaries with truncation + stable-index pagination.
  • Provider injection (mcpInjection.ts, harnessPolicy.ts) — Claude provider wired this slice; the bearer token rides an HTTP Authorization header (never the process env, so exec subprocesses cannot inherit it).

Shared-file injection (hand-re-applied at current symbol anchors, not merged from the donor):

  • http.ts — register agentGatewayRouteLayer; documented auth bypass.
  • config.ts / main.tsagentGatewayEnabled flag (SYNARA_AGENT_GATEWAY_ENABLED env + --agent-gateway-enabled CLI), default false.
  • effectServer.ts — publish the actually-bound port via setListeningPort so injection targets the real endpoint under ephemeral-port binding.
  • ClaudeAdapter.ts / runtimeLayer.ts — mint token at session start (flag-gated), revoke on teardown and on failed installation.
  • packages/contracts/src/agentGateway.ts (+ index.ts re-export) — new contract types.

Security posture

  • Disabled by default, gated at both the route (httpRoute.ts 404s when off) and the injection (ClaudeAdapter.ts skips minting when off) — no endpoint, no tokens minted otherwise.
  • Token exposure minimized: Claude transport is header-only; token never enters process env or logs; revoked on teardown so a retired session cannot keep coordinating.
  • Default-deny authorization with cross-project denial that does not leak out-of-scope thread/project existence.
  • Single-user desktop threat model: primary risks are prompt injection (cross-thread content framed as untrusted data), cross-project leakage (scope-gated), and secret exposure (header transport + revoke). No multi-tenant boundary is claimed.

Testing

  • bun run typecheck — 9/9 packages green.
  • bun run test (agentGateway) — 168 tests across 12 files, all passing: protocol, bearer token, tool input, mcp injection, authorization matrix (default-deny, read-token-rejected-for-write, cross-project deny), harness policy, transport ingress (401 paths: no/invalid token, thread gone, provider ownership changed; capability/turn gates; batch caps), read-tool shaping + pagination + wait timeout/terminal detection, thread summary, session registry, credentials, and the HTTP route (new httpRoute.test.ts: flag-disabled 404 parity across POST/GET/DELETE, bearer-check-before-body-read, 1 MiB cap → 413, invalid JSON → 400, GET 405 + Allow: POST, DELETE 405, success dispatch).
  • bun run test (full workspace) — 12/12 packages green, no regressions (incl. the pre-existing main.test.ts CLI layer-resolution suite after the layer-wiring change).
  • bun run lint — 0 errors (no new warnings in agentGateway/).
  • bun run brand:check — passed.

Self-review

A security-focused self-review pass found no auth-bypass, cross-tenant leak, or live-when-disabled path. Three findings were fixed in this PR: GET/DELETE now honor the disabled-flag 404 parity (previously an unconditional 405 that revealed the route existed while disabled); a top-level defect net on handleMcpPost upholds its never-fails contract; and the session-registry Map.get verification path is documented (opaque ~122-bit random tokens, not secret-compared values). The biggest test gap — the network entry point had no test file — is closed by httpRoute.test.ts.

Known limitations / follow-ups

Low-severity items surfaced during self-review, deferred deliberately. All fail closed, all require an authenticated caller acting on its own session, and proper fixes touch contract/plumbing surface better handled in a focused change:

  • wait_for_threads is poll-based and all-or-nothing. The plan's subscription-based rebuild on the event bus is not in this slice — the tool polls getShellSnapshot on a backoff up to its own deadline. Two consequences: (a) a wait inside a JSON-RPC batch holds that request open (batches process sequentially), and (b) if any one pinned thread disappears mid-wait, the whole call fails thread_not_found rather than returning terminal results for the survivors. Bounded by the caller-supplied timeout.
  • Duplicate-id detection covers only well-formed requests. Batch dedup keys on kind === "request"; two invalid messages sharing an id are not deduped (each gets its own error response). Cosmetic — no auth or state impact.
  • Read tools authorize after fetching the target shell. Project-scope denial maps to thread_not_found and does not leak existence, but authorize-before-fetch would be cleaner defense-in-depth.

Deliberately out of scope (later slices)

  • Slice 1b — diagnostics tools + redaction pass (diagnostics:read).
  • Slice 2synara_send_message / synara_interrupt_thread (turn-pinned write authority + TOCTOU handling + idempotency; the assertCallerTurnActive seam is built here but unused).
  • Future — creation/fork saga.
  • Other providers (codex TOML + env-exclusion, ACP http/stdio) — this slice wires Claude only.

🤖 Generated with Claude Code

First slice of the agent gateway: a thread-scoped, host-served MCP
endpoint (`POST /mcp`) that lets an agent in one thread observe and
coordinate sibling top-level threads. Read/coordination only — no
send/interrupt, no create/fork. Behind a feature flag, disabled by
default (`SYNARA_AGENT_GATEWAY_ENABLED` / `--agent-gateway-enabled`).

Lift-and-reconcile of Synara's MIT-licensed agent gateway onto a
Scient-owned authorization + credential spine. `synara_*` names and
`synara/` namespaces kept intentionally (rebrand is a later pass).

New `apps/server/src/agentGateway/` subsystem:
- Transport/ingress auth: per-request bearer verify → thread-existence
  recheck → provider-ownership recheck → central authorization, with no
  cached grant; body cap (1 MiB) + batch cap (50); GET/DELETE mirror the
  disabled-state 404; top-level defect net upholds the never-fails
  contract on handleMcpPost.
- Credential spine: opaque per-thread tokens, in-memory registry (dies
  on restart), least-privilege (`thread:read` only), revoke-on-teardown.
- Central default-deny authorization: capability → project scope →
  thread-type; cross-project reads denied as thread_not_found.
- Read tools: synara_context, list_projects, list_threads, read_thread,
  wait_for_threads, backed by ProjectionSnapshotQuery.
- Claude provider injection via HTTP Authorization header (never the
  process env), flag-gated, revoked on teardown and failed install.

Shared-file injection hand-re-applied at current symbol anchors:
http.ts route registration, config.ts/main.ts flag, effectServer.ts
bound-port publish, ClaudeAdapter.ts/runtimeLayer.ts token lifecycle,
new packages/contracts/src/agentGateway.ts.

typecheck 9/9, full test suite 12/12 (agentGateway 168/12 incl. new
httpRoute.test.ts), lint 0 errors, brand:check green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 26, 2026
Resolves the two CI failures on this PR:

1. migration:check — Slice 1 added `export * from "./agentGateway"` to
   `packages/contracts/src/index.ts`. That barrel is a released-migration
   dependency (released migrations import `@synara/contracts`, and the
   lineage guard traverses the barrel's re-exports and fingerprints its
   runtime source). Adding the line both *modified* the frozen barrel and
   *pulled* `agentGateway.ts` into the released dependency closure, tripping
   the append-only guard.

   Fix: relocate the gateway contract into the server subsystem as
   `apps/server/src/agentGateway/contract.ts` and drop it from the barrel.
   Every consumer is server-side (no renderer/client consumer exists), so
   the shared package was never required. `index.ts` is restored byte-for-
   byte to the v0.5.13 baseline and `agentGateway.ts` leaves the closure.

2. fmt:check — donor-lifted test files and the ClaudeAdapter edits were not
   run through oxfmt. Applied `oxfmt` (formatting only; no behavior change).

Full gate green: brand, workflow, migration, fmt, lint (0 errors),
typecheck (9/9), test (12/12 packages, 2530 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant