Repository navigation
feat(mcp): per-account and per-node opt-out for the subshell MCP - #486
Open
theogravity wants to merge 10 commits into
Open
theogravity wants to merge 10 commits into
theogravity wants to merge 10 commits into
Conversation
Two switches stop wiring the subshell MCP into panes; off wins on either.
An account's Preferences card is the person declining the tool surface and
its context cost; a node's overview card is the machine's owner declining
it, enforced on the machine itself (mirror file, launch-time strip, and a
refusing 'subshell mcp' verb), so a host-registered harness stops with a
clear notice instead of serving anyway.
Both default ON, which inverts the ssh and preview gates beside them: MCP
wiring predates its own switch, so an untouched row reads allowed and an
upgrade strips comms from nobody. A disabled pane's token is minted
{report: [write]} instead of the full map, which drops the sibling surface
docs/security.md §4 accounts for, while the harness hooks (attention, exit,
harness-session, subagents/report) and token self-extension keep working.
A 403 at identity registration is now fatal in the MCP child: on a
narrowed token, serving anyway would make the off switch a lie.
Protocol 27 (set_mcp_enabled frame, ready.mcpEnabled, mcp_enabled event)
raises MIN_NODE_VERSION to 3.2.0 in the same commit as the agent, so
server and node ship together; the release cuts as app=all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Surface/docs slice (all MINORs and NITs): the account switch lives under Account, not Settings; a misled agent, not a mislead one; the configure page names the refusing process (the MCP child, not the harness); repoint the dead services/ token citation; label the manage-page link by its destination. Protocol/node slice (NITs): name set_mcp_enabled in the wired-command inventory; a mirror unlinked mid-read answers ABSENT (the allowed answer) rather than refusing; the version-pin comment states the pre-27 hazard the right way round. Server slice: an audit row only records a change that happened, so the setNodeMcpEnabled trail waits on a confirmed UPDATE. And one honest caveat the review surfaced: opencode, pi and oh-my-pi ship their roster reporter inside the MCP registration, so a switched-off pane of those dialects loads no bridge and reports no roster. The door stays open for it; nothing walks through. Documented at the tokens factory, the route, the AGENTS inventory, security.md, and the rules summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…one syscall The module had no test file while its ssh and preview twins did, which left the round-1 ENOENT branch and the corrupt-refuses rule unpinned. Clone of preview-flags.test.ts, with the inversion cells spelled out: absent ALLOWS, off stays a visible kind (a fold into absent would un-say every owner's refusal), an unreadable file announces the refusal it acts on, and a plane push is memoized as reported so nothing echoes back. The read itself loses the existsSync probe: readFileSync answers the existence question, ENOENT is the allowed absent, and the probe/read pair it replaced was two syscalls racing over one answer. The dangling-symlink asymmetry the first review noted goes with it; both now read absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…each Round-2 review: name which refusal fires where (the command refuses on a mirrored node; identity registration is the account-only or never-mirrored path), drop the false claim that a report-only key can still read its own log (it asks for the subshells scope), and stop the Preferences card JSDoc promising a roster the registration-coupled dialects never report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cts; pin the new arms Round-2 review on the control plane: - A disabled pane launches no MCP child, so nothing armed the 7-day extend and its hooks would quietly 401 once the TTL ran out under a pane in daily use. Each report verb now rides the extend in parallel, capped below the main POST's budget so no hook's worst case grows, skipped on . The claim the switch made always deserved. - oh-my-pi reports no roster at ANY switch position; its extension is an empty manifest stub. The caveat (and the restore path it promised) belongs to opencode and pi only, in all six places I had spread it. - setNodeMcpEnabled's absent-row guard and the ws-handler's mcp arms had no regression pins; both twins do. Added the ghost-row cell and four capture cells walking ready.mcpEnabled and the mcp_enabled event into the recording hook, including the silence-reads-ALLOWED inversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mark the main POST handled the moment it exists, so an injected transport that throws at creation cannot strand a rejection in a hook process; the extend-rides-every-verb loop now covers all four REPORT_VERBS members plus needs_attention, so a future verb that forgets the extend fails a cell; and the goes-quiet comment states the real healing path for a hand-deleted off-mirror (the next ready's silence re-pushes the row) rather than the default-row-only framing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins the review asked for: a report-only pane's extend-token acceptance at the HTTP boundary (a future scope-annotation on that door would otherwise 401 disabled panes' hooks a week in, silently); two card test files for the new cards, per the precedent each card's own JSDoc cites - the account card's inert-Unknown/fallback-on/rollback trio, and the node card's absent-reads-ALLOWED polarity (a triple-equals-true edit fails it), owner-only refusal copy, failed-write state. Docs accuracy: AGENTS.md's subshell-token bullet caught up to the switch (TTL story, report-only map, transfers in the quoted literal - stale since the transfers feature); section 6's reach claim now names the section 3 public bearer reads instead of overselling; section 4's map gained the transfers member; the extend wording says every report but exit, and notes derived keys meet the route's execute grant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The node's MCP switch now explains itself like the preview card beside it: one reason string feeds the tooltip, the focusable wrapper, and the gold line, including a Saving reason while a write is in flight (the bare switch was tab-past-and-never-know). The owner cell pins the aria wiring, which a bare-switch edit fails. Docs accuracy: the protocol reference page still said 26 while the branch shipped 27 (the 25-to-26 bump commit updated this same sentence, and this adds the ready-field clause the page's enumeration asks for); configure says what opencode and pi additionally lose when switched off, since their roster reporter rides the registration; the node's own AGENTS.md gains the mirror, the launch drop, and the refusing verb, per the subsystem-rules ruling that node gates live beside node code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The review caught that the acceptance was pinned for only two of the four report routes: a per-file revert of the session or roster door to the old requirePerm would silently 403 an opted-out pane's hooks (runReport swallows failures as a lost report) with every other suite green. The cell now POSTs each door with the report-only key, exit last because it writes the death row, and its comment said every verb rather than every verb but exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
theogravity
force-pushed
the
worktree-mcp-opt-out
branch
2 times, most recently
from
November 1, 2026 09:28
c4d33fb to
d6186b5
Compare
Fresh-eyes full passes on all three slices came back 0 MAJOR with ten smaller items, all quote-verified and fixed here. Accuracy: the token-lifetime story in three docs said the report-verb extend is the switched-off pane's substitute, but it rides every pane's reports; the four-door comment inherited a clause from round 4 that round 5 disproved (exit parks the row with status running, and the session door deliberately still records a parked row; only the roster door 409s); security section 6's "never serves" became "never acts", since a disabled child with an unreachable plane enumerates inert tools; the extend budget comment now states its real inequality (within every per-verb budget, equal at the fast one); the node-view polarity comment names the default-deny switches instead of "the two above"; the mcp-availability param cites the edit-grantee restart that actually ships; and the refused-mirror notice admits a corrupt file reads the same as an off one. Reference page: set_mcp_enabled joins the command table (the only set_* command the bump added and the page lacked), and the ready enumeration names all three mirror states. Structure: the account cards' duplicated probe/optimistic/rollback state machine is now one hook, useBooleanSetting, run by the notifications master switch and the MCP card alike, per the dedup rule their own test comments cited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two switches stop Subshell from wiring the MCP into panes, and off wins on either. An account's Preferences card is that person declining the tool surface and its per-turn context cost. A node's overview card is the machine's owner declining it, enforced on the machine itself, not just skipped by the plane.
docs/security.md§6 accounts for it.){report: [write]}instead of the full map. The sibling surfacedocs/security.md§4 accounts for is gone from the key, not merely unexposed; harness hooks and token self-extension still work, because everysubshell reportverb butexitnow rides a token extend in parallel, so a hook-live pane never reaches the 7-day wall without an MCP child to arm the periodic one.subshell mcpverb. A 403 at identity registration became fatal in the MCP child, so a host-registered harness exits with a clear notice rather than serving on a report-only key.MIN_NODE_VERSION3.2.0 land in the same commit as the agent's ownset_mcp_enabledcommand. Release cut must dispatchapp=all.Verification
bun run verify:changedplus per-package suites through the review rounds:@internal/server5035 pass / 0 fail,@internal/node1322, mcp-core 140, web 3204, docs 184, protocol, and the cross-subshell e2e set.lint:design,lint:lockfile, verify-types: green. CI's full lane set is green on the final revision (14 checks, all pass).test:clipassed every scenario exceptserver-update.sh, which refuses to run on a host with a per-user Subshell Server service installed (this dev box has one; that's the guard documented in.claude/rules/verification.md, CI runs the scenario).subshellMCP server; with the account switch off a new pane reports none AND still lights its attention bell (the report-only token working end to end); with the node (local) row off, a new pane likewise reports none. The stale-frame drop line on a real agent node cannot be staged againstlocal(no mirror file there); it's pinned by the node launch tests and the scripted-node push suites.Six review rounds (fan-out over protocol/node, server-api, and docs/UI slices, the last two a fresh full pass and its confirming re-verification) fixed, among others: the inverted reach claim in §6's refusal chain, an over-claimed roster dialect (oh-my-pi never had one), the "whole API reach" oversell against §3's public bearer reads, a false self-extension claim for disabled panes (answered by the extend-at-report behavior above), the stale token bullet in the server's AGENTS.md, the protocol reference page still saying 26, a disabled switch with no keyboard-reachable reason (now the preview card's composition), and pins for all four report doors, the extend door's ownership-only shape, and both new cards (including the absent-reads-ALLOWED polarity). Rounds 5-6 found the subtler ones: the report-verb token extend rides every pane's reports, not just the switched-off one's (three docs corrected); the exit report parks the row with status running and the harness-session door deliberately still records a parked row (a round-4 comment had it backwards); "never serves" became "never acts", since a disabled child with an unreachable plane enumerates inert tools; the protocol reference gained set_mcp_enabled; and the two account cards' cloned state machine became one shared useBooleanSetting hook. The final fresh full pass returned 0 MAJOR / 0 MINOR on every slice, confirmed against the fixing commit.
Fixes that verification found along the way: migration 0073 registered with the backup ledger (
LATEST_BACKUP_MIGRATION+ strip set), the remote-manager fixture runsrunAuthMigrationslike a real boot so a nodes delete resolves its FK graph, and the scoped-key route cell counts audit delta rather than emptiness.Docs: configure/security/troubleshooting and nodes/manage pages, the protocol reference,
docs/security.md§4/§6/§10, the rules summary, both appAGENTS.mddeltas, and the server audit inventory. Changeset:@internal/serverminor,@internal/nodepatch,@internal/docspatch.Co-Authored-By: Claude Fable 5 noreply@anthropic.com