A session can rename itself through the live Collins tool socket - #209
Conversation
PR 3 of the session MCP tools plan: the app-side socket service goes live and the first tool works end to end. mcpserver.py owns a Gio.SocketService on the per-instance Unix socket, fully async on the main loop (read -> reply -> read per connection, so a peer that stops reading stalls only itself), with injected list_tools/dispatch so CI tests the whole connection machinery without GTK. The service refuses socket paths past sun_path's 107 bytes: Gio silently truncates them, which produced a listener no shim could ever dial (observed live). providers.py gains MCP_CONFIG_PATH plus a supports_mcp_config capability flag, and appends --mcp-config to new/resume/continue/chat commands - never to `claude attach`, which takes no flags and joins a process that already has its servers. app.py brings the service up in do_startup (any failure logs and leaves commands untouched), adds the codebase's first do_shutdown to tear it down, and dispatches calls by walking /proc ancestry from the shim's hello pid to the tab whose shell spawned its claude (TerminalTab.owns_pid_ancestors); processes not under any tab get a clean identity error. set_session_title lands in the manual-name slot via MainWindow.rename_session_tab, exactly as a hand rename does. Verified against claude CLI 2.1.226 headlessly: the tool listed, called, and replied through a real -p session; a throwaway app instance renamed a real tab through the socket, and the CLI merges an stdio server's env block into the inherited environment (the question PR 2 deferred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ScBAgKFEdAAeQmcYGoGCA
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ScBAgKFEdAAeQmcYGoGCA
|
@claude review |
|
Claude finished @ghackett's task in 4m 55s —— View job Review complete
Solid, well-tested PR — the framing, lifecycle, and capability-flag plumbing are careful and the 24+7 new tests cover the protocol edges (bad hellos, oversize lines, concurrent connections, stop-mid-request, and the two real shim↔service end-to-end tests) well. One security gap worth fixing before more tools land on top of this identity mechanism, plus a couple of minor notes. 1. The shim's identity (pid) is self-reported and never verified against the real peer
That pid is then load-bearing for authorization: Unix domain sockets give you a kernel-verified alternative for exactly this: 2. No automated coverage for the dispatcher glue in
|
Review follow-ups. The shim's declared pid is load-bearing for authorization (the dispatcher walks /proc ancestry from it to pick the tab a call may act on), and any local process of the user's can open the socket - so _greet now checks the hello pid against the peer's real pid via Gio.Socket.get_credentials() and drops liars. Credentials being unreadable also fails the hello: this path is Linux-only already (the dispatcher reads /proc), so an unanswerable kernel means broken, not trustworthy. The dispatcher's branching moves into GTK-free mcptools.run_tool_call() (validate first and unconditionally, identity second so a bad call fails identically whoever makes it, then the handler), with CI tests pinning the order and error strings; app.py keeps only the widget-touching halves. Re-verified live: the headless app-instance probe still renames a real tab through the socket, and a non-tab client still gets the clean identity error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ScBAgKFEdAAeQmcYGoGCA
|
Both review findings addressed in 5e7c015: 1. Spoofable hello pid → kernel-verified via SO_PEERCRED. 2. Dispatcher glue coverage. The validate → identity → handler branching moved into GTK-free Re-verified live after the changes: the headless throwaway-app probe still renames a real tab through the socket, and a non-tab client still gets the clean identity error. Full suite: 1240 passing. 🤖 Generated with Claude Code |
PR 4 of the [session-mcp-tools spec](#201): the `open_in_editor` and `show_image` handlers land behind the socket service #209 wired up. An agent can now put the file it's talking about on the user's screen — "I've put the failing test on screen" instead of hoping they click a path — and pop a screenshot, plot, or render into the in-app lightbox without launching an external viewer. ## What changed - **`mcptools.TOOLS`** grows the two schemas: `open_in_editor(path, line?)` (line is 1-based, minimum 1) and `show_image(path)`. The existing table-driven validator covers them — wrong types, `line: 0`, and stray arguments are all rejected app-side, since the socket is reachable by any local process. - **`app.py`** adds the two handlers plus `_mcp_resolve_file`: a relative path resolves against the running agent's cwd first (it may have cd'd into a worktree), then the tab's project root — the same order clickable file references use (`terminal._reference_roots`). - `open_in_editor` requires the file inside the session's project (`can_open_in_editor`), converts the 1-based line to the editor's cursor, and opens via the calling tab's own editor. - `show_image` follows the clicked-image-reference recipe: any readable image shows, inside the project or not; the lightbox's "Open in Editor" button only appears when the tab could actually open it. - **`window.py`** promotes `_open_in_tab_editor` to public `open_in_tab_editor`: a session-attributed call must land in the calling session's own tab — the window-level `_open_in_editor` picks a tab by project membership and may switch tabs, which is the wrong door for an agent-driven open. ## Screenshots A staged session called its own tools through the real socket. `open_in_editor("src/widget.py", line=12)` — cursor at 12:1 in the status bar:  `show_image` on a staged render — lightbox over the window, with its Open in Editor button since the image lives inside the project:  ## Verification - 75 GTK-free tests pass locally (`test_mcptools` + `test_mcp_shim` + `test_mcpserver`), including new schema/validation coverage for both tools and the tool-list assertions updated to track the table. - E2E, headless, against a throwaway app instance driving the **real socket path** (hello + call frames; identity faked to the open tab, since the probe isn't a descendant of the tab's shell — the ancestry walk itself was e2e-verified in #209 and is unchanged here): - `open_in_editor` with a relative path + line → editor opened at 12:1 ✔ (screenshot) - `show_image` with an absolute path → lightbox presented ✔ (screenshot) - `/etc/hostname` → "That file is outside this session's project" ✔ - missing file → "No such file: src/nope.py" ✔ - non-image to `show_image` → "Not an image Collins can display" ✔ - `line: 0` → "'line' must be at least 1" ✔ - missing `path` → "Missing required argument: path" ✔ Per the spec, v1 pre-allows nothing — both tools go through the CLI's normal permission prompts. Next up is PR 5, `notify_user`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016dyuyFPinBYmRVsRUHYRTQ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR 3 of the session MCP tools plan (after #201's ancestry walk and #204's shim + protocol layer): the app-side socket service goes live, launched commands carry
--mcp-config, and the first tool —set_session_title— works end to end through a realclaudeCLI.What's here
collins/mcpserver.py(new):SessionToolService, aGio.SocketServiceon the per-instance Unix socket. Fully async on the GLib main loop — no threads; per connection the cycle is strictly read → reply → read, so a peer that floods requests without reading replies stalls only itself. Takes injectedlist_tools()/dispatch(pid, tool, args)callables, so the whole connection machinery is CI-testable without GTK. Untrusted-peer rules: a first frame that isn't a well-formed hello, or any framing violation, disconnects rather than guesses.sun_path's 107 bytes, producing a listener no shim can ever dial (the shim degrades cleanly, so the feature would just silently not work).start()now refuses over-long paths loudly; real paths under$XDG_RUNTIME_DIRsit far below the limit.collins/providers.py: module-levelMCP_CONFIG_PATH(set by app.py only once the whole chain is up) plus asupports_mcp_configcapability flag mirroringsupports_fork. New/resume/fork/continue commands and the chat argv all carry the flag; the attach branch carries nothing by construction (claude attachaccepts no flags and joins a process that already has its servers) — tested.collins/app.py: service bring-up indo_startup(one failure path: log, leaveMCP_CONFIG_PATHunset, commands go out exactly as before — the tools are conveniences, never load-bearing), the codebase's firstdo_shutdownto tear it down, and the dispatcher:/procancestry walk from the shim's hello pid to the tab whose shell spawned itsclaude, argument re-validation app-side (the socket is reachable by any local process), and theset_session_titlehandler.collins/window.py:MainWindow.rename_session_tab()— the rename dialog's save path without the dialog. The rename lands in the manual-name slot, so it permanently stops auto-titling for that session, exactly as a hand rename does.collins/terminal.py:TerminalTab.owns_pid_ancestors()over_candidate_pids()(both the pty's foreground pgrp leader and the spawned child, per the daemon-wrapper note there).mcp_shimsubprocess against the real service — the seam neither module's own tests crossed — plus 7 new provider tests for the flag.Verified live (headless, CLI 2.1.226)
claude -p --mcp-config …session listed the tool, called it through--allowedTools, and echoed the service's "Session renamed." reply; dispatch received the shim's pid.COLLINS_APP_ID, isolated data): service up, runtime dir keyed by app id, typed resume command carries the flag, a client fed into the tab's own terminal renamed the session through the socket (screenshots below), and a client not descended from any tab got the clean identity error — the expected shape for daemon-hosted bg jobs until the identity fallback lands.envblock into the inherited environment (probe saw both the config var and a parent-env marker). Either behavior works for the shim, but now it's recorded.Screenshots
Before: the tab's typed command carries
--mcp-config. After: a client run from the tab's own shell calledset_session_title— tab title and sidebar row both renamed, ok reply visible in the terminal.Per the spec, v1 pre-allows nothing — every first use goes through the CLI's own permission prompt. Next up: PR 4 (
open_in_editor+show_image), PR 5 (notify_user).🤖 Generated with Claude Code
https://claude.ai/code/session_019ScBAgKFEdAAeQmcYGoGCA