ADR-0026 — Platform built-in tool dispatch surface (gateway-hosted, dedicated /mcp/platform route)
- Status: Accepted (Principal Architect, 2026-07-25) — resolves the #2119 dispatch-fork ruling. No founder product gate (the feature was green-lit 2026-07-25; no stack / dependency / secret / external-egress change). The new session-bearer audience for the platform route reuses the existing gateway OAuth-termination machinery (ADR-0021 T7); it is flagged for Ashik ratification of the auth-surface / deployment shape at implementation, mirroring ADR-0023's split-status precedent.
- Date: 2026-07-25
- Deciders: Principal Architect (author + decision), Backend SME (implementation feasibility), Ashik (auth-surface / deployment-shape ratification at build)
- Supersedes: the "orchestrator-hosted MCP endpoint" phrasing in the #2119 ruling — deeper investigation places the correct home in context-engine (the gateway process). See D1.
- Refs: #2119 (C-read read tools + the dispatch-fork ruling), #2120 (C-write mutating tools), #2121 (this surface / LLD). ADR-0021 (registry & gateway, OAuth termination T7), ADR-0022 (auto-derived team gateway), ADR-0024 (MCP tool-governance surfaces), ADR-0025 (egress SSRF hardening).
Context
The founder green-lit conversational workflow ops (#2119 C-read, #2120 C-write). The product value is the Quad LLM answering and operating workflows via real tool calls — not a stub. The architect ruling on #2119 (2026-07-25) established, verified against origin/main (719201b0):
- Snapshot wiring works. A team-less Quad orchestrator agent keeps its legacy tool list verbatim into
snapshot.tools(internal/runtime/session/initializer.go:192; the v2.4 resolver +freezeResolvedToolsare guarded byif teamID != ""at:220). - Dispatch is dead. Team-less agents route every tool through
_execute_tool_stub(services/agent-worker/.../graph/nodes.py:744) because_mcp_translation()returnsNone(no team ⇒ nomcp_serverstable in the snapshot,:979-981) and no gateway token is minted for them. The three read tools would return"[stub] Tool 'X' executed successfully", not real data. - The registry precedent is orphaned.
middleware.Registry.Execute— the only method that dispatches a tool through schema→governance→handler — has ZERO production callers (test-only).GetMCPRegistry/subagent.RegisterDelegateToAgentare test-only;cmd/agent-orchestrator/main.go:752-753constructs the registry andsetMCPRegistrys it, but nothing reads it back.delegate_to_agent's real production behaviour is the deterministic @mention delegator (lifecycle_mention_delegate.go/mention_delegator.go), not an LLM tool call.
Investigation for this ADR surfaced the decisive home question. The team MCP gateway (/mcp/team/{unit}, /mcp/group/{id}), the OAuth Terminator, the edge governance (CEL / allowlist / clearance-floor, ADR-0024 D3/D5/D6), and the in-process WorkflowService handle all live in context-engine (cmd/context-engine/main.go:669, :1563; the worker's default gateway base is http://context-engine:8080). The orphaned middleware.Registry lives in agent-orchestrator — a vestige of a pre-ADR-0021 plan in which the orchestrator was to host MCP tools. That plan was superseded by the context-engine gateway (ADR-0021/0022).
Decision
D1 — Home: context-engine (the gateway process), not agent-orchestrator. The platform built-in tool dispatch surface lives in context-engine, co-located with the MCP gateway, the OAuth Terminator, the edge-governance chokepoint, and the in-process WorkflowService handle. This means the built-in tool handlers call WorkflowService in-process — no new gRPC dial, no new auth hop, no new network path (the worker already POSTs to context-engine). The orchestrator's mcp_registry.go / setMCPRegistry / GetMCPRegistry and subagent.RegisterDelegateToAgent (registry helper) are confirmed-orphaned and are RETIRED under #2121 (see D7); the governed-tool registry is re-instantiated in context-engine where it gains a real consumer.
D2 — Mechanism: a dedicated /mcp/platform MCP route. context-engine mounts a new streamable-HTTP JSON-RPC route (same wire shape as the team gateway) whose tools/list + tools/call dispatch to an in-process governed-tool registry instead of proxying to an external upstream. The worker reuses its existing MCP JSON-RPC client (_post_rpc / _initialize / _dispatch_call) with a /mcp/platform URL builder; a reserved platform entry in the snapshot's mcp_servers translation table makes _mcp_translation() yield a platform ctx so the EXISTING nodes.py:727 governed path fires instead of the stub. Chosen over overloading /mcp/team/{unit} with a sentinel unit (rejected below) so platform tools get their own chokepoint, decoupled from auto-derived team-binding / allowlist / group semantics that do not apply to platform tools and would be dangerous to overload on the #2120 write path.
D3 — Identity & scope: session-scoped bearer, org-scoped, never ambient. The orchestrator mints an agent_gateway_token for team-less Quad sessions with aud = {base}/mcp/platform, carrying org + agent + session claims (reusing the ADR-0021 T7 Terminator + token scheme — a new audience, not a new auth primitive). The /mcp/platform handler terminates OAuth, derives scope.Path{OrgID} from the validated claim, sets the RLS GUC on the request transaction, and every handler calls WorkflowService under that scope. No ambient credentials; RLS is the tenant-isolation backstop — the cross-tenant probe MUST fail.
D4 — Dispatch through the governed-tool middleware pipeline. Each built-in tool is a middleware.GovernedTool (Name, InputSchema, Handler, Action / Resource / RiskLevel, optional TargetResolver / ApprovalMode) registered on a context-engine middleware.Registry; a /mcp/platform tools/call maps {prefix}__{tool} → the registry name and runs registry.Execute(ctx, name, input) (schema-validation → target-resolver → governance Check → handler). This gives registry.Execute its first production consumer.
D5 — Fail-closed for mutations; reads ride RLS + scope. The guardrails engine is fail-OPEN on no-match (the WF-30 repo-pin lesson). Read tools (#2119) are governed but bounded by session scope + RLS; fail-open on a read is acceptable (no cross-tenant leak is possible through the scoped path). Mutating tools (#2120) get an EXPLICIT fail-closed gate on this route: default-deny unless a policy explicitly allows, and always trip requires_approval (in-thread confirm). A removed policy ⇒ the tool refuses — never a silent mutation.
D6 — Bounded output discipline. Every tool result is size-bounded before it re-enters the agent context. get_node_transcript truncates with an explicit marker (never dumps a full transcript); list_workflows / get_run_status page and cap. Token discipline is a handler responsibility, not the model's.
D7 — Retire the orphaned orchestrator registry shelfware. As part of #2121, delete (or relocate to context-engine) cmd/agent-orchestrator/mcp_registry.go, setMCPRegistry / GetMCPRegistry, and the unused subagent.RegisterDelegateToAgent registry helper — after confirming each symbol's callers (the live delegate_to_agent behaviour is the @mention delegator; the registry helper is dead). Net: the platform registry has exactly one home and one real consumer, and net shelfware goes DOWN.
Consequences
- Positive: genuine E2E (a live LLM tool call → real workflow data) becomes achievable; the surface is designed ONCE and reused by #2119 (reads), #2120 (writes), and future internal tools (Runtime-v2 adjacency);
registry.Executegains a production consumer while the orphaned orchestrator copy is retired (net shelfware down); platform tools get a governance chokepoint decoupled from team-binding semantics. - Negative / cost: a new internal HTTP route + a new session-bearer audience (auth surface — Ashik ratification at build); a small
agent-workerchange (URL builder + platform-sentinel handling); snapshot provisioning for team-less Quad agents must add the platformmcp_serversentry + the token. All bounded and independently reviewable. - Security: a new INTERNAL surface only (worker ↔ context-engine); no new external egress, no new secret. Tenant isolation rests on the same OAuth-termination + RLS path the team gateway already uses; the cross-tenant probe is a required acceptance test. Mutations are fail-closed (D5).
Alternatives rejected
- Reverse
RuntimeServiceRPC (the worker dials a new orchestrator RPC for built-in tools): a bespoke new internal RPC + proto + worker gRPC client, duplicating auth/scope the gateway already performs, and reusing none of the MCP client path. More surface, less reuse. ExecuteStepbidi stream (relaytool_call_request/tool_resultover the existing stream): mutates a hot, recently-churned runtime path and changes its request→response-stream shape to bidirectional. Highest blast radius; rejected.- Reserved sentinel unit on
/mcp/team/{unit}(zero worker change): overloads the auto-derived team-gateway resolver, edge allowlist, clearance-floor, and group logic with a special-case that does not semantically apply to platform tools and is risky on the write path. Rejected in favour of a dedicated route (D2). - Home in agent-orchestrator (consume the existing registry): the worker does not POST there,
WorkflowServiceis not there, the gateway is not there — it would need a new listener + aWorkflowServicedial + duplicated token validation. Rejected (D1).