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

A forge link says when it ends and its sender revokes it in one tap #10830

Merged opened this

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

Merged

A sender can now revoke an open forge link and undo it within a minute. A revoked or expired link shows a plain "This link has ended" page with status 410 and sets no session.

shipped production is serving it deploy 951be3d

opened this

A sender can now revoke an open forge link and undo it within a minute. A revoked or expired link shows a plain "This link has ended" page with status 410 and sets no session.

The new test/claim-revoke.test.ts and the MCP size and table tests pass, and bun x tsc --noEmit is clean. I ran the related forges*, mcp*, spawn-api, spawn-events and audience tests; the only two failures were the ones fixed below. I did not run the full suite. scripts/fast.ts called the diff too broad to name its tests.

What changed

  • Link state: src/spawn/claim.ts stores a revoked timestamp (r) inside the existing claim setting. The link's secret is untouched and never logged.

- revokeClaim and restoreClaim flip that timestamp. A restore only works within 60 seconds of the revoke, while the link is unspent and still in date. - linkStateIn reads a child's state (open, used or ended) without writing to it.

  • Revoke action: changeClaim in src/actions/spawn.ts is admin-only and same-origin checked. It writes a forge.claim_revoked or forge.claim_restored event whose payload holds only the forge name.
  • Sent-links list: /forges shows a state badge on each row, plus a Revoke button when the link is open.

- Revoke and Undo go through POST /forges/:name/revoke-link and /undo-link, and the confirmation page carries the Undo button.

  • MCP: claim_link takes action: "revoke" | "undo" instead of a new tool. docs/MCP.md is regenerated, and no ceiling was raised.
  • Claim page: a revoked link answers 410 with "This link has ended". Expired links keep their existing page.

Tests

  • test/claim-revoke.test.ts covers revoke-then-use (410 on both /claim/options and /claim, no set-cookie) and use-then-revoke (refused). It also checks that a cross-origin revoke is refused, and that undo works and the receipts omit the secret.
  • I added link: null to hand-built rows in test/forges-attention.test.ts and test/tui-forges.test.ts, so they type-check with the new ClientRow.link field.
  • test/mcp-forge-view.test.ts read only the first page of tools/list. My action enum pushed forge_view onto page two. That test was wrong, so it now follows nextCursor.

Not done: a revoked link cannot be revived after the minute; send a fresh link instead. There is no new MCP tool.

To try: open /forges as an admin and press Revoke beside an open link. Open the link in another browser to see the 410 page, then press Undo within a minute.

Closes #10725

commented

A sender can now revoke an open forge link and undo it within a minute. A revoked or expired link shows a plain "This link has ended" page with status 410 and sets no session.

The new test/claim-revoke.test.ts and the MCP size and table tests pass, and bun x tsc --noEmit is clean. I ran the related forges*, mcp*, spawn-api, spawn-events and audience tests; the only two failures were the ones fixed below. I did not run the full suite. scripts/fast.ts called the diff too broad to name its tests.

What changed

  • Link state: src/spawn/claim.ts stores a revoked timestamp (r) inside the existing claim setting. The link's secret is untouched and never logged.

- revokeClaim and restoreClaim flip that timestamp. A restore only works within 60 seconds of the revoke, while the link is unspent and still in date. - linkStateIn reads a child's state (open, used or ended) without writing to it.

  • Revoke action: changeClaim in src/actions/spawn.ts is admin-only and same-origin checked. It writes a forge.claim_revoked or forge.claim_restored event whose payload holds only the forge name.
  • Sent-links list: /forges shows a state badge on each row, plus a Revoke button when the link is open.

- Revoke and Undo go through POST /forges/:name/revoke-link and /undo-link, and the confirmation page carries the Undo button.

  • MCP: claim_link takes action: "revoke" | "undo" instead of a new tool. docs/MCP.md is regenerated, and no ceiling was raised.
  • Claim page: a revoked link answers 410 with "This link has ended". Expired links keep their existing page.

Tests

  • test/claim-revoke.test.ts covers revoke-then-use (410 on both /claim/options and /claim, no set-cookie) and use-then-revoke (refused). It also checks that a cross-origin revoke is refused, and that undo works and the receipts omit the secret.
  • I added link: null to hand-built rows in test/forges-attention.test.ts and test/tui-forges.test.ts, so they type-check with the new ClientRow.link field.
  • test/mcp-forge-view.test.ts read only the first page of tools/list. My action enum pushed forge_view onto page two. That test was wrong, so it now follows nextCursor.

Not done: a revoked link cannot be revived after the minute; send a fresh link instead. There is no new MCP tool.

To try: open /forges as an admin and press Revoke beside an open link. Open the link in another browser to see the 410 page, then press Undo within a minute.

Changes requested — before the latest push

Revoke/undo logic is sound (compare-and-set update, admin + same-origin check, secret kept out of events). Changes needed, all in this round:

  1. FIRST: both doors. Revoke/undo exists on the page and MCP but not in the terminal. Add it to the forges screen in src/tui/ (state per row, revoke, undo), entered where AGENTS.md says, gated like the page (admin only). tui-forges.test.ts only gained link: null, so nothing covers it. Add docs/features/<name>.md for the how-to (nothing was added).
  2. Tests missing: (a) undo after 60s must be refused (restoreClaim takes now; test it at 61s), and undo of a used or expired link; (b) the web routes POST /forges/:name/revoke-link and /undo-link: a non-admin or guest gets nothing, cross-origin is refused at the route, revoke page offers Undo. Only the action is tested today.
  3. Undo is reachable only from the confirmation page. Once you leave it, an ended link on /forges shows no Undo for the minute, so 'undoable for the first minute' is met only by luck. Show Undo on the row while the link is ended and revoked < 60s (expose revoked-at in LinkState or a field on ClientRow).
  4. Performance: clientRows now opens every child's forge.db synchronously on each /forges render (linkStateIn). That is N sqlite opens on the hot path with a 50 ms budget. Read it off the heartbeat/ledger or cache it, and bound the work.
  5. Clean: const { stored } = found ?? {} in restoreClaim is convoluted; the two UPDATE statements duplicate; Database as Sqlite import duplicates the existing Database type import in claim.ts. Tidy.
  6. tools/list budget (#6217): the new action enum pushed forge_view to page two and you changed that test to follow the cursor. Shrink the claim_link doc/schema so it stays on the page instead, or say plainly in the PR why not.
  7. Design (principles 1, 8): the post-revoke page is a whole extra page for one sentence; a revoke on the row with an inline receipt would show the real row (principle 2). Prefer redirecting back to /forges with the receipt and Undo beside the row.

Any admin can revoke any child's link, not only its sender; confirm that is intended or restrict to the spawner.

commented

A forge link now says when it ends, and its sender can revoke it in one tap and undo it for the first minute. This round adds the terminal door, an Undo on the row, a cheaper /forges render and the missing tests.

The tests I ran pass: claim-revoke, tui-forges, mcp-forge-view, mcp-tools, mcp-table, client-list, forges-* and the spawn and claim suites. tsc and bun scripts/gate.ts are clean. I did not run the whole suite; the forge's check does that.

Changes, in the order of the review

  1. Terminal. The forges screen in src/tui/screens/forges.ts gains l (end a link) and u (undo one), admin only. Each row says link open or link ended, undo with u. Page, MCP and terminal all go through one changeLink in src/spawn/manage.ts, which src/spawn/door.ts wraps with the admin and same-origin checks. The how-to is docs/features/forge-link-revoke.md.
  2. Tests in test/claim-revoke.test.ts and test/tui-forges.test.ts:

- Undo is refused at 61 s and allowed at 59 s. - A link that expired cannot be revoked or brought back, and neither can a used one. - On the web routes, a member and a guest get 404, a cross-origin post is refused, the revoke redirect shows the receipt, and the row offers Undo. - The MCP revoke/undo and the terminal's changeLink are covered.

  1. Undo on the row. An ended link revoked less than 60 s ago shows Undo on /forges, so it survives leaving the page. LinkInfo now carries revokedAt and undoable() decides.
  2. Performance. clientRows no longer opens any child database. The child's /api/v1/forge answer now includes link, only for the parent's secret, and the list reads it from the cached probe it already makes. A revoke or undo drops that cache entry, so the next list asks again. A child running older code reports no link and shows no Revoke button.
  3. Tidy. The duplicate sqlite import is gone and a single swap replaces the two UPDATE statements. restoreClaim no longer uses found ?? {}. changeClaim is removed from src/actions/spawn.ts.
  4. MCP budget. I shortened the claim_link description and the schema, so forge_view stays on page one. I reverted the mcp-forge-view test to its original form. The margin was under 20 bytes, so the next tool added will hit it again.
  5. Post-revoke page. It is replaced by a 303 back to /forges?link=revoked&forge=<name>, with an inline receipt. The receipt is fixed words and only appears for a forge in the list.

Not changed: any admin can revoke any child's link, not just its spawner. This matches re-issuing a link, which is already any admin's, so I left it. Say if you want it restricted to the spawner.

To try it: as an admin, open /forges and press Revoke on a forge with an open link. Undo sits beside the row for a minute. In the terminal, open Forges you sent and press l, then u. Over MCP, call claim_link with {name, action: "revoke"} or "undo".

Approved

Earlier asks are met. The terminal door is there (l and u on Forges you sent, via the shared changeLink) and its how-to is docs/features/forge-link-revoke.md. README is untouched. Undo is on the row and survives leaving the page. The list no longer opens child databases. Tests cover the 59 s and 61 s edges, an expired or used link, the member and guest 404s, cross-origin refusal, MCP and the terminal action. Security reads clean: link state in /api/v1/forge is returned only after the parent-secret check, both web routes are admin-only and same-origin checked, the receipt uses fixed words and only names a forge in the list, and no secret is in any event. The design uses badge, btn and row with no new component class. Non-blocking: the MCP tools/list margin is under 20 bytes, so the next tool added will hit it. Non-blocking: the TUI picker path itself is only tested for the empty case; the action is tested.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs