Skip to content

Remote-proxied tool calls don't enforce the origin session's tool-surface policy or workspace boundary #696

Open

Description

@timkjr

Follow-up from your latest review on PR #649 (findings #2 and #3's remote half) — opening this separately since it's a design question about the multi-server/federation protocol, not a bug in that PR's diff.

The gap

When a tool call routes to a remote gortex daemon (Router.RouteToolCall → proxy path, reached from internal/server/handler.go's handleToolCall, internal/mcp/streamable/transport.go's tryRouteToolCall, and the daemon dispatcher's tryProxyToolCall), two things about the origin session are not currently transmitted to, or enforced by, the remote:

  1. The origin session's tool-surface policy. A tool hidden by the calling session's facade-v1/hide-mode policy is checked locally by checkToolGate inside the wrapped handler — but a proxied request never reaches that handler locally. Router.RouteToolCall only gates on remoteEnabledIn/IsEffectful (whether the remote is enabled at all, and whether the tool is read-only) before forwarding — nothing checks whether this session is even allowed to call the tool in the first place. The remote executes under its own default policy, whatever that happens to be.

  2. A trusted, enforceable workspace/cwd ceiling. The origin resolves a cwd/workspace for routing purposes (peekRouteContext, RouteInputs.Cwd), but that's used only to pick a remote destination — it isn't carried as an authoritative boundary the remote must honor once it receives the request. A remote hosting multiple repositories could, in principle, answer using a different repo than the one the caller's resolved cwd pointed to.

Neither of these is a bug in a single line — the origin-side gate simply doesn't exist on the remote-proxy branch at all, by construction: the local branch relies entirely on the downstream handler enforcing policy, and there is no equivalent downstream handler on the remote-proxy branch to rely on.

Why I'm asking rather than guessing at a fix

This needs an actual protocol decision, not just a code patch:

  • Where does the origin-policy check belong? The natural shared choke point is ProxyDecision.Decide (used by all three call sites above), but it doesn't currently have access to the MCP Server's session-policy machinery (IsToolEnabledForSession etc.) — that'd mean threading a callback/dependency into daemon.Router/ProxyDecision the way localExecute is already wired in.
  • For the workspace ceiling: does the origin refuse to route at all once it knows the resolved cwd doesn't match anything the target remote can honor, or does the remote need a new authenticated field (a resolved workspace identity) it's expected to enforce against its own tracked repos? The latter is a real trust-boundary addition to the wire protocol between daemons, not just an origin-side check.
  • Whatever the shape, it should apply uniformly across all three entry points (HTTP /v1/tools/*, the Streamable HTTP /mcp transport, and the stdio daemon dispatcher's proxy path) rather than being patched into each independently — that's the mistake that let the ctx-timing bug in Promote deferred MCP tools on a registry-miss instead of 404ing #649 hide in one of three near-identical call sites for as long as it did.

Happy to implement once there's a direction — just don't want to unilaterally invent a trust-boundary design for the federation protocol without your steer, especially given how much back-and-forth #649 already took on narrower issues.

Activity

  1. timkjr commented on Aug 29, 2026

    @timkjr
    Author

    Addendum, connecting this to a pattern I noticed while implementing the local-only fixes from your review (separate PR #649 comment coming once those land):

    That review's finding #1 — X-Gortex-Overlay-Session silently substituting for the real session identity when evaluating tool policy — turned out to have a bigger blast radius than "overlay vs. policy" once traced properly. The session ID this repo carries via gortexmcp.WithSessionID/SessionIDFromContext isn't overlay-specific or policy-specific — it's the one universal identity consulted by roughly 30 call sites: policy gating, token-stats accounting, the agent registry, diagnostics/health/readiness/stale-refs subscriptions, localization state, query logging, and notes/memory scoping, plus the ~8 genuinely overlay-related ones. The fix I landed (a separate WithOverlayCohortID/OverlayCohortIDFromContext, falling back to the real session id when unset) only had to touch the overlay-specific call sites — but it underlined that this class of bug (one context value doing double duty for two purposes that should stay independent) is exactly the shape of gap this issue is about, one layer further out.

    Right now there are at least four independent context-carried values a request needs correctly attached at every dispatch entrypoint for its identity/authorization to be enforced correctly: session id, session cwd (workspace boundary), the (now separated) overlay cohort, and the authorized-tool-call marker. Each one has to be remembered independently at each of the several entrypoints (HTTP /v1/tools/*, the Streamable HTTP transport's two dispatch paths, the daemon's stdio dispatcher, the daemon's Streamable dispatcher) — which is exactly how the ctx-timing bugs from PR #649 slipped in three separate times over four review rounds.

    Not proposing a design here, just flagging it as something worth factoring into whatever approach you land on for this issue: a single, structured request-identity type (bundling session id + cwd + overlay cohort, maybe more) constructed once per inbound request at each entrypoint, carried under one context key, would likely be the more durable foundation for the remote-enforcement fix this issue is asking for — rather than adding yet another independent field to the pile and hoping every entrypoint remembers to set it. Whether that's worth doing now or as a later follow-up is your call; wanted you to have the full picture before deciding.

  2. added
    bugSomething isn't working
    securityConfinement, isolation and access control
    mcpMCP tool surface, schemas, facade and errors
    on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpMCP tool surface, schemas, facade and errorssecurityConfinement, isolation and access control

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions