mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 18:59:33 +01:00
feat(adopt): adopt tmux sessions a human started, in all three locations
A tmux session someone opened by hand (`tmux new -s work`, then `claude`, or just a shell) is invisible to Codeman: it lives on the default socket, not the instance-scoped one Codeman owns. The home screen now lists those, and one click wraps one in a Codeman tab. Local, in-container and over-ssh all work. The shape is a WRAPPER session, not a direct attach: adoption creates an ordinary `codeman-<8hex>` session on our own socket whose pane runs the attach. That makes "detach, never kill the foreign session" structural rather than a matter of discipline — `killSession` can only ever reach our own wrapper, so no shape of caller bug reaches the foreign server. View reclamation differs by location: local relies on client death and ssh on SIGHUP, but a `docker exec` does NOT die with its client, so the in-container view has to be reclaimed explicitly on kill or every adoption leaks one. Measured facts, recorded in the code comments: - A grouped session (`new-session -t`) buys `status off` and an independent current window. It does NOT buy an independent size. A window is one object with one size and a group shares it: on tmux 3.3a a bare attach and a grouped attach both shrink a 200x49 target to 80x23. Only `window-size largest` holds it, and that is a shared window option which survives our detach, so setting it would permanently rewrite the owner's configuration. Left unset. - Session names come from other people, while the local launch chain ends in `bash -c` + `JSON.stringify`, which escapes neither `$` nor backticks — the outer shell performs command substitution before the inner single quotes are considered (verified: both substitutions inside the inner quotes ran). So session names and socket paths go through a character allowlist and are dropped at DISCOVERY, which means a non-conforming candidate never gets an id and the adopt endpoint fails closed when it re-resolves. - `pane_current_command` is `node` for both claude and codex, so the mode can only come from a bounded process-tree walk of `pane_pid`'s argv; anything unrecognized is treated as a shell. An adopted session has to say no to every assumption that Codeman owns the pane: respawn, Ralph, the orchestrator and hook waits all refuse. The hook check runs BEFORE the mode check, because adoption asks who launched the process and no mode can vouch for a workspace Codeman never touched. `paneExit` is forced to unknown, since the pane runs `tmux attach` rather than the agent and its exiting with 0 only means the connection ended. Reboot restore refuses too: all Codeman ever had was an attach command, and replaying it would build a connection to a foreign session a reboot has almost certainly taken with it, while presenting whatever it lands on as a session we restored. Adoption is admin-only under multi-user mode: adopting someone's shell is equivalent to arbitrary host execution. Discovery is read-only, never creates a session, and degrades to an empty list whenever a probe cannot reach its target.
This commit is contained in:
1 parent
848ab48b0a
commit
245ee68c7c
27 files changed
+2622
-35
No files matched your search
+18
-3
@@ -1821,13 +1821,20 @@ export class WebServer extends EventEmitter {
|
||||
this.runSummaryTrackers.set(session.id, summaryTracker);
|
||||
summaryTracker.recordSessionStarted(session.mode, session.workingDir);
|
||||
|
||||
// Set working directory for Ralph tracker to auto-load @fix_plan.md (not supported for external CLIs)
|
||||
if (!isExternalCliMode(session.mode)) {
|
||||
// Set working directory for Ralph tracker to auto-load @fix_plan.md (not supported for external CLIs).
|
||||
// ⚠️ Also skipped for an ADOPTED session, and for two reasons: Ralph is refused
|
||||
// for one anyway, and its `workingDir` is the FOREIGN pane's cwd — a path that
|
||||
// need not exist on this host at all. Watching it logged a caught ENOENT on
|
||||
// every in-container adoption (`watch '/workspace/pythonserver'`), which is
|
||||
// noise pointing at a real category error rather than a real failure.
|
||||
if (!isExternalCliMode(session.mode) && !session.isAdopted) {
|
||||
session.ralphTracker.setWorkingDir(session.workingDir);
|
||||
}
|
||||
|
||||
// Start watching for new images in this session's working directory (if enabled globally and per-session)
|
||||
if ((await this.isImageWatcherEnabled()) && session.imageWatcherEnabled) {
|
||||
if ((await this.isImageWatcherEnabled()) && session.imageWatcherEnabled && !session.isAdopted) {
|
||||
// Same reason as the Ralph watcher above: an adopted session's workingDir is
|
||||
// an observation about ANOTHER host's (or container's) filesystem.
|
||||
imageWatcher.watchSession(session.id, session.workingDir);
|
||||
}
|
||||
|
||||
@@ -3524,6 +3531,14 @@ export class WebServer extends EventEmitter {
|
||||
// MuxSession.docker; state.json carries SessionState.docker), so recovery
|
||||
// rebuilds the `docker exec` launch instead of a broken local command.
|
||||
docker: muxSession.docker ?? savedState?.docker,
|
||||
// Adoption metadata round-trips for the same reason remote/docker do,
|
||||
// and one more: it is the ONLY thing that marks this session as
|
||||
// wrapping a process Codeman never launched. Dropping it on recovery
|
||||
// silently re-enabled respawn, Ralph and hook-backed waits against
|
||||
// someone else's live tmux session after every server restart —
|
||||
// measured, not hypothetical. The mux record is preferred because it
|
||||
// is what `killSession`'s detach-not-kill guard already reads.
|
||||
adopt: muxSession.adopt ?? savedState?.adopt,
|
||||
owner: recoveredOwner,
|
||||
// Tab lineage survives a restart. It is only decoration, so a parent
|
||||
// that did NOT come back is harmless: the frontend draws an edge only
|
||||
|
||||
Reference in new issue
Block a user