Agent gateway (Slice 1): host-served MCP read/coordination tools#140
Open
yaacovcorcos wants to merge 2 commits into
Open
Agent gateway (Slice 1): host-served MCP read/coordination tools#140yaacovcorcos wants to merge 2 commits into
yaacovcorcos wants to merge 2 commits into
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 andsynara/namespaces are kept intentionally (the rebrand is a later coordinated pass).What ships
New subsystem
apps/server/src/agentGateway/:mcpTransport.ts,httpRoute.ts,protocol.ts,toolInput.ts,toolRuntime.ts) —POST /mcpstreamable-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/DELETEmirrorPOST's disabled-state 404 so a disabled instance is indistinguishable from one that never registered the route, and return405only once the feature is enabled. A top-level defect net guarantees the documented "never fails" contract onhandleMcpPost, returning a generic500(detail logged server-side, never in the body).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 onlythread:read.authorization.ts) — one policy every request funnels through: capability match → project scope → thread-type. Cross-project reads are denied asthread_not_foundwithout disclosing the target project's existence.threadReadTools.ts,threadSummary.ts) —synara_context,synara_list_projects,synara_list_threads,synara_read_thread,synara_wait_for_threads. Backed byProjectionSnapshotQuery. Token-friendly summaries with truncation + stable-index pagination.mcpInjection.ts,harnessPolicy.ts) — Claude provider wired this slice; the bearer token rides an HTTPAuthorizationheader (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— registeragentGatewayRouteLayer; documented auth bypass.config.ts/main.ts—agentGatewayEnabledflag (SYNARA_AGENT_GATEWAY_ENABLEDenv +--agent-gateway-enabledCLI), defaultfalse.effectServer.ts— publish the actually-bound port viasetListeningPortso 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.tsre-export) — new contract types.Security posture
httpRoute.ts404s when off) and the injection (ClaudeAdapter.tsskips minting when off) — no endpoint, no tokens minted otherwise.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 (newhttpRoute.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-existingmain.test.tsCLI layer-resolution suite after the layer-wiring change).bun run lint— 0 errors (no new warnings inagentGateway/).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/DELETEnow honor the disabled-flag 404 parity (previously an unconditional 405 that revealed the route existed while disabled); a top-level defect net onhandleMcpPostupholds its never-fails contract; and the session-registryMap.getverification 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 byhttpRoute.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_threadsis poll-based and all-or-nothing. The plan's subscription-based rebuild on the event bus is not in this slice — the tool pollsgetShellSnapshoton a backoff up to its own deadline. Two consequences: (a) awaitinside a JSON-RPC batch holds that request open (batches process sequentially), and (b) if any one pinned thread disappears mid-wait, the whole call failsthread_not_foundrather than returning terminal results for the survivors. Bounded by the caller-supplied timeout.kind === "request"; twoinvalidmessages sharing an id are not deduped (each gets its own error response). Cosmetic — no auth or state impact.thread_not_foundand does not leak existence, but authorize-before-fetch would be cleaner defense-in-depth.Deliberately out of scope (later slices)
diagnostics:read).synara_send_message/synara_interrupt_thread(turn-pinned write authority + TOCTOU handling + idempotency; theassertCallerTurnActiveseam is built here but unused).🤖 Generated with Claude Code