From 1f6f21fd24304006a19f0a4d5baac9be25a94960 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 6 Nov 2026 03:29:35 +0100 Subject: [PATCH] docs(adopt): document the routes and the overlay, correct stale comments The two routes are public under /api/v1, so they get an API reference section. The overlay gets an architecture note and a CLAUDE.md rule: an adopted session has session.remote and session.docker undefined and a neutral workingDir, so any new code that reads workingDir as a workspace must also test isAdopted. That is the class of bug this round fixed a dozen times. Records a measured limit: while the view exists an owner's kill-session leaves the window alive under it until the tab closes. Comments that still called workingDir the foreign pane's cwd now say what is true since the wrapper got its neutral folder. Also drops a zh-CN key that duplicated an existing one, and updates the two structure tests pinned on the lines this round changed. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 + docs/api-reference.md | 65 +++++++++++++++++++++ docs/architecture-invariants.md | 14 +++++ src/foreign-tmux-discovery.ts | 4 +- src/foreign-tmux.ts | 15 ++++- src/session.ts | 14 ++--- src/tmux-manager.ts | 10 ++-- src/web/public/foreign-sessions.js | 4 +- src/web/public/i18n.js | 7 +-- src/web/routes/readmymind-routes.ts | 3 +- test/foreign-sessions-i18n.test.ts | 9 ++- test/foreign-sessions-ui.test.ts | 6 +- test/foreign-tmux.test.ts | 10 +++- test/routes/foreign-adoption-routes.test.ts | 5 +- test/session-options-structure.test.ts | 14 +++-- 15 files changed, 148 insertions(+), 34 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4f350fdb..66bee256 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,6 +175,8 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph **Docker cases**: a case can point at a **container** running any CLI mode inside it, a **LOCATION OVERLAY on cases, never a `SessionMode`**. One long-lived container **per case**, shared by its sessions: killing a session kills only its in-container tmux, **never** `docker stop` while siblings remain. The workspace is bind-mounted at the **same absolute path**. Credentials are **seeded**, never shared RW. **NEVER a create-time `-e` for secrets, NEVER `--privileged`, NEVER the docker socket.** A drifted config (label hash) REFUSES the launch. ⚠️ An **adopted** container (`DockerCase.owned === false`) is only `exec`ed into: never create, start, stop, restart, remove, `docker commit` or `docker pause` it; fail closed. Test `owned === false`, never truthiness. ⚠️ Apply `owned` AFTER `dockerConfigHash`. ⚠️ Run modes come from the CONTAINER (`availableModes`), and a failed probe is normal for an OWNED case. ⚠️ Root exec user drops the bypass flag via the registry's `overlays.docker.rootCommand`, never a branch. ⚠️ Adoption is admin-only in multi-user mode. ⚠️ Loopback prod needs `CODEMAN_DOCKER_BRIDGE_HOOKS=1` for in-container hooks. → [architecture-invariants#docker-cases](docs/architecture-invariants.md#docker-cases), `docs/docker-cases.md` +**Adopted tmux sessions** (`src/foreign-tmux.ts` pure, `foreign-tmux-discovery.ts`, `foreign-sessions.js`, `POST /api/sessions/adopt`, admin-only in multi-user mode): a tmux session a human started (local, in a container, over ssh) opens as a tab through a wrapper session on OUR socket, a LOCATION OVERLAY like remote and Docker, never a `SessionMode`. ⚠️ `session.remote` and `session.docker` are undefined on one (the connection is on `adopt.*`) and its `workingDir` is the neutral `/tmp`, so anything that treats `workingDir` as the session's workspace, or branches on remote or docker to mean "not here", must also test `session.isAdopted`. ⚠️ `isAdopted` asks who launched the process, so it is never folded into `isExternalCliMode()`. ⚠️ `foreignTargetKey()` is the ONE identity of a target; a socket and a name collide across containers and hosts. ⚠️ Never set anything on the foreign server beyond our own `codeman-view-*`: no `window-size`, no key tables. ⚠️ Session names are someone else's text: they pass the allowlist at discovery, so a browser only ever sends an opaque id. → [architecture-invariants#adopted-foreign-tmux-sessions](docs/architecture-invariants.md#adopted-foreign-tmux-sessions), `docs/api-reference.md` + **Docker Compose deployment** (`docker/`): Codeman runs in a container and spawns Docker cases as **SIBLING** containers via the host socket, never nested. `resolveDockerDaemonMountSource()` maps HOME bind sources into the daemon's namespace (`CODEMAN_DOCKER_HOST_HOME`); `CODEMAN_CASES_PATH` makes workspaces resolve to the same absolute path on both sides. ⚠️ `CODEMAN_CASES_PATH` must move every consumer: resolve it only via `config/cases-dir.ts`. ⚠️ `.dockerignore` matches whole paths: keep `**/.env` or `docker/.env` secrets ship in the image. ⚠️ Long-form binds create missing sources ROOT-OWNED: `Start-Codeman.sh` pre-creates them, and `docker/entrypoint.sh` (root, `cap_add: [CHOWN, DAC_OVERRIDE, KILL, SETGID, SETUID]` against `cap_drop: ALL`, `KILL` for tini; pinned by the test) fixes ownership then drops to `PUID:PGID` via `setpriv`, never re-owning foreign dirs. ⚠️ Append `/opt/codeman-cli` and `~/.local/bin` to `PATH`, never prepend. ⚠️ `server.Dockerfile`, the compose file and `.env.example` feed the self-updater's environment gate (`docs/docker-self-update.md`). → [architecture-invariants#docker-compose-deployment](docs/architecture-invariants.md#docker-compose-deployment), `docs/docker-compose.md` **CLI registry** (`src/config/cli-registry/`): every run mode is a `CliEntry` (discovery, launch argv template, env handling, `capabilities`, and the `overlays` behind remote/docker pane commands). **No code outside `stock.ts` may branch on a CLI id**: use a capability field or a NAMED PROFILE (`profiles.ts`); `test/cli-registry-no-id-branching.test.ts` and `test/frontend-cli-no-id-branching.test.ts` enforce it. ⚠️ Config holds typed argv tokens, never shell text; literals are validated at LOAD time and a bad one rejects the whole entry. ⚠️ Keep `external`, `hooks` and `altScreen` independent; never derive one from another. ⚠️ Config regexes (`discovery.version.regex`, `capabilities.workDetect.workingLine`, `capabilities.workDetect.watchingLine`, `capabilities.workDetect.awaitingLine`, `capabilities.workDetect.blockedLine`, `capabilities.workDetect.composerFooterLine`, `capabilities.workDetect.composerPopupLine`, `capabilities.modelDetect.screenLine`) must compile through `compileVersionRegex()`. ⚠️ `privilegedParams[].param` names a LAUNCH PARAM, not the legacy `Config` field (bridged only by `launch.legacyConfigAliases`); a wrong name silently clamps nothing. ⚠️ Resolve the registry AT CALL TIME, never in a module-level const. ⚠️ Remote claude/omp arms of `buildRemoteLaunchCommand` are not covered by the pane-command golden. `~/.codeman/clis.json` overrides entries. It is WRITTEN only by the opt-in CLI management routes (`cliManagementEnabled`, default OFF; `/api/clis`, `cli-registry-routes.ts`), and only through `mutateRegistryFile()` in `registry-writer.ts` (the one exception: a config-backup restore of `clis.json`, which then calls `reloadCliRegistry()`), which serializes mutations and refuses (409) a file that does not parse or has group/world permission bits rather than overwriting it. Importing the registry still writes nothing. → [architecture-invariants#cli-registry](docs/architecture-invariants.md#cli-registry), `docs/cli-registry.md` diff --git a/docs/api-reference.md b/docs/api-reference.md index 9a269ad3..aaed4292 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1085,6 +1085,71 @@ the caller's case space, a linked case's exact path, or a Claude worktree of eit session, the create response carries `tabLayout` as it does for `tabGroupId`. Deleting the case forgets the value; existing sessions never move. +## Adopting a foreign tmux session + +A tmux session someone started by hand (`tmux new -s work`) lives on its own socket, +where Codeman does not look for its own sessions. These two routes list such sessions +and open one as a tab. Admin-only in multi-user mode (403), since the list shows other +people's processes and adopting a shell is arbitrary execution as the server account. + +`GET /api/v1/mux/foreign` lists candidates. Local sessions are always scanned, and +cached for 5 s. `?docker=1` and `?remote=1` add the containers behind the saved Docker +cases and the saved ssh hosts, one `docker exec` or ssh connection per target, so the +browser sends them only on an explicit scan. `?force=1` skips the local cache. + +```json +{ + "sessions": [ + { + "id": "fa9b76d36", + "location": "local", + "socketPath": "/tmp/tmux-1000/default", + "sessionName": "work", + "windows": 1, + "attached": false, + "createdAt": 1793931275000, + "mode": "claude", + "command": "claude", + "workingDir": "/home/me/project", + "adoptedBy": "961d0a82-6888-425d-a3ee-dab38ecbb598" + } + ], + "scannedAt": 1793931277916, + "notes": ["remote Build box: unreachable, or no tmux"], + "pollIntervalMs": 8000, + "canScanWide": true +} +``` + +`hostId`, `hostLabel` and `containerName` appear on a docker or remote candidate. +`adoptedBy` names the session already wrapping that target. `notes` says why a location +gave nothing, each opening with where it came from (`local`, `docker `, +`remote