Skip to content

feat(runner): authenticate the internal MCP loopback endpoint (WP1) + continuation measurement protocol (WP0)#5245

Merged
mmabrouk merged 6 commits into
big-agentsfrom
feat-client-tool-continuation-v1
Jul 12, 2026
Merged

feat(runner): authenticate the internal MCP loopback endpoint (WP1) + continuation measurement protocol (WP0)#5245
mmabrouk merged 6 commits into
big-agentsfrom
feat-client-tool-continuation-v1

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Symptom

The internal MCP endpoint that delivers Agenta gateway tools to local Claude (a loopback HTTP server the runner serves on 127.0.0.1) authenticated callers by nothing but reachability. Any other process on the runner host could POST to it and list or call the run's tools — the same host-boundary hole #4831 closed for user stdio MCP servers, reopened for the internal channel. This is the v1 slice of the client-tool continuation plan (#5201): harden the endpoint now; defer the warm hold-open path behind measurement gates.

What changed

WP1 — authenticate the loopback endpoint (tool-mcp-http.ts, mcp-bridge.ts)

  • Per-server bearer token. startInternalToolMcpServer mints a randomBytes(32).toString("base64url") token per instance; buildToolMcpServers advertises it as the entry's Authorization: Bearer … header. Each run/environment owns a distinct token.
  • Authenticate before parse. The request listener rejects a missing/wrong token with 401 before readBody — attacker-controlled input is never read or parsed when unauthorized. Comparison is timing-safe (crypto.timingSafeEqual), length-guarded so a mismatch never throws.
  • Client-tool batch rejection. A JSON-RPC batch containing a client-kind tools/call is rejected up front (-32600) before any sibling item executes — a client tool crosses a turn boundary and cannot safely share a concurrent batch.

Before: headers: [], "the channel is unauthenticated on loopback." After: headers: [Authorization: Bearer <per-session>].

Layering (the important invariant): the bearer lives only in the HTTP transport path. The in-sandbox stdio shim (#5234, tool-mcp-stdio.ts) is byte-unchanged and stays credential-free by construction — different file, different transport. The bearer is a local loopback access guard, never a provider/control-plane credential; the private callRef still never reaches the advertisement (pinned in session-mcp-layering.test.ts).

WP0 — measurement protocol for the deferred warm path (docs only)

projects/mcp-client-tool-continuation/experiments/README.md: a checked-in, repeatable protocol for the two measurements that gate the deferred warm hold-open path (transport ceiling: does Claude keep the MCP request usable past the 60s/300s TTLs; cold-path baseline: the five user-cost metrics), plus an unlock-gate scorecard. All observed values are PENDING LIVE RUN. No runtime behavior change from WP0.

Deferred (NOT in this PR)

WP2–WP5 (the continuation kernel, the hold-open path, the failure envelope, the canary) stay deferred behind the two unlock gates. The cold-replay fallback remains the one continuation mechanism. See plan.md "Decision summary." The warm-path deferral is the owner's top open review question — it has not been ruled on.

Tests

New real-socket HTTP integration tests in tool-bridge.test.ts: per-server bearer, token rotation across instances, initialize/list/normal call, missing + wrong token → 401 with no dispatch, malformed JSON, oversized body (>1 MB), client-tool batch rejection, normal close. Updated the two layering pins (session-mcp-layering.test.ts, sandbox-agent-orchestration.test.ts) that asserted headers: [] to expect the loopback-guard header while keeping the "private callRef never leaks" assertion.

pnpm run typecheck and pnpm test: all WP1/layering tests green. The 4 remaining failures are pre-existing baseline failures on this stack, unrelated to this change: code-tool.test.ts (orphaned test for removed code), sandbox-agent-qa-transcript-replay (×2), sandbox-agent-run-plan hasApiKey.

Open rollout gate

One live check remains (local, credit-free, deliberately not run in this autonomous pass to conserve budget): confirm the local Claude ACP adapter forwards the Authorization header on each MCP request. Confidence is high without it — the user HTTP MCP path (#4834, e.g. Linear) already forwards this exact headers field to external MCP servers, so the internal channel uses a proven mechanism — but it should be verified before merge. If the adapter did not forward it, every gateway-tool call on local Claude would 401.

Where to look first

  • services/runner/src/tools/tool-mcp-http.tshasValidAuthorization (timing-safe), the 401 gate before readBody, the batch preflight.
  • services/runner/src/tools/mcp-bridge.ts — token generation seam + the advertised Authorization header.
  • services/runner/tests/unit/tool-bridge.test.ts — the auth/batch integration tests.

Stack

Base: feat-in-sandbox-tool-mcp (#5244). This lane depends on #5244's shim work only for stack ordering; the WP1 change is confined to the HTTP path.

https://claude.ai/code/session_0127AM79khCdvD2b8BG2joZL

mmabrouk added 4 commits July 12, 2026 05:54
…ndbox stdio MCP shim

Slice 1 of in-sandbox-tool-mcp (docs/design/agent-workflows/projects/in-sandbox-tool-mcp/).
Today a Claude run on Daytona with any custom tool is refused up front
(REMOTE_TOOLS_UNSUPPORTED_MESSAGE): execution via the file relay is solved, but nothing
inside the sandbox advertises the tools to an MCP-client harness. This revives PR #4873
onto today's paths, consuming the relay client PR #5243 extracted.

- tools/tool-mcp-stdio.ts: a dependency-free stdio MCP shim (9.6 kB bundled) the harness's
  ACP adapter spawns inside the sandbox. NDJSON JSON-RPC; tools/list serves the run's
  public specs from a specs FILE; tools/call writes a relay request through the shared
  relay client (atomic publication, per-tool timeoutMs) and maps failures to isError
  results. Fail-loud exit 1 on missing env/specs. Env contract in tools/tool-mcp-env.ts:
  AGENTA_AGENT_TOOLS_RELAY_DIR + AGENTA_AGENT_TOOLS_PUBLIC_SPECS_FILE (a file, not an
  unbounded env value copied through four exec layers).
- engines/sandbox_agent/tool-mcp-assets.ts: bundle + specs upload, always-write (closes
  bundle-version skew in reused sandboxes), THROWS on missing bundle/failed upload on the
  path that requires the shim. Bundle override SANDBOX_AGENT_RELAY_MCP_BUNDLE.
- engines/sandbox_agent/mcp.ts: dedicated internal-entry constructor building the typeless
  ACP stdio shape named agenta-tools (claude_settings.py couples permission rules to it);
  McpServerStdio moved here out of mcp-bridge.ts; user-declared servers may not claim the
  reserved name; toAcpMcpServers still cannot emit stdio; user-stdio refusal unrelaxed.
- run-plan.ts: the remote-tools gate narrows - executable tools on Daytona pass (new
  toolMcpDir ephemeral sibling of the relay dir), client tools refuse loud with their own
  message, non-Daytona remote providers stay fail-closed.
- engines/sandbox_agent/relay-guard.ts: the relay execution guard now guards EVERY harness
  (review finding): allow/deny identical everywhere; ask consumes the Pi grant ledger on Pi
  and passes on MCP harnesses whose own dialog gates the call, with the forged-ask residual
  documented. Previously Claude relay records executed unguarded.
- The slice-0 restart spike locked the A2 transport: the pinned Claude ACP adapter
  (claude-agent-acp 0.22.2 in snapshot agenta-sandbox-pi) respawns the stdio MCP subprocess
  on the session/load path after a VM stop/restart (spike-restart.md in the workspace).

Tests: 25+ new (shim handler + real startToolRelay round trip incl. crash-after-write,
concurrency, stdout purity; assets fail-loud; three-layer session MCP pin; gate matrix;
non-Pi guard semantics). Suite 1054 passed / 4 known pre-existing failures; typecheck clean
for the slice; both bundles build with the forbidden-server-symbols gate.

Claude-Session: https://claude.ai/code/session_01Fr4A5zjs5rsufRaWgWiUNC
…internal MCP name

The engine half of the shim slice, split from the previous commit after a cross-lane
hunk-dependency snag (a formatting pass had touched the parallel Daytona-secret lane's
committed regions; reverted, insertions re-anchored):

- sandbox_agent.ts: upload the shim bundle + specs file on the Daytona non-Pi
  executable-tools path (fail-loud helper, deps-injectable) and pass the assets into
  buildSessionMcpServers; the relay execution guard is now built for EVERY harness via
  buildRelayExecutionGuard (allow/deny identical everywhere; ask consumes the Pi grant
  ledger on Pi and passes on MCP harnesses whose own dialog gates the call).
- mcp.ts: materialization-time reserved-name defense (assertNoReservedUserMcpName at the
  top of buildSessionMcpServers).
- run-plan.ts: declaration-time refusal of a user MCP server named agenta-tools, before
  any resource is created, plus its gate test.

Claude-Session: https://claude.ai/code/session_01Fr4A5zjs5rsufRaWgWiUNC
The three-layer test used the credentials wire field owned by the parallel Daytona-secret
work, so this lane's tip failed typecheck standalone. The layering pin is about WHICH
servers are delivered; auth delivery has its own tests. Also swap the 'no Bearer' assertion
for 'no user MCP url leaks into the internal entry'.

Claude-Session: https://claude.ai/code/session_01Fr4A5zjs5rsufRaWgWiUNC
…CP shim

The internal gateway-tool channel now has two transports (local loopback HTTP; the
in-sandbox stdio shim on Daytona): update the runner-to-MCP-server interface page (shim
env contract, SANDBOX_AGENT_RELAY_MCP_BUNDLE, reserved server name, narrowed remote gates,
the every-harness relay execution guard and its stated ask residual), the interface index
rows, the harness-adapters and mcp-models coupling notes, the permission-responder guard
note, the tools/ground-truth/claude-code narrative pages, and the operator env-var
inventory in running-the-agent.

Claude-Session: https://claude.ai/code/session_01Fr4A5zjs5rsufRaWgWiUNC
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 12, 2026 8:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change secures the internal loopback MCP endpoint with per-server bearer tokens, rejects unsupported client-tool batches, expands endpoint tests, updates channel assertions, and adds a README for measuring MCP client-tool continuation experiments.

Changes

MCP loopback endpoint security

Layer / File(s) Summary
Authorization contract and server wiring
services/runner/src/tools/tool-mcp-http.ts, services/runner/src/tools/mcp-bridge.ts
Internal MCP servers generate and return per-server tokens, while advertised HTTP entries include matching bearer authorization headers.
Request authorization and batch gating
services/runner/src/tools/tool-mcp-http.ts
POST requests validate bearer credentials before parsing, and batches containing client tool calls are rejected before dispatch.
Endpoint regression coverage
services/runner/tests/unit/tool-bridge.test.ts, services/runner/tests/unit/sandbox-agent-orchestration.test.ts, services/runner/tests/unit/session-mcp-layering.test.ts
Tests cover token propagation and rotation, unauthorized requests, batch rejection, malformed and oversized payloads, shutdown, and authenticated paused requests.

Continuation experiment protocol

Layer / File(s) Summary
Experiment measurement protocol
docs/design/agent-workflows/projects/mcp-client-tool-continuation/experiments/README.md
Documents pinned environments, transport-ceiling and cold-path measurement procedures, placeholder result tables, and warm-path unlock-gate rules.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant InternalMcpServer
  participant ClientRelay
  MCPClient->>InternalMcpServer: POST with per-server bearer token
  InternalMcpServer->>InternalMcpServer: Validate authorization and batch contents
  InternalMcpServer->>ClientRelay: Dispatch supported client tool call
  ClientRelay-->>InternalMcpServer: Return tool result
  InternalMcpServer-->>MCPClient: Return JSON-RPC response
Loading

Possibly related PRs

  • Agenta-AI/agenta#4847: Re-enables the internal HTTP MCP architecture extended here with per-server bearer authorization.
  • Agenta-AI/agenta#4985: Also changes client tool behavior in internal MCP batch handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: MCP loopback authentication and the continuation measurement protocol.
Description check ✅ Passed The description is directly related to the code and docs changes and accurately describes their intent and scope.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-client-tool-continuation-v1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b71b07df-51ca-4798-b5f2-c48867c2fa2d

📥 Commits

Reviewing files that changed from the base of the PR and between 0fb9186 and 6ab5d45.

📒 Files selected for processing (6)
  • docs/design/agent-workflows/projects/mcp-client-tool-continuation/experiments/README.md
  • services/runner/src/tools/mcp-bridge.ts
  • services/runner/src/tools/tool-mcp-http.ts
  • services/runner/tests/unit/sandbox-agent-orchestration.test.ts
  • services/runner/tests/unit/session-mcp-layering.test.ts
  • services/runner/tests/unit/tool-bridge.test.ts

Comment on lines +22 to +24
1. Start one local runner environment with one browser-fulfilled client tool. Capture the runner,
harness, ACP, and MCP logs needed to correlate request identities. Do not enable or implement the
deferred warm path for this experiment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Define repeated-trial criteria before making a permanent transport decision.

The protocol starts one local environment, but any transport failure is said to cut the warm path “permanently.” Require repeated independent trials and a predefined aggregation rule so a transient runner, OS, or Claude failure does not become an irreversible product decision.

Also applies to: 79-80

Comment on lines +39 to +47
| Transport result | Observed value |
| --- | --- |
| Same MCP request id after more than 60 seconds | `PENDING LIVE RUN` |
| Same ACP tool-call id after more than 60 seconds | `PENDING LIVE RUN` |
| Client close, retry, or settled error after more than 60 seconds | `PENDING LIVE RUN` |
| Same ids after more than 300 seconds | `PENDING LIVE RUN` |
| Measured request lifetime ceiling and safety margin | `PENDING LIVE RUN` |
| One quiet pending socket fd delta | `PENDING LIVE RUN` |
| One quiet pending socket RSS/PSS delta | `PENDING LIVE RUN` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Measure successful fulfillment, not only ID retention.

The transport gate requires the original MCP request to remain usable, but these rows only record whether IDs remain present or whether the connection closes/retries/errors. Add whether the delayed result was accepted and produced a continued Claude response, and keep close, retry, and settled-error outcomes distinct.

Suggested measurement rows
 | Same MCP request id after more than 60 seconds | `PENDING LIVE RUN` |
 | Same ACP tool-call id after more than 60 seconds | `PENDING LIVE RUN` |
+| Delayed result accepted and continued answer after more than 60 seconds | `PENDING LIVE RUN` |
 | Client close, retry, or settled error after more than 60 seconds | `PENDING LIVE RUN` |
 | Same ids after more than 300 seconds | `PENDING LIVE RUN` |
+| Delayed result accepted and continued answer after more than 300 seconds | `PENDING LIVE RUN` |

Also applies to: 82-85

Comment on lines +82 to +85
| Gate | Passing rule | Measurement | Decision |
| --- | --- | --- | --- |
| Transport | The original MCP request remains usable for at least 60 seconds, with a measured ceiling and safety margin. | `PENDING LIVE RUN` | `PENDING LIVE RUN` |
| Value | First-reissue mismatch is greater than 5%, argument-drift re-interaction is greater than 2%, or cold continuation adds user-visible p50 latency measured in seconds or model cost that users or support report. | `PENDING LIVE RUN` | `PENDING LIVE RUN` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the value gate deterministic.

“p50 latency measured in seconds” has no threshold, and “model cost that users or support report” has no numeric criterion or evidence requirement. Different reviewers could reach opposite decisions from the same results; define explicit latency/cost thresholds and the required supporting evidence.

@mmabrouk

mmabrouk commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Reviewed with Codex (high effort). The auth logic is clean: the 401 gate runs before any body parse, the token compare is timing-safe, and a batch with a client tool runs nothing before it is rejected.

The minor items are fixed in #5248: authenticate before the method check (401, not 405, for GET/DELETE) and the extra auth tests.

One blocker is yours to decide: the bearer rides the harness command line. The Claude SDK puts the MCP config, including our Authorization header, into --mcp-config in the child's argv (verified in sdk.mjs), so a local process can read the token from /proc/<pid>/cmdline. The fix is to pass the token by environment variable instead, which needs a live check that the pinned Claude CLI expands ${VAR} in header values. The tradeoff is in the PR body.

// a distinct token, which protects the loopback endpoint from other local processes.
headers: [
{
name: "Authorization",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This bearer guards the loopback endpoint from other local processes. Heads-up for review: the Claude SDK serializes this header into the child's --mcp-config argv (verified in sdk.mjs), so the token is readable from /proc/<pid>/cmdline. That is the blocker in my summary comment. The fix is to deliver the token by environment variable, pending a live check that the pinned Claude CLI expands ${VAR} in header values.

@mmabrouk mmabrouk added the needs-review Agent updated; awaiting Mahmoud's review label Jul 12, 2026
mmabrouk added 2 commits July 12, 2026 20:46
Harden the internal gateway-tool MCP endpoint that delivers Agenta tools to
local Claude over loopback HTTP. Until now it relied only on 127.0.0.1
reachability, so any other local process could list or call tools through it.

- Mint a per-server bearer token (randomBytes 32, base64url) in
  startInternalToolMcpServer; advertise it as the entry's Authorization header
  in buildToolMcpServers.
- Authenticate before reading or parsing the request body; reject missing/wrong
  tokens with 401 and no dispatch. Timing-safe comparison, length-guarded.
- Reject a JSON-RPC batch containing a client tool before executing any item.
- The bearer lives ONLY in the HTTP transport path; the in-sandbox stdio shim
  (#5234) stays credential-free by construction (different file, different
  transport). It is a local loopback guard, never a provider/control-plane
  credential — the private callRef still never reaches the advertisement.

Adds real-socket HTTP integration tests (auth, malformed/oversized body, batch
client-tool rejection, close) and updates the two layering pins that asserted
headers: [] to expect the loopback guard header.

WP0: checked-in measurement protocol + unlock-gate scorecard for the deferred
warm hold-open path (results marked PENDING LIVE RUN). No runtime behavior
change from WP0.

Claude-Session: https://claude.ai/code/session_0127AM79khCdvD2b8BG2joZL

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

lgtm

@mmabrouk mmabrouk marked this pull request as ready for review July 12, 2026 19:42
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. Backend enhancement New feature or request tests labels Jul 12, 2026
@mmabrouk mmabrouk added lgtm This PR has been approved by a maintainer and removed enhancement New feature or request Backend tests size:L This PR changes 100-499 lines, ignoring generated files. needs-review Agent updated; awaiting Mahmoud's review labels Jul 12, 2026
@mmabrouk mmabrouk force-pushed the feat-client-tool-continuation-v1 branch from 6ab5d45 to 43ed55a Compare July 12, 2026 20:13
mmabrouk added a commit that referenced this pull request Jul 12, 2026
Addresses the mechanical review findings on #5243, #5244, and #5245 that carry
no design decision and need no live verification. Landed as one commit on top
of the stack because GitButler cannot retro-assign a new delta onto a lower
applied lane; each fix is mapped to its PR below.

Relay (#5243):
- relay.ts: bound every relay-file removal with a timeout so a hung daemon
  deletion can no longer wedge pickup, stale-sweep, or shutdown; a timed-out
  removal is treated as failed and the loop continues.
- relay-watch.ts: tie a safety-poll miss to the active watch generation so a
  later zero-exit of the same generation cannot erase it (the demotion counter
  now accumulates); validate the whole watch-window string instead of
  parseInt accepting partial garbage.

Shim (#5244):
- tool-mcp-stdio.ts: answer MCP 'ping' with an empty result; return -32700 on
  unparseable input and -32600 on an invalid request; document the
  at-least-once relay delivery contract. Protocol-version negotiation is left
  unchanged (that needs a live check).
- tool-mcp-assets.ts: cache the shim bundle by path and write bundle + specs
  concurrently, removing avoidable serial Daytona round trips.
- restored the direct client-only Daytona refusal test; pinned the Pi vs
  non-Pi ask guard wiring; updated the build:extension docs.

Continuation (#5245):
- tool-mcp-http.ts: authenticate before the method check so an unauthenticated
  GET/DELETE gets 401, not 405.
- added auth tests: unauthorized GET/DELETE, wrong scheme, empty and
  whitespace tokens, and that neither the callback executor nor the client
  relay runs on an unauthorized call.

Deferred (need a design decision or a live check, not in this commit): the
non-Pi ask authorization model (#5244), the bearer-in-argv delivery (#5245),
MCP protocol-version negotiation (#5244).

Claude-Session: https://claude.ai/code/session_0127AM79khCdvD2b8BG2joZL
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 12, 2026
@mmabrouk mmabrouk changed the base branch from feat-in-sandbox-tool-mcp to big-agents July 12, 2026 20:30
@mmabrouk mmabrouk merged commit f0208e0 into big-agents Jul 12, 2026
17 of 19 checks passed
@mmabrouk mmabrouk deleted the feat-client-tool-continuation-v1 branch July 12, 2026 20:30
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant