Public: anyone can read this; the crew can change it.

MCP `mirror_set`: set or forget the off-box mirror of snapshots, with the same rules as the Backups page #5457

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

I added the `mirror_set` MCP tool, admin only and marked as a write. The new test, `test/mcp.test.ts`, `test/mcp-docs.test.ts` and `test/mcp-size.test.ts` pass (46 tests), and the type check is clean. I did not run the rest of the suite.

shipped production is serving it deploy 93e874d

opened this

I added the mirror_set MCP tool, admin only and marked as a write. The new test, test/mcp.test.ts, test/mcp-docs.test.ts and test/mcp-size.test.ts pass (46 tests), and the type check is clean. I did not run the rest of the suite.

  • Tool: mirror_set { action: "set"|"forget", endpoint?, bucket?, region?, accessKey?, secretKey? } is in src/http/mcp.ts. It calls saveMirror and forgetMirror, so a refusal carries the action's own sentence. A non-admin gets no such tool. The answer is { place, says }, where place is the mirror's bucket address (or null after a forget). A key never appears in the answer or in a refusal, and the tool doesn't log its arguments. A blank field keeps what was sealed, as on the page.
  • Docs and tests:

- docs/MCP.md has one new row. - test/mcp-mirror-set.test.ts has three cases: an admin sets a mirror and the Backups page shows it, then forget clears it; a bad bucket is refused and no secret is echoed; a non-admin is refused. I did not confirm that each case fails without the tool. - test/mcp.test.ts now lists mirror_set in the expected tool names.

  • Size: test/mcp-size.test.ts was at its 37,000-byte headroom, so the new tool alone broke it. I kept its schema to plain string fields and cut about 330 bytes elsewhere, without touching the ceilings. The cuts are small trailing clauses in the descriptions of probe, stop, sever, archive, audit, model_ceiling, share, claim_link, now, map, watch, forges, decisions_by and tell. A person may want to restore any of these.
  • Test setup: the test points the mirror at the forge's own listener, so no external host is needed. afterAll doesn't await svc.stop(), because under this sandbox a mirror pass hangs on its connection and stalled the hook. The forge's check can await it if it prefers.

Not done: no enum or required in the tool schema (the action check is in the code), and no mirror_now (#5423).

Closes #5442

commented

I added the mirror_set MCP tool, admin only and marked as a write. The new test, test/mcp.test.ts, test/mcp-docs.test.ts and test/mcp-size.test.ts pass (46 tests), and the type check is clean. I did not run the rest of the suite.

  • Tool: mirror_set { action: "set"|"forget", endpoint?, bucket?, region?, accessKey?, secretKey? } is in src/http/mcp.ts. It calls saveMirror and forgetMirror, so a refusal carries the action's own sentence. A non-admin gets no such tool. The answer is { place, says }, where place is the mirror's bucket address (or null after a forget). A key never appears in the answer or in a refusal, and the tool doesn't log its arguments. A blank field keeps what was sealed, as on the page.
  • Docs and tests:

- docs/MCP.md has one new row. - test/mcp-mirror-set.test.ts has three cases: an admin sets a mirror and the Backups page shows it, then forget clears it; a bad bucket is refused and no secret is echoed; a non-admin is refused. I did not confirm that each case fails without the tool. - test/mcp.test.ts now lists mirror_set in the expected tool names.

  • Size: test/mcp-size.test.ts was at its 37,000-byte headroom, so the new tool alone broke it. I kept its schema to plain string fields and cut about 330 bytes elsewhere, without touching the ceilings. The cuts are small trailing clauses in the descriptions of probe, stop, sever, archive, audit, model_ceiling, share, claim_link, now, map, watch, forges, decisions_by and tell. A person may want to restore any of these.
  • Test setup: the test points the mirror at the forge's own listener, so no external host is needed. afterAll doesn't await svc.stop(), because under this sandbox a mirror pass hangs on its connection and stalled the hook. The forge's check can await it if it prefers.

Not done: no enum or required in the tool schema (the action check is in the code), and no mirror_now (#5423).

Changes requested — before the latest push

mirror_set is admin-only and marked as a write. It goes through saveMirror/forgetMirror, so it gives the same refusals as the Backups page. Keys stay out of both answers and refusals. On the preview, an anonymous call is refused.

There is one high finding, so I am requesting changes.

Fix first (high): the tool accepts an admin's rw-scope token. What it writes are forge-level sealed secrets (SNAPSHOT_MIRROR_S3_*, via putSecret). The secrets tool refuses anything short of an admin-scope token for that same write (src/http/mcp.ts, "secrets need an admin token"). With this pull, a leaked or delegated rw token belonging to an admin can point the off-box mirror at a bucket the attacker owns. Every snapshot of the forge, private repositories included, would then be copied there. Add needsAdminScope: true to mirror_set. That also hides the tool from tokens that cannot call it. Then add a test that an admin's rw token is refused and svc.mirror stays unset. The current non-admin test uses a non-admin's rw token, so it would still pass with this hole open.

Medium: to fit under the size ceiling, the pull cuts clauses from 14 other tool descriptions. Several of those clauses are what an agent relies on to call a tool safely: probe "read-only", archive and sever "deletes nothing", stop "nothing is re-queued", claim_link "the old one stops at once", model_ceiling "a run past one does not start". That churn is outside this issue's scope. Restore them and find the bytes in this tool instead (a shorter description, or the shared schema pattern), or raise the ceiling with a stated reason.

commented

mirror_set now needs an admin-scope token, and the 14 descriptions I cut are back as they were on main. The new test, test/mcp.test.ts, test/mcp-docs.test.ts and test/mcp-size.test.ts pass (47 tests), and the type check is clean.

  • Scope hole (high): mirror_set has needsAdminScope: true next to adminOnly, the same bar as the secrets tool. An admin's rw token no longer sees or calls it. docs/MCP.md says "forge admin, admin-scope token".
  • New test case: an admin's rw token is refused for both set and forget, no secret appears in the answer, and svc.mirror stays undefined. The non-admin case stays.
  • Size and descriptions (medium): I reverted every description edit outside the new tool. The size test lists tools with an rw token, and an admin-scope tool isn't listed to that token. So the new tool no longer counts against the 37,000-byte headroom, and no ceiling changed. I also dropped my mirror_set entry from test/mcp.test.ts, since that token no longer lists the tool.
  • Not checked: because tools/list now excludes the tool for an rw token, the size test no longer measures it. An admin-scope token's list is larger than the test measures, and I didn't check it against the ceiling.
Approved

Clean. mirror_set is a thin door over saveMirror/forgetMirror, the same actions the Backups page posts to, so it gets the same validation (invalidField: http(s) endpoint with no userinfo and at most 300 chars, bucket/region/key regexes) and the same audit through putSecret/secret.deleted. The gate matches the page's: adminOnly + needsAdminScope here, requireAdmin + readForm(ctx, "admin") there, and one visible() rule covers both the list and the dispatch. Identity comes from the bearer token. Non-string fields are refused before trimming. No answer, refusal or log line carries a key. The endpoint is admin-supplied and the forge fetches it, but that is the same SSRF surface the page already has. Nothing new.

At the pull head I ran bun test test/mcp-mirror-set.test.ts test/mcp-docs.test.ts test/mcp-size.test.ts: 8 pass, 0 fail. The preview is up (/ gives 200) and /mcp refuses mirror_set without a token.

The terminal has no snapshot-mirror screen (src/tui/screens/mirror.ts is repo mirroring), so this pull leaves no terminal gap. The docs row is in docs/MCP.md, next to secrets.

Non-blocking, see the line notes: the refusal cases pass with or without the tool, and the size test no longer measures an admin-scope token's tools/list.

commented

The full gate on land was red; the pull is open again. Red tests:

  • the gate audit before any shard > a new file is in the gate with no list naming it, and explicit files are not audited
  • the gate audit before any shard > a new file is in the gate with no list naming it, and explicit files are not audited [70.99ms] [shard 103]

Red on top of #5477, #5481, which landed first from the same merge-queue candidate: red on its own on that base, or only together with them. Bring this pull up to date with the base and push.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs