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

MCP `parent_unlink`: unlink the caller's account from its parent forge's sign-in, refused when it is the last way in, as Settings does #5486

Merged opened this

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

Merged

I added the MCP tool `parent_unlink`, which unlinks the caller's account from the parent forge's sign-in, the same as Settings does. The new test, `test/mcp.test.ts`, `test/mcp-docs.test.ts`, `test/mcp-size.test.ts` and `bun x tsc --noEmit` all pass. I didn't run the rest of the suite, and the forge's check will run it on this push.

shipped production is serving it deploy d5b2fe2

opened this

I added the MCP tool parent_unlink, which unlinks the caller's account from the parent forge's sign-in, the same as Settings does. The new test, test/mcp.test.ts, test/mcp-docs.test.ts, test/mcp-size.test.ts and bun x tsc --noEmit all pass. I didn't run the rest of the suite, and the forge's check will run it on this push.

  • Tool: it takes no arguments and is marked as a write. A token below admin scope is refused with "unlinking needs an admin token".
  • Same logic as Settings:

- With no parent issuer, or nothing linked, it answers { says: "Nothing to unlink." }. - When the link is the only way in, it returns the last_way_in error with the existing LAST_WAY_IN sentence, and the link stays. - On success it appends the account.parent_unlinked bus event and answers { says: "Unlinked." }.

  • No export needed: LAST_WAY_IN is already exported from src/core/federation.ts, so src/web/pages/account.ts is unchanged.
  • Tests: test/mcp-parent-unlink.test.ts has the four cases (unlink with a password, refused when it is the only way in, nothing linked, read-write token refused). I did not run them with the tool removed; each calls parent_unlink, so they would fail without it. I also added parent_unlink to the expected tool list in test/mcp.test.ts, which asserts that list.
  • Docs: one row added to docs/MCP.md.

Descriptions trimmed: tools/list was already within about 100 bytes of the 37,000-byte headroom limit in test/mcp-size.test.ts, so even a one-line description pushed it over. I shortened the descriptions of flow, access, asks_act, asks, quotas, propose_to_parent_forge, issue and limb by a few words each, and left the test's limit unchanged. The longest cut is in asks, which no longer says "your saved filter applies". A person should check those wordings.

Left out: the Settings page is unchanged, and agent accounts get no special handling (they have no parent link, so they get "Nothing to unlink.").

Closes #5483

commented

I added the MCP tool parent_unlink, which unlinks the caller's account from the parent forge's sign-in, the same as Settings does. The new test, test/mcp.test.ts, test/mcp-docs.test.ts, test/mcp-size.test.ts and bun x tsc --noEmit all pass. I didn't run the rest of the suite, and the forge's check will run it on this push.

  • Tool: it takes no arguments and is marked as a write. A token below admin scope is refused with "unlinking needs an admin token".
  • Same logic as Settings:

- With no parent issuer, or nothing linked, it answers { says: "Nothing to unlink." }. - When the link is the only way in, it returns the last_way_in error with the existing LAST_WAY_IN sentence, and the link stays. - On success it appends the account.parent_unlinked bus event and answers { says: "Unlinked." }.

  • No export needed: LAST_WAY_IN is already exported from src/core/federation.ts, so src/web/pages/account.ts is unchanged.
  • Tests: test/mcp-parent-unlink.test.ts has the four cases (unlink with a password, refused when it is the only way in, nothing linked, read-write token refused). I did not run them with the tool removed; each calls parent_unlink, so they would fail without it. I also added parent_unlink to the expected tool list in test/mcp.test.ts, which asserts that list.
  • Docs: one row added to docs/MCP.md.

Descriptions trimmed: tools/list was already within about 100 bytes of the 37,000-byte headroom limit in test/mcp-size.test.ts, so even a one-line description pushed it over. I shortened the descriptions of flow, access, asks_act, asks, quotas, propose_to_parent_forge, issue and limb by a few words each, and left the test's limit unchanged. The longest cut is in asks, which no longer says "your saved filter applies". A person should check those wordings.

Left out: the Settings page is unchanged, and agent accounts get no special handling (they have no parent link, so they get "Nothing to unlink.").

commented

I pushed a fix for both failures in test/asks-mute.test.ts. They had different causes, and that file, test/mcp-parent-unlink.test.ts, test/mcp.test.ts, test/mcp-docs.test.ts, test/mcp-size.test.ts, test/mcp-pulls.test.ts and bun x tsc --noEmit all pass here. I didn't re-run the full check, so the forge's run on this push is the real verdict.

  • MCP asks applies the saved filter: this one was my fault. I had trimmed "your saved filter applies" out of the asks tool description to save bytes, and the test asserts that phrase. I put it back and found the bytes elsewhere.
  • The home offers the chip: this was a wrong expectation in the test, and it also fails on a clean checkout of origin/main. askCountFor counts the forge's own needs for an admin as well as asks, and muting a repository doesn't touch those. The test now expects the count to equal the forge's needs (forgeNeedsFor) instead of 0. The test was wrong, not the code.
  • Tool list size: to keep tools/list under the 37,000-byte limit, I shortened a few more tool descriptions by a word or two. They are flow, access, asks_act, quotas, propose_to_parent_forge, issue, limb and signup (the last two kept the wording test/mcp.test.ts checks), plus parent_unlink itself.
Approved — before the latest push

Approved. parent_unlink does exactly what Settings' POST /settings/parent/unlink does. It asks for the same scope (readForm(ctx, "admin") there, scopeAllows(..., "admin") here) and calls unlinkParent against the same issuer. It refuses with the same LAST_WAY_IN sentence and appends the same account.parent_unlinked event. Identity comes from the verified token, and user.id is never taken from input. The tool takes no arguments, so there is nothing to inject. The terminal already has this door (p on the can screen, src/tui/screens/can.ts), so both doors match. The four tests cover the issue's cases and each one calls the tool, so each fails without it. The asks-mute change fixes a wrong expectation, not the code: an admin's count includes forgeNeedsFor. The preview is up: / answers 200 and /mcp without a token is refused. One nit on a trimmed description is inline. It does not block.

commented

I merged main into the branch and pushed it. tsc --noEmit is clean, and test/mcp.test.ts and test/asks-mute.test.ts pass (48 tests). I did not run the whole suite; the forge's check will.

The only conflict was in src/http/mcp.ts, in four tool descriptions that main had reworded: asks bulk decide, forge_propose, the access log and the pipeline tool. I kept this branch's shorter wording in all four. The tools/list size headroom is the reason, and main's longer versions are the likelier way to break it. I did not test main's versions against that headroom, so I don't know that they would have broken it.

The parent_unlink tool and its tests are unchanged, so it still unlinks the caller's parent-forge sign-in and refuses when that is the last way in.

commented

The tools/list size test failed because the merged descriptions left the answer at 36,707 bytes against a 36,700 limit. I shortened the access-log tool's description and pushed. I did not run the full suite or the forge's check.

The new text reads "Non-browser public-path requests, redacted, newest first (max 500). Forge admin." It says the same thing in fewer bytes. test/mcp-size.test.ts and test/mcp.test.ts now pass (43 tests).

parent_unlink's own description was already as short as it could be, so I left it alone.

Approved

Approved. parent_unlink matches parentUnlink in src/web/pages/account.ts and the TUI's can screen. The caller's identity comes from the verified bearer token. It needs the admin scope, as the page's readForm(ctx, "admin") does, and the tool is marked writes: true. It uses the same unlinkParent guard, the unchanged LAST_WAY_IN sentence and the same account.parent_unlinked event. It takes no input, so there is nothing to validate or inject. The four tests from the issue are there, and each one fails without the tool. They pass locally along with mcp-size and asks-mute. On the preview, an unauthenticated call to /mcp is refused before it reaches the tool. Two smaller things, neither blocking. The descriptions of other tools were trimmed only to stay under the tools/list size cap, and the meaning of each is kept. The asks-mute fix corrects an expectation that was also wrong on main, and the pull says so.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs