(remote): launch a session on a host inside tmux (#218, #222) - #427
Conversation
Starts claude --session-id <uuid> in a new detached tmux session on a declared host, after checking the directory, tmux and claude exist, then attaches to the created pane through the existing attach adapter. The launch tier is available with tmux, and the project's + opens a directory and permission dialog on such a host. Refs #218 Refs #222
|
Reviewing |
devsuitup
left a comment
There was a problem hiding this comment.
Adversarial review at 351e09c: 0 blocking. Injection holds. The cwd is allow-listed and single-quoted inside sh -c '…'. It went through both parsing layers with spaces, a leading - segment and a trailing space, and arrived intact in the stub tmux argv. Every other value is either generated or allow-listed. Host output is parsed strictly and re-validated before attach. --dangerously-skip-permissions is sent only on the explicit Dangerous Skip choice. The open-terminal extraction is verbatim, and local launch is untouched (full suite green). Stop issues kill-pane, never kill-session. Of 16 mutations replayed, 14 were killed. Being fixed: a wiring pin for the new IPC, the id case before the active-session guard, launch exempted from a failed refresh (like Send), the remoteAlias match in the pending re-injection, the two surviving mutations (4096/4097, strict === true), and parsing the last stdout line.
Lowercase the session id before the activeSessions guard, keep the launch tier available after a failed last refresh (launch runs its own ssh), re-inject a pending remote session under its own host's project, and read the launch target from the last non-empty output line. Pin the main.js wiring as source text and add the boundary and real-sh tests. Refs #218 Refs #222
devsuitup
left a comment
There was a problem hiding this comment.
Re-review at 504acdd (delta from 351e09c), merged with main as cefb336: 0 blocking. Text pins now cover the new IPC wiring: launchBlockReason in the deps, the lowercased id before the guard, and registration under the descriptor id. Launch is exempt from a failed refresh, as Send is, while a host never synced still refuses. The pending re-injection matches on remoteAlias, covered by a test that runs the shipped loadProjects. Two cases are now pinned: 4096/4097 and strict === true. A real sh -c run checks the argv. The stdout parse reads the last line. All 9 mutations were killed, and the launch tests pass under Node 22 + c8.
Starts a Claude session on a remote host from the project's
+, inside tmux, and attaches to it. Second item of #218 and the launch half of #222; enrolment stays open.What changes
computeHostProfilemakeslaunchavailable under the attach rule: the probe foundtmux, or a live descriptor names a tmux pane, and the probe did not saytmux: false. Otherwise the reason starts with "needs tmux on the host".launchBlockReason(profile)is the gate the main process checks.remote-launch.js. Builds onesh -c '<script>'ssh command:test -don the directory,command -v tmux,command -v claude(exit codes 9/10/11, each with its own message), thentmux new-session -d -P -F ... -s switchboard-<uuid8> -c "$cwd" 'claude --session-id <uuid> [flags]'. The output carries the pane target and pid, which go straight to the existing attach adapter (socket discovery and pid-reuse guard unchanged). No wait for the next refresh; the host is refreshed afterwards.remote-launch-session. Checks the host is declared, the tier is available, the id is a uuid, then launches and attaches. The attached session is registered throughregisterRemoteAttachSession, now shared with the remote branch ofopen-terminal.+of a remote project is enabled when the launch tier is available and opens a dialog: directory (known paths of that host in a datalist, or typed) and permission mode, Dangerous Skip included.launchRemoteSessionadds a pending row, launches, shows the session; a refusal is written in the terminal tab.docs/remote-hosts.md("Launch a session"),.ai/contexts/session-cache.md, CHANGELOG (New).Validation and quoting
No interpolated value is trusted: the cwd must match
^/[A-Za-z0-9._+@:,=/ -]*$, be at most 4096 bytes and have no..segment (so no quote,$, backtick, backslash, newline or leading-can reach the shell); the uuid is matched by regex and the tmux name derives from it; the permission mode is checked against an allow-list. The cwd is still single-quoted (shellSingleQuote) and the whole script is wrapped insh -c, so a fish or csh login shell never parses it. The same checks run in the dialog, inhandleLaunchRequestand in the adapter.Follow-ups from review
remote-launch-sessionlowercases the id before itsactiveSessionsguard.loadProjectsre-injects a pending remote session under the project of its own host.parseLaunchOutputreads the last non-empty line, so an rc file printing on stdout does not hide the target.remote-launch-sessionandregisterRemoteAttachSessionpinned as source text intest/main-remote-launch-wiring.test.js; added tests for the 4096/4097-byte cwd bound, strictdangerouslySkipPermissions === true, a realsh -crun ofbuildLaunchCommandwith stub tmux, and the pending re-injection (test/app-pending-remote-reinject.test.js). Nine more mutations, all killed.Tests
test/remote-launch.test.js(20): exact command string; hostile cwd (',$(), backtick, newline,;,|,.., leading-, relative, NUL, oversize, non-string); uuid; permission flags; output parsing; stop command for the created pane target iskill-pane, neverkill-session; adapter outcomes (no dir, no tmux, no claude, ssh failure, timeout, unreadable output, throw); flow with a fake transport (tier refusal, undeclared host,test -dfailure reported with attach never called, attach called with the created target and size); two runs under a realshwith stubbedtmuxandclaudepinning the exact tmux argv and the missing-directory exit code.test/remote-host-profile.test.js: launch available with tmux (probe or descriptor), unavailable without it with its reason,launchBlockReason.test/dom-sidebar-remote-tier-gates.test.js:+enabled and opening the remote dialog, not the local popover; disabled without tmux.test/dom-remote-launch.test.js(8): dialog lists that host's known paths, free path, mode and Dangerous Skip mapping, refusal of empty/relative/hostile paths, Cancel/Escape/Enter/backdrop;launchRemoteSession(extracted frompublic/app.js) adds the pending row, calls the IPC, and writes a refusal into the terminal.attachto be the top tier on a tmux host now expectlaunch.Red first:
remote-launch.test.jscould not load (module absent), five profile tests failed, all dialog and flow tests failed. One mutation per guard, all killed: cwd allow-list,..guard,test -d,command -v tmux,command -v claude, uuid regex, permission allow-list, tier gate, declared-host check, attach-failure check, tmux name, launch-failure stop, launch tier without tmux, launch via descriptor, null-profile gate, button enabled, button click, dialog path check, known paths per host, failure branch oflaunchRemoteSession. The-c "$cwd"flag was also mutated against the real-shtest.task check: 0 lint errors, 3123 tests passed, 0 failed.Not verified
claude. tmux output of-P -Fandpane_pidcarryingTMUXin/proc/<pid>/environfollow the tmux manual and the existing attach code, not an observation.claudeon the PATH of a non-interactive ssh command is required; when it is added only by an interactive profile the launch reports it as not found.registerRemoteAttachSessioninmain.jshave no direct test (main.js cannot be loaded in the suite); the refactor of theopen-terminalremote branch is covered only by the existing source pins.claudethat exits at once closes the pane and the attach reports the probe error.