security: adopt the SDK roots-replacement (SEP-2322) so the wrong-repo write guard survives 2026-07-28 clients #428

Open
opened 2026-08-17 15:52:57 +00:00 by hexajon · 3 comments
Owner

Follow-up from #387 (2026-07-28 / go-sdk v1.7.0). This changes a security and isolation posture, so I want it settled deliberately rather than folded into routine SDK upkeep.

The degradation

SEP-2577 deprecates the MCP roots feature, and go-sdk v1.7.0 refuses server-initiated roots/list on any session that negotiates >= 2026-07-28. madtea's wrong-repo write guard (callerRepoFromRoots in internal/mcp/repoguard.go - the guard exists because a PR once merged to the wrong repo) uses that roots round-trip as its structural caller-repo signal. On a 2026-07-28 session it now gets no signal, and the guard falls through to the MADTEA_CALLER_REPO env hint alone, which is unset by default.

Current exposure

None immediate. No current published client negotiates 2026-07-28 (the TypeScript conformance harness cannot), and every consumer today, Claude Code included, speaks 2025-11-25, where the roots cross-check still works. #387 preserved today's behavior for today's clients, documented the fork in repoguard.go, and pinned the roots test to a wire-level 2025-11-25 client. The change is fail-open into the guard's existing no-signal path, so it adds no false blocks. But the deprecation window is about 12 months, and once the primary client moves to 2026-07-28 the secondary wrong-repo protection stops functioning for callers that do not set the env hint.

Options

  1. Accept the degradation and adopt the SDK's roots-replacement path (SEP-2322 InputRequests), so the guard regains a structural signal before consuming clients move to 2026-07-28. My lean.
  2. Migrate now, before any wider 2026-07-28 adoption.
  3. Require MADTEA_CALLER_REPO set by the harness for modern-client sessions, as a compensating control.

Related: #387.

Follow-up from #387 (2026-07-28 / go-sdk v1.7.0). This changes a security and isolation posture, so I want it settled deliberately rather than folded into routine SDK upkeep. ## The degradation SEP-2577 deprecates the MCP roots feature, and go-sdk v1.7.0 refuses server-initiated `roots/list` on any session that negotiates >= 2026-07-28. madtea's wrong-repo write guard (`callerRepoFromRoots` in internal/mcp/repoguard.go - the guard exists because a PR once merged to the wrong repo) uses that roots round-trip as its structural caller-repo signal. On a 2026-07-28 session it now gets no signal, and the guard falls through to the `MADTEA_CALLER_REPO` env hint alone, which is unset by default. ## Current exposure None immediate. No current published client negotiates 2026-07-28 (the TypeScript conformance harness cannot), and every consumer today, Claude Code included, speaks 2025-11-25, where the roots cross-check still works. #387 preserved today's behavior for today's clients, documented the fork in repoguard.go, and pinned the roots test to a wire-level 2025-11-25 client. The change is fail-open into the guard's existing no-signal path, so it adds no false blocks. But the deprecation window is about 12 months, and once the primary client moves to 2026-07-28 the secondary wrong-repo protection stops functioning for callers that do not set the env hint. ## Options 1. Accept the degradation and adopt the SDK's roots-replacement path (SEP-2322 InputRequests), so the guard regains a structural signal before consuming clients move to 2026-07-28. My lean. 2. Migrate now, before any wider 2026-07-28 adoption. 3. Require `MADTEA_CALLER_REPO` set by the harness for modern-client sessions, as a compensating control. Related: #387.
hexajon self-assigned this 2026-08-17 17:45:48 +00:00
Author
Owner

Decided: migrate the roots-derived signals to SEP-2322 within the deprecation window, not now.

State of play: roots is deprecated (SEP-2577, June 2026) but fully functional on a rolling ~12-month window, into 2027. SEP-2322 (Multi Round-Trip Requests / InputRequests) is the successor for the roots/list request. No published client negotiates SEP-2322 yet, and Claude Code still implements roots/list and roots/list_changed today.

Decision:

  • Keep using roots now. The wrong-repo accident-detector and the #416 scope re-derivation both read it, and it works for a year-plus.
  • Track client SEP-2322 adoption. Migrate the roots-derived signals before roots degrades, once a client actually negotiates SEP-2322 - not against a spec surface no client speaks yet.
  • Do not drop roots for the MADTEA_CALLER_REPO env hint: #416 relies on roots, and the hint is unset by default, so it is not a substitute.

The wrong-repo guard degrades fail-open on no signal (accident-detector, ADR 0007), so there is no security-boundary regression during the transition; explicit owner_repo / dir targeting and MADTEA_CALLER_REPO stay available. Clearing needs-decision. Related: #416 (uses roots), #412 (MCP-spec adoption epic).

Decided: migrate the roots-derived signals to SEP-2322 within the deprecation window, not now. State of play: roots is deprecated (SEP-2577, June 2026) but fully functional on a rolling ~12-month window, into 2027. SEP-2322 (Multi Round-Trip Requests / InputRequests) is the successor for the roots/list request. No published client negotiates SEP-2322 yet, and Claude Code still implements roots/list and roots/list_changed today. Decision: - Keep using roots now. The wrong-repo accident-detector and the #416 scope re-derivation both read it, and it works for a year-plus. - Track client SEP-2322 adoption. Migrate the roots-derived signals before roots degrades, once a client actually negotiates SEP-2322 - not against a spec surface no client speaks yet. - Do not drop roots for the MADTEA_CALLER_REPO env hint: #416 relies on roots, and the hint is unset by default, so it is not a substitute. The wrong-repo guard degrades fail-open on no signal (accident-detector, ADR 0007), so there is no security-boundary regression during the transition; explicit owner_repo / dir targeting and MADTEA_CALLER_REPO stay available. Clearing needs-decision. Related: #416 (uses roots), #412 (MCP-spec adoption epic).
Author
Owner

A correction to the description above, from reading the code today.

The per-call wrong-repo guard does not use roots. consolidated_tool.go calls guardRepoResolutionForCall(ctx, nil, ...) with a nil session, so callerRepoFromRoots returns no signal there and the guard relies on the MADTEA_CALLER_REPO hint alone.

The one place the roots answer matters is guardLocalGitTarget (internal/mcp/multirepo.go). It guards the local-git tools (add, commit, push, pull, branch create and delete, finish) when a call passes no dir=:

  • Roots name a repo other than the server's repo: the call is refused.
  • Roots confirm the server's repo: the call is allowed. Without that confirmation, a server repo that contains nested repos is ambiguous, and the call is refused with a request for dir=.

So migrating off roots affects nested-repo workspaces (dir-less local-git calls are refused until dir= or MADTEA_CALLER_REPO is given) and the root-versus-server mismatch check. The new gopls gate leg in #624 raises this again, because gopls flags the deprecated call.

A correction to the description above, from reading the code today. The per-call wrong-repo guard does not use roots. `consolidated_tool.go` calls `guardRepoResolutionForCall(ctx, nil, ...)` with a nil session, so `callerRepoFromRoots` returns no signal there and the guard relies on the `MADTEA_CALLER_REPO` hint alone. The one place the roots answer matters is `guardLocalGitTarget` (`internal/mcp/multirepo.go`). It guards the local-git tools (add, commit, push, pull, branch create and delete, finish) when a call passes no `dir=`: - Roots name a repo other than the server's repo: the call is refused. - Roots confirm the server's repo: the call is allowed. Without that confirmation, a server repo that contains nested repos is ambiguous, and the call is refused with a request for `dir=`. So migrating off roots affects nested-repo workspaces (dir-less local-git calls are refused until `dir=` or `MADTEA_CALLER_REPO` is given) and the root-versus-server mismatch check. The new gopls gate leg in #624 raises this again, because gopls flags the deprecated call.
Author
Owner

Update from reading the code today. The premise in the description above, that no client negotiates 2026-07-28 and Claude Code speaks 2025-11-25, no longer holds. A 2026-07-28 session now rejects a tool result that omits resultType (fixed in #740), which only happens when the negotiated revision is 2026-07-28. So the primary client is on 2026-07-28, go-sdk refuses server-initiated roots/list on that session, and guardLocalGitTarget's roots cross-check has no signal there. It degrades fail-open (ADR 0007): a dir-less local-git call in a nested-repo workspace falls back to needing dir= or MADTEA_CALLER_REPO. No trust-boundary regression, and the narrow impact the 2026-09-24 correction described.

The successor is ready in the SDK. The go-sdk implements SEP-2322 on both ends: a handler returns InputRequests as an InputRequiredResult, and the client fulfills a ListRootsRequest input request and retries. So migrating the roots-derived signal is now viable, not against a surface no client speaks.

It is not a drop-in. On a 2026-07-28 session the server cannot request roots synchronously; the SEP-2322 shape is a result that asks for input and a client-driven retry. So guardLocalGitTarget changes from a synchronous roots call into that two-round-trip flow. I will do that migration as its own focused change, linked here, rather than fold it into unrelated work. It connects to #624 (gopls flags the deprecated call) and #416 (also reads roots).

Update from reading the code today. The premise in the description above, that no client negotiates 2026-07-28 and Claude Code speaks 2025-11-25, no longer holds. A 2026-07-28 session now rejects a tool result that omits `resultType` (fixed in #740), which only happens when the negotiated revision is 2026-07-28. So the primary client is on 2026-07-28, go-sdk refuses server-initiated `roots/list` on that session, and `guardLocalGitTarget`'s roots cross-check has no signal there. It degrades fail-open (ADR 0007): a dir-less local-git call in a nested-repo workspace falls back to needing `dir=` or `MADTEA_CALLER_REPO`. No trust-boundary regression, and the narrow impact the 2026-09-24 correction described. The successor is ready in the SDK. The go-sdk implements SEP-2322 on both ends: a handler returns InputRequests as an `InputRequiredResult`, and the client fulfills a `ListRootsRequest` input request and retries. So migrating the roots-derived signal is now viable, not against a surface no client speaks. It is not a drop-in. On a 2026-07-28 session the server cannot request roots synchronously; the SEP-2322 shape is a result that asks for input and a client-driven retry. So `guardLocalGitTarget` changes from a synchronous roots call into that two-round-trip flow. I will do that migration as its own focused change, linked here, rather than fold it into unrelated work. It connects to #624 (gopls flags the deprecated call) and #416 (also reads roots).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
sixfold-space/madtea#428
No description provided.