Repository navigation
fix(mcp): an agent cannot close its own pane, workspace or window (0228 C S1) - #534
Closed
nt-nerdtechnic wants to merge 7 commits into
Closed
nt-nerdtechnic wants to merge 7 commits into
nt-nerdtechnic wants to merge 7 commits into
Conversation
ui_invoke("ui.pane.close", {paneId: <own>}) closed the caller's own pane:
only cli_close_agent refused self. Add one self-scope check derived from
the caller's credential alone (pane id from its token, workspace from the
registry, windows mirroring that workspace's panes) and apply it to every
close route ui_invoke reaches: ui.pane.close -> self-close,
ui.workspace.close -> self-workspace-close, ui.window.close ->
self-window-close. A ui.pane.reclaim batch drops the caller's own pane and
reports it refused (reason "self"), reclaiming the rest.
The window repeats the same rule in useUiActionBus using caller_pane_id and
the new caller_workspace_path field, which the backend fills from the
registry, never from args.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…folders not strings HIGH-1: workbench.action.closeWindow is reachable through ui_invoke and runs window.close() in the window that answers, which for a pane caller is its own. Refused in the backend whenever the request lands in one of the caller's windows, and again in the window (hosts the caller or holds its workspace). MED-1: workbench.action.closeActivePane and rebuildFocusedPane act on the focused pane; the window refuses them while that pane is the caller. MED-2: own workspace is compared by folder identity (st_dev, st_ino), so a case variant on a case-insensitive volume is still the caller's own; the window compares case-folded. MED-3: the S4 contract for an empty ui.workspace.close path is recorded as a TODO comment and a skipped test. LOW: a pipeline slot cannot abort, reset, restart or restart_from its own run (window side); a reclaim batch keeps the caller's refused row when the window answers with an error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… only via their tools - A main window holding the caller's workspace with none of its panes in it (they all sit in detached group windows) is now part of the caller's own windows in the backend too. Windows announce the workspaces they hold with ui.invoke.ready (new optional `workspaces` field, re-announced on change; [] from a detached group window). - ui_invoke answers ui.window.close / ui.workspace.close with ui_dedicated_tool: only window_close / workspace_close record the action and tell the user which pane did it (review #541 MED-1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ndow; closeWindow via window_close - ui.pipeline.reset ends every pane in the window that runs it (onKillAll), manual panes included and without a run. Refused for a pane caller when it lands in one of the caller's windows: in pipeline_reset and ui_invoke (backend, via _lands_in_own_window) and in the window whenever it hosts the caller. - workbench.action.closeWindow joins the dedicated close actions: ui_invoke answers ui_dedicated_tool and points at window_close, so closing anyone's window leaves a record. - _lands_in_own_window also reads the workspaces windows announce; _same_directory falls back to a case-folded compare on macOS/Windows for a folder that cannot be stat'ed. test_pipeline_window_requests_address_the_panes_own_workspace now expects the reset from a pane of that window to be refused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nt-nerdtechnic
added a commit
that referenced
this pull request
Oct 20, 2026
- Merge resolution with #534 R2: S1 now owns app.main_window_sessions() (S3 had added one of the same name); the S3 copy is dropped and the window helpers keep only sessions with a page-load id. ui_invoke's close actions are S1's ui_dedicated_tool now, so the S3/S4a pane check and name stamping for them are gone (ui.window.focus keeps its stamp). - MED-2: window_close refuses the last main window off macOS ("last-window"), on the tool side and again in main (agentCloseRefusal, asked by the window through window:canCloseSelf before it lets go of anything, and at window:closeSelf). - LOW-1: the 55s deadline answers only its own confirm dialog. - Tests: last window (off macOS / on macOS), a main window holding the caller's workspace with none of its panes is the caller's own, and the ui_invoke route points at the tools. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n sys.platform CI's test_osplat ratchet (TestNoScatteredPlatformBranches) failed on macOS and Windows: _same_directory read sys.platform in mcp_server/server.py. The decision now sits behind a new osplat seam, paths_ignore_case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ge-load id Same definition as DESIGN-C S3 (#535) uses, so the two slices share one function instead of defining it twice (test_module_level_duplicate_defs). Every caller here already skips a window without an id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nt-nerdtechnic
added a commit
that referenced
this pull request
Oct 20, 2026
…indows - #534 and #535 each added app.main_window_sessions(); with both merged test_module_level_duplicate_defs failed. #534 now carries the window_id filter #535 used; the second copy is dropped here (C2 drops it from #535 when #534 lands). - test_plugin_agent_tools_proxy: MAIN_TOOLS_LIST_SHA256 re-pinned at this tip for cli_open_agent's description and `window` parameter (DESIGN-C S2). With main's cli_open_agent entry the digest is the previous pin. - test_a_tilde_path_is_expanded_before_it_is_used: expanduser reads USERPROFILE on Windows, so the test sets it next to HOME. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nt-nerdtechnic
marked this pull request as ready for review
October 21, 2026 02:06
nt-nerdtechnic
added a commit
that referenced
this pull request
Oct 21, 2026
…28 C S1) (#534) Squash-merged from origin/pr-534. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Collaborator
Author
|
Squash-merged into main as 5ca9fc8. |
nt-nerdtechnic
added a commit
that referenced
this pull request
Oct 21, 2026
…ssions - Merges main at #534 (5ca9fc8). CHANGELOG: both sides' entries kept. - #534 and this branch each added app.main_window_sessions() (identical since ef110ff); git does not see two new definitions as a conflict, so the merged module defined it twice. This branch's copy is dropped and #534's is kept: app.py now equals main. - ko-KR (new on main, #553) gets this branch's one key, msg.window-focused-by. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
nt-nerdtechnic
added a commit
that referenced
this pull request
Oct 21, 2026
#534, #535, #541 and #546 landed as squashes, so their commits on this branch and on main differ while their content is the same. Merged with -X ours (conflicting hunks were that shared content, kept as this branch has it with S4b on top), then checked against origin/main: the only differences are S4b's own 21 files. Two hunks were redone by hand: CHANGELOG had lost five of main's entries and is rebuilt from main's file plus the S4b entry; the golden cli_open_agent entry is regenerated from the live server. Co-Authored-By: Claude Opus 5.5 (1M context) <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.
Slice S1 of DESIGN-C (cross-workspace child panes): an agent can never close its own pane, its own workspace or its own window. Refused at the tool/backend side and again in the window. Includes the review #534 fixes and review #541 MED-1.
The self scope (backend, from the credential only)
_self_scope/_self_scope_refusalinmcp_server/server.py:os.stat→(st_dev, st_ino),_same_directory). A case variant on macOS/Windows, a symlink, a trailing slash or~all match. String compare is used only when a path cannot be stat'ed.ui.invoke.ready(new optionalworkspacesfield, re-sent on change; a detached group window sends[]).When a path cannot be stat'ed (a folder that is gone), the fallback string compare is case-folded on macOS and Windows.
Case variants are stopped by the backend. The window only knows its own spelling of a path, so its second check compares case-folded strings and cannot see symlinks. The backend's stat comparison catches case variants and symlinks. Tests:
test_a_case_variant_of_your_own_workspace_is_still_yoursandtest_a_case_variant_registration_still_counts_its_window_as_yours; they run on this APFS volume and skip on a case-sensitive one.Inventory: registered commands that act on the caller's own window, workspace or pane
Main-window command registry (
App.vue), reachable throughui_invoke. Editor, Git and plugin windows do not answerui.invoke.request.ui.pane.closeself-closewhen the named pane (or an alias of it) is the callerui.pane.reclaimreason: "self"), kept on an error reply tooworkbench.action.closeWindow(⌘⇧W)window.close(), which tears down that window's PTYsui_invokeanswersui_dedicated_tooland points atwindow_closefor ANY window, so closing someone else's window also leaves a record (R2 MED-1). The self check (self-window-closewhen it lands in one of the caller's windows) stays in_self_scope_refusal. Window: refused when it hosts the caller or holds its workspaceworkbench.action.closeActivePane(⌘W)self-closewhile the focused pane is the caller (only the window knows focus)workbench.action.rebuildFocusedPaneself-closewhile the focused pane is the callerui.pipeline.resetonKillAll(): ends EVERY pane in the window, manual ones included, with no confirmation and no run neededself-closein bothpipeline_resetandui_invokewhen the request lands in one of the caller's windows (_lands_in_own_window, which now also reads announced held workspaces). Window: refused whenever it hosts the caller (R2 HIGH-1)ui.pipeline.abort/restart/restart_fromself-closewhen the caller is a pipeline slot pane in that window (review LOW-1). Window only: the backend does not know slot membershipui.window.close/ui.workspace.close(registered by S4)ui_invokeanswersui_dedicated_tool; onlywindow_close/workspace_close(S4) may do it, because they record the action and show which pane did it. Those tools call_self_scope_refusal, which refusesself-window-close/self-workspace-closeChecked and not guarded, because none of them closes or ends the caller:
workbench.action.reloadWindow: the PTYs live in the backend and are reattached.ui.workspace.switch: switching does not disturb panes.ui.pane.interrupt: aborts a turn, closes nothing.ui.pane.focus,ui.pane.place,ui.settings.close,workbench.action.closeModaland theopen*/focus*/zoom*commands.The existing
cli_close_agent(self-close) andcli_reclaim_agent(self-reclaim) are unchanged.Contract left for S4 (review MED-3): S4 must use exactly the arg names
pathandwindowId, and must either requirepathor treat an empty one as the caller's own workspace. This is recorded as a TODO in_self_scope_refusaland as a skipped test intest_mcp_self_scope.py.Red → green
Every behaviour was red first. Each round's tests were run against the previous commit with the source change reverted (
git apply -R):All green now.
Local verification (targeted only)
safe-pytest.sh <wt> -k "test_mcp_self_scope or test_plan_mcp_ui_invoke or test_plan_mcp_reclaim_agent or close_agent or test_share_mcp_tools or pipeline or ui_invoke_ready or window_id" -n 2→ 322 passed, 1 skipped (the S4 TODO), fake-security calls 0. This includes the golden tools/list test.test_plan_mcp_subdir_workspace::test_pipeline_window_requests_address_the_panes_own_workspace: its pane resets its own window's pipeline, which is now refused by design. The test now assertsself-closefor that step; re-run → 27 passed together with the self-scope file.pnpm vitest run src/renderer/src/composables/__tests__/useUiActionBus.test.ts --maxWorkers 1→ 67/67.pnpm typecheck:web→ 0 errors.ruff→ clean.RULE ZERO
…still_goes_through/…not_refusedtests and the existing ui_invoke, reclaim and pipeline suites.caller_workspace_path;ui.invoke.readygains optionalworkspaces.ui_invoke;ui_invokeno longer runsui.window.close/ui.workspace.close(neither exists on main yet), norworkbench.action.closeWindow(usewindow_close);Protection check
credential_*,quota_*,usage_*): diff vs origin/main is empty.deflines are a signature change and two tests turned from async to sync under the same names._self_scope_refusal(caller, action, args)→(caller, action, args, workspace_path="")(new in this PR)._same_directory,_lands_in_own_window,_DEDICATED_CLOSE_ACTIONS,app.main_window_sessions(),Session.held_workspaces;hostsPane,focusedPaneIds,isPipelineSlot,heldWorkspaces(all optional).Known boundary (documented, unchanged)
A same-user process that reads another pane's token can act as that pane (architecture.md, "Trust boundaries"). These guards stop argument-level and tool-level self-close, not token theft.
Security-related: second review requested after this round.
🤖 Generated with Claude Code