feat: Docker session mode (isolated per-case containers + export/import) — v1.4.0 - #158
Merged
Conversation
…ilders) Phase 0-2 of the Docker cases feature (docs/docker-cases-plan.md). Docker is a LOCATION OVERLAY on cases (not a 6th SessionMode), mirroring the remote-SSH feature: a local tmux pane runs `docker exec -it` into a durable in-container tmux server. The container is per-CASE, so multiple sessions share it. - types: DockerHost/DockerCase/SessionDocker + docker? on SessionState/MuxSession - src/docker-hosts.ts: storage, toSessionDocker, pure buildDockerBaseArgs/ buildDockerCreateArgs (cap-drop, no-new-privileges, --pull=never, mem==swap, never privileged/socket), containerApiUrl, hostGatewayAlias, config-hash, credential-mount resolution, daemon probes (VITEST no-op) - schemas: DockerHostSchema + DockerCaseLinkSchema (NO_SHELL_META guards) - tmux-manager: buildDockerLaunchCommand (image-check -> ensure -> start -> exec, resume-aware), buildDockerKillCommand (in-container tmux only, multi-session safe), stop/remove; wired into createSession/respawnPane/killSession - 40 unit tests (docker-hosts + docker-exec-options), typecheck clean Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docker/agent.Dockerfile: node:22 + claude/codex/gemini/opencode CLIs + git/ tmux/ripgrep/curl, secret-free, OpenShift arbitrary-uid-writable HOME (gid 0). scripts/build-agent-image.mjs: local build (decision "build locally on first use"), docker/podman auto-detect, --engine/--image/--no-cache flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
node:22-bookworm-slim ships a `node` user at uid 1000, so `useradd -u 1000` failed. Auto-assign the uid and rely on gid-0 + group-writable HOME so any runtime `--user <hostUid>:0` can write $HOME. Verified: image builds; toolchain (node/tmux/claude/codex/gemini/opencode) present; `--user 1000:0` writes /home/agent and `claude --version` runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Session: _docker field, constructor config, toState, createSessionOptions/ respawnPaneOptions (both interactive + shell paths), docker getter - resolveMuxAttachCwd returns /tmp for docker sessions (local wrapper only execs) - skip the LOCAL claude version probe for docker; probe the IN-CONTAINER version instead (deferred) so wheel-forwarding stays enabled (#154) - server restoreMuxSessions round-trips MuxSession.docker / SessionState.docker - full CI suite green (3444 passed) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- case-routes: GET/POST/PUT/DELETE /api/docker-hosts, POST /api/cases/docker-link
(creates workspace, probes daemon + tmux-in-image), docker listing in
GET /api/cases, docker-unlink (best-effort docker rm -f) in DELETE, single GET
- session-routes: /api/quick-start docker branch (rejects envOverrides/effort/
per-CLI config, probes availability + tmux, casePath=hostWorkspacePath, seeds
resume id, scaffolds hooks+CLAUDE.md if missing, threads docker into Session,
Ralph auto-config skipped for docker)
- CaseInfo gains location:'docker' + docker{} block
- typecheck clean; 157 route+docker tests pass
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An in-container hook curl carries Host: host.docker.internal:<port> (the derived CODEMAN_API_URL), so the always-on host guard must allow host.docker.internal / host.containers.internal or every in-container hook is blocked 403. Exact-match only; not a browser DNS-rebinding surface (resolves to the host only from inside a container netns). Verified end-to-end: quick-start launches claude/shell in a real container with the workspace bind-mounted and hooks scaffolded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fied) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oot reaper - src/docker-export.ts: full-image export (pause-consistent commit + save|stream + workspace tar + manifest -> one .codeman-container.tgz) and workspace-only; import validates manifest + per-member sha256, traversal-guards the workspace tar, docker load + quarantine re-tag (never overwrites a local tag). Bounded by runWithConversionLimit; free-space precheck; docker rmi in finally; sealed containers refuse full-image export. - routes: POST /api/docker-cases/:name/export (background + SSE), GET/DELETE /api/docker-exports, GET download, POST /api/docker-cases/import (-> new host+case) - instance-scoped boot reaper (docker-hosts.reapOrphanedDockerContainers) wired after restoreMuxSessions; never touches another instance's containers - SSE docker:exportComplete/exportFailed/importComplete (both registries) - fix: stream pipeline in saveImageToTar so the bundle isn't truncated VERIFIED end-to-end on real docker: full export -> 326MB valid bundle -> delete case -> import -> new container runs from the quarantined image with the workspace file AND the in-image change both restored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- index.html: Create Case "Docker" tab (name/workspace/host/image/network + advanced memory/cpus/mountCredentials/resumeOnStart), and a Docker-exports section in the Manage tab - session-ui.js: linkDockerCase (POST docker-host, PUT on conflict, then docker-link; omitted optionals as undefined not null), case-picker label "name @ container" + search fields, switchCaseModalTab/submitCaseModal docker branch, and export/import UI (refresh/export/import/delete). Docker cases route through /api/quick-start like remote (runClaude/runShell/runOpenCode/Codex/Gemini) - verified in a real browser (Playwright): Docker tab renders, linking through the UI creates the case and it appears in the picker as "uitest @ codeman-case-uitest" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… toggles Per-device App Settings > Header Displays toggles that show/hide the session manager and away-digest header buttons (default OFF) and the cron footer button (default ON). Adds the load/save/apply/default/displayKeys wiring in settings-ui.js plus the marker CSS in styles.css. Client-only display keys, stripped from the settings PUT so they never reach the strict server schema (mirrors the showAttachmentsButton pattern); session/away stay hidden on phones via the existing mobile.css rules. The button markup and checkbox rows landed earlier in 5728b86. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found in live testing: claude refuses its default /tmp/claude-<uid> temp dir when that path pre-exists root-owned (happens when the workspace bind-mount traverses it, e.g. a workspace under /tmp/claude-<uid>). Set CLAUDE_CODE_TMPDIR to a nonexistent HOME subpath the running uid creates+owns, so docker claude sessions are robust to any workspace location. Also document the hook-reachability constraint: in-container hooks POST to host.docker.internal (the bridge gateway), so they only fire when Codeman is reachable from the container (bind 0.0.0.0 + password); on a loopback-only bind they don't fire and idle detection falls back to output-based (which works). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New POST /api/cases/docker-quickcreate: creates a normal case (folder in CASES_DIR, scaffolded CLAUDE.md + hooks) AND links it to a hardened container with default settings, auto-provisioning a shared `default` docker host — the user never touches host/image/network fields. - Create New tab gains a "Run in isolated Docker container" checkbox; on submit it calls docker-quickcreate then auto-starts a claude session inside the container. - Case Manage list gains an Export (full-image) button per docker case. - SSE listeners for docker:exportComplete/exportFailed toast + refresh the exports list. Verified end-to-end on the live instance: one-click create put the case in ~/codeman-cases/<name>, auto-created the default host, launched claude in the container; export button produces a bundle; checkbox + button render (Playwright). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tener - One-click "Run in Docker" gains an expandable settings panel with a Template picker (Small 2G/1 · Medium 4G/2 default · Large 8G/4 · GPU 8G/4/all) plus memory/cpu/gpu/network/image/mount-creds overrides. Any tweak creates a dedicated per-case host; the plain checkbox keeps using the shared `default` host. - GPU passthrough: `gpus` on DockerHost/SessionDocker -> `--gpus <value>` in create args (needs the NVIDIA container toolkit). Elastic disk: no `--storage-opt` cap, so container storage grows as data flows in. - CODEMAN_DOCKER_BRIDGE_HOOKS=1: opt-in second listener on the docker bridge gateway (auto-detected 172.17.0.1, override CODEMAN_DOCKER_BRIDGE_HOST) that serves ONLY the hook endpoints and delegates into the secret-gated pipeline, so in-container hooks fire on a loopback-only server. Non-hook paths -> 403; host-internal, not LAN. Verified live: Large template applies real 8GB/4CPU limits; a secret-authenticated hook POST from inside a container now reaches the handler (was connection-refused); non-hook paths return 403; template UI + GPU field verified via Playwright. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 19, 2026
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.
Summary
Adds Docker session mode: a case can run inside an isolated Docker container instead of on the host, with configurable network/resource/credential settings, multiple sessions sharing one per-case container, and export to move a container (toolchain + workspace) to another machine. Bumps to 1.4.0 (minor).
Docker is a location overlay on cases (not a new
SessionMode), a near-1:1 mirror of the existing remote-SSH cases feature: a local tmux pane runsdocker exec -itinto a durable in-container tmux server; the container is scoped to the case (codeman-case-<name>).What's included
src/docker-hosts.ts(storage, pure command builders, probes),buildDockerLaunchCommand/Kill/stop/remove intmux-manager.ts, Session threading + recovery,/api/docker-hostsCRUD +/api/cases/docker-link+/api/quick-startdocker branch,DockerHostSchema/DockerCaseLinkSchema.docker/agent.Dockerfile+scripts/build-agent-image.mjs(built locally; node + claude/codex/gemini/opencode + tmux; secret-free; OpenShift arbitrary-uid HOME).--cap-drop ALL,--security-opt no-new-privileges, non-root (--user <hostUid>:0),--pids-limit,--memory==--memory-swap,--init; never--privileged/socket. Convenient credential default bind-mounts host~/.claudeetc. (never captured bydocker commit); sealed profile opt-in. Host-guard allowlists the container→host gateway aliases.--resume.src/docker-export.ts): full-image (commit+save+workspace tar+manifest) or workspace-only → one portable.codeman-container.tgz; import validates checksums, guards path traversal, quarantine-re-tags the loaded image. Instance-scoped boot reaper.docker:*SSE events.docs/docker-cases.md(guide),docs/docker-cases-plan.md(design).Testing
Full CI suite green (3456 tests), plus extensive end-to-end verification against a real Docker daemon (both isolated instances and the live deployed instance): CRUD, shell + claude launch, multi-session shared container, session-kill-keeps-container, full export → import round-trip (image + workspace + in-image change all restored), and the frontend via Playwright. Two findings handled: a
CLAUDE_CODE_TMPDIRrobustness fix, and a documented hook-reachability constraint (in-container hooks require the server reachable from the container; loopback-only bind falls back to output-based idle detection).Notes for review
8b2c857 feat(settings): wire session/away-digest/cron button visibility toggles(tested, CI green).🤖 Generated with Claude Code