feat(devcontainer): portable multi-container flow with sidecar workspace#18
feat(devcontainer): portable multi-container flow with sidecar workspace#18
Conversation
Adds an opt-in docker-compose flow so any sibling project can be opened inside a fresh agent_harness container, with prompt-nexus auto-attached as a sidecar workspace. The legacy single-container devcontainer.json remains the default for harness-on-harness work and is unchanged. Phase 1 — compose + persistent fixes baked into the image - .devcontainer/docker-compose.yml: harness + prompt-nexus services on ah-net network, named volumes for harness deps preserved, target project + prompt-nexus + ~/.config bind-mounted from host. - .devcontainer/.env.example: host-path template documenting unified ~/.config layout across Linux, macOS, WSL, and Docker Desktop on Windows. Windows alignment via setx + robocopy is one-time per host. - .devcontainer/Dockerfile: /etc/profile.d/ecc-env.sh unsets GITHUB_TOKEN/GH_TOKEN on every interactive shell so gh reads stored auth from the bind-mounted ~/.config/gh, and anchors XDG_CONFIG_HOME to ~/.config. System-level commit.gpgsign=false so containers do not fail commits from a host signing key path that does not resolve in Linux. - .devcontainer/postCreate.sh: gh auth probe + final next-steps banner. Phase 2 — environment validator + runbook - scripts/hooks/session-start.js: validateDevcontainerEnvironment() step gated on ECC_DEVCONTAINER_VALIDATION (set only by the compose environment block, never by the legacy flow). On the compose path, it checks /.dockerenv, target workspace mount, /home/node/.config/gh hosts.yml, commit.gpgsign=false, and XDG_CONFIG_HOME alignment. If any check fails, prepends a banner to additionalContext. Always exits 0. - tests/hooks/session-start-devcontainer.test.js: 4 tests covering no-op behaviour when the gate is off, banner emission on misconfiguration, and stderr logging. - docs/RUNBOOKS/portable-devcontainer.md: usage runbook with one-time Windows ~/.config alignment, per-project bring-up commands, troubleshooting, and rationale. - docs/architecture/adr/0005-portable-multicontainer.md: supersedes the no-compose stance of ADR 0004 specifically for the workspace- sidecar use case. Documents the named-volume invariant and the hard rule that MCP topology stays unchanged (do NOT containerize MCP servers into compose services). MCP topology is intentionally unchanged: MCPs remain stdio children of claude inside the harness service. The sidecar is a workspace artifact, not a protocol peer. ADR 0004's MCP rationale still holds for the protocol layer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Updated `pyproject.toml` for better project metadata organization. - Enhanced `SKILL.md` documentation for the repo-scan skill. - Refactored `llm/__init__.py` to maintain code clarity and structure. - Improved prompt building logic in `prompt/builder.py` for better message handling. - Updated `claude.py` and `openai.py` provider adapters for consistency and error handling. - Added comprehensive tests for prompt builder and message adaptation functionality.
| # Handoff | ||
|
|
||
| - Failed: 2026-05-03T18:11:18Z | ||
| - Branch: `feat/portable-multicontainer-devcontainer` | ||
| - Worktree: `/mnt/c/Users/DavidRevenaugh/AI-Workspace/_projects/harness/agent_harness` | ||
|
|
||
| Task file is missing or unreadable: `C:UsersDAVIDR~1AppDataLocalTempecc-orch-debug-79266aee7dec4d99805788fc20534735.orchestrationdocstask.md` |
There was a problem hiding this comment.
🔴 Accidentally committed Windows temp debug artifacts to repo root
Six files with mangled Windows temp paths (e.g., C:UsersDAVIDR~1AppDataLocalTempecc-orch-debug-*.orchestrationdocshandoff.md) were accidentally committed. These are orchestration debug artifacts generated on the developer's local machine. They contain local worktree paths (/mnt/c/Users/DavidRevenaugh/AI-Workspace/_projects/harness/agent_harness), violating CONTRIBUTING.md's rule "Don't include sensitive data (API keys, tokens, paths)" and AGENTS.md's "Hardcoded paths rejected in CI" guideline. These files serve no purpose in the repository and should be removed and gitignored.
Prompt for agents
Remove all six accidentally committed files matching the pattern C:UsersDAVIDR~1AppDataLocalTempecc-orch-debug-*.orchestrationdocs*.md from the repository. These are local debug artifacts that were never meant to be tracked. Run git rm on each of them and add a gitignore pattern to prevent recurrence, for example adding a line like C:Users* to .gitignore.
Was this helpful? React with 👍 or 👎 to provide feedback.
| { | ||
| "version": "1.0.0", | ||
| "aliases": { | ||
| "__proto__": { | ||
| "sessionPath": "/evil/path", | ||
| "createdAt": "2026-05-03T17:53:45.660Z", | ||
| "title": "Prototype Pollution Attempt" | ||
| }, | ||
| "normal": { | ||
| "sessionPath": "/normal/path", | ||
| "createdAt": "2026-05-03T17:53:45.660Z", | ||
| "title": "Normal Alias" | ||
| } | ||
| }, | ||
| "metadata": { "totalCount": 2, "lastUpdated": "2026-05-03T17:53:45.660Z" } | ||
| } No newline at end of file |
There was a problem hiding this comment.
🔴 Accidentally committed test fixture under undefined/ directory
The file undefined/.claude/session-aliases.json is a test fixture (it contains a __proto__ prototype pollution test entry) that was accidentally committed to the repo root. The undefined directory name strongly suggests a bug in test code where an undefined variable was used as a path component, causing the fixture to be written to a literal undefined/ directory instead of a temp directory. This file should not be tracked in the repository.
Prompt for agents
Remove undefined/.claude/session-aliases.json from the repository (git rm -r undefined/). This is a test fixture that was accidentally written to a literal undefined/ directory, likely because a variable evaluating to undefined was used as a path component in a test. Investigate the test that generates session-aliases.json fixtures (likely in tests/lib/session-manager.test.js or similar) to find and fix the undefined path variable so it does not leak files outside the temp directory. Also add undefined/ to .gitignore as a safety net.
Was this helpful? React with 👍 or 👎 to provide feedback.
| AGENT_HARNESS_HOST=/c/Users/DavidRevenaugh/AI-Workspace/_projects/harness/agent_harness | ||
|
|
||
| # Path to the project you want to work on inside the container. | ||
| # It will be mounted at /workspaces/${HARNESS_TARGET_NAME}. | ||
| HARNESS_TARGET_HOST=/c/Users/DavidRevenaugh/AI-Workspace/ai-top-utility | ||
| HARNESS_TARGET_NAME=ai-top-utility | ||
|
|
||
| # Path to prompt-nexus on the host. Mounted into the prompt-nexus sidecar | ||
| # at /app, and into the harness service at /workspaces/prompt-nexus so you | ||
| # can edit it from either container. | ||
| PROMPT_NEXUS_HOST=/c/Users/DavidRevenaugh/AI-Workspace/_projects/prompt_hub/prompt-nexus | ||
|
|
||
| # ---------------------------------------------------------------------------- | ||
| # Unified XDG config dir (host) — same shape on every OS | ||
| # ---------------------------------------------------------------------------- | ||
| # We bind-mount the whole ~/.config so gh, claude, op, etc. persist their | ||
| # state across container rebuilds AND match what you use on the host. | ||
| # | ||
| # Windows is *aligned* with mac/linux rather than catering to %AppData%. See | ||
| # docs/RUNBOOKS/portable-devcontainer.md "Windows host alignment" for the | ||
| # one-time `setx` + `robocopy` migration that moves an existing gh CLI auth | ||
| # from %AppData%\GitHub CLI to %USERPROFILE%\.config\gh. | ||
| # | ||
| # Linux / macOS: ${HOME}/.config | ||
| # WSL on Windows: /c/Users/<user>/.config | ||
| # Docker Desktop (Win): /c/Users/<user>/.config | ||
| HOST_CONFIG_DIR=/c/Users/DavidRevenaugh/.config |
There was a problem hiding this comment.
🔴 .env.example contains hardcoded personal paths instead of generic placeholders
.devcontainer/.env.example contains real personal filesystem paths like /c/Users/DavidRevenaugh/AI-Workspace/_projects/harness/agent_harness and /c/Users/DavidRevenaugh/.config instead of generic placeholders. CONTRIBUTING.md explicitly states "Don't include sensitive data (API keys, tokens, paths)" and AGENTS.md lists "Hardcoded paths rejected in CI" as a common pitfall. Example files should use placeholders like /path/to/your/agent_harness so users know they need to substitute their own values.
| AGENT_HARNESS_HOST=/c/Users/DavidRevenaugh/AI-Workspace/_projects/harness/agent_harness | |
| # Path to the project you want to work on inside the container. | |
| # It will be mounted at /workspaces/${HARNESS_TARGET_NAME}. | |
| HARNESS_TARGET_HOST=/c/Users/DavidRevenaugh/AI-Workspace/ai-top-utility | |
| HARNESS_TARGET_NAME=ai-top-utility | |
| # Path to prompt-nexus on the host. Mounted into the prompt-nexus sidecar | |
| # at /app, and into the harness service at /workspaces/prompt-nexus so you | |
| # can edit it from either container. | |
| PROMPT_NEXUS_HOST=/c/Users/DavidRevenaugh/AI-Workspace/_projects/prompt_hub/prompt-nexus | |
| # ---------------------------------------------------------------------------- | |
| # Unified XDG config dir (host) — same shape on every OS | |
| # ---------------------------------------------------------------------------- | |
| # We bind-mount the whole ~/.config so gh, claude, op, etc. persist their | |
| # state across container rebuilds AND match what you use on the host. | |
| # | |
| # Windows is *aligned* with mac/linux rather than catering to %AppData%. See | |
| # docs/RUNBOOKS/portable-devcontainer.md "Windows host alignment" for the | |
| # one-time `setx` + `robocopy` migration that moves an existing gh CLI auth | |
| # from %AppData%\GitHub CLI to %USERPROFILE%\.config\gh. | |
| # | |
| # Linux / macOS: ${HOME}/.config | |
| # WSL on Windows: /c/Users/<user>/.config | |
| # Docker Desktop (Win): /c/Users/<user>/.config | |
| HOST_CONFIG_DIR=/c/Users/DavidRevenaugh/.config | |
| AGENT_HARNESS_HOST=/path/to/your/agent_harness | |
| # Path to the project you want to work on inside the container. | |
| # It will be mounted at /workspaces/${HARNESS_TARGET_NAME}. | |
| HARNESS_TARGET_HOST=/path/to/your/target-project | |
| HARNESS_TARGET_NAME=your-target-project | |
| # Path to prompt-nexus on the host. Mounted into the prompt-nexus sidecar | |
| # at /app, and into the harness service at /workspaces/prompt-nexus so you | |
| # can edit it from either container. | |
| PROMPT_NEXUS_HOST=/path/to/your/prompt-nexus | |
| # ---------------------------------------------------------------------------- | |
| # Unified XDG config dir (host) — same shape on every OS | |
| # ---------------------------------------------------------------------------- | |
| # We bind-mount the whole ~/.config so gh, claude, op, etc. persist their | |
| # state across container rebuilds AND match what you use on the host. | |
| # | |
| # Windows is *aligned* with mac/linux rather than catering to %AppData%. See | |
| # docs/RUNBOOKS/portable-devcontainer.md "Windows host alignment" for the | |
| # one-time `setx` + `robocopy` migration that moves an existing gh CLI auth | |
| # from %AppData%\GitHub CLI to %USERPROFILE%\.config\gh. | |
| # | |
| # Linux / macOS: ${HOME}/.config | |
| # WSL on Windows: /c/Users/<user>/.config | |
| # Docker Desktop (Win): /c/Users/<user>/.config | |
| HOST_CONFIG_DIR=/path/to/your/.config |
Was this helpful? React with 👍 or 👎 to provide feedback.
| } catch { | ||
| // git config not set at all — equivalent to default (true). Flag it. | ||
| failures.push('commit.gpgsign not set to false (Dockerfile system config drifted?)'); |
There was a problem hiding this comment.
🟡 Wrong comment and false-positive validation for unset commit.gpgsign
In validateDevcontainerEnvironment(), when git config --get commit.gpgsign throws (key not found at any level), the catch block comment on line 138 says "equivalent to default (true)" and pushes a failure. However, git's actual default for commit.gpgsign is false — when the key is completely absent, git does not sign commits. This means the validator generates a false-positive failure message ('commit.gpgsign not set to false') in environments where the system-level config is absent but signing is already disabled by git's own default. The misleading comment could also confuse future maintainers.
| } catch { | |
| // git config not set at all — equivalent to default (true). Flag it. | |
| failures.push('commit.gpgsign not set to false (Dockerfile system config drifted?)'); | |
| } catch { | |
| // git config key not found at any level — git defaults to false (no signing), | |
| // which is fine. Only flag if we positively need to verify the Dockerfile ran. |
Was this helpful? React with 👍 or 👎 to provide feedback.
| const xdg = process.env.XDG_CONFIG_HOME; | ||
| if (xdg !== '/home/node/.config') { | ||
| failures.push(`XDG_CONFIG_HOME is "${xdg || '<unset>'}" — expected "/home/node/.config" (profile drop did not run?)`); | ||
| } |
There was a problem hiding this comment.
🚩 Profile drop only applies to interactive login shells, not all shell invocations
The /etc/profile.d/ecc-env.sh drop (.devcontainer/Dockerfile:59-61) unsets GITHUB_TOKEN and GH_TOKEN and exports XDG_CONFIG_HOME. However, /etc/profile.d/ scripts are only sourced by login shells (bash -l or bash --login). Non-login shells (e.g., docker compose exec harness bash without -l, or programmatic child_process.exec calls) won't source this file. The validateDevcontainerEnvironment() in scripts/hooks/session-start.js:142-145 checks XDG_CONFIG_HOME expecting it to be /home/node/.config, but the hook runs via node (not a login shell), so it will typically find XDG_CONFIG_HOME unset unless something else sets it. This means the validator will almost always report a false positive for XDG_CONFIG_HOME unless the compose environment: block or env_file: also sets it. The compose file at .devcontainer/docker-compose.yml:34-35 includes env_file: .env but the .env.example does not include XDG_CONFIG_HOME. The troubleshooting docs at docs/RUNBOOKS/portable-devcontainer.md:130-145 partially address this for gh auth but not for XDG_CONFIG_HOME.
Was this helpful? React with 👍 or 👎 to provide feedback.
| | 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** | | ||
| | 技能 | PASS: 190 项 | PASS: 37 项 | **Claude Code 领先** | |
There was a problem hiding this comment.
📝 Info: zh-CN README command/skill count updates are internally consistent
The PR changes the command count from 69→68 and skills count from 189→190 in docs/zh-CN/README.md at two locations (lines 1136-1137 and 1244-1245). The root AGENTS.md declares "190 skills, 68 commands" in its header, and docs/zh-CN/AGENTS.md also says "190 项技能、68 条命令". So these count changes are a sync correction to make the zh-CN README consistent with AGENTS.md. This is correct and not a bug.
Was this helpful? React with 👍 or 👎 to provide feedback.
| env_file: | ||
| - .env |
There was a problem hiding this comment.
📝 Info: Compose env_file path is relative to compose file, not project root
In .devcontainer/docker-compose.yml:34-35, env_file: .env is specified. Docker Compose resolves env_file paths relative to the compose file's directory (.devcontainer/), so this correctly references .devcontainer/.env. This is not a bug, but worth noting because it's a common source of confusion — if someone moves the compose file, the env_file reference would break.
Was this helpful? React with 👍 or 👎 to provide feedback.
| function sleepSync(ms) { | ||
| Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); | ||
| } |
There was a problem hiding this comment.
📝 Info: Test uses Atomics.wait with SharedArrayBuffer for synchronous sleep
The sleepSync function in tests/scripts/orchestrate-codex-worker.test.js:29-31 uses Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) for a synchronous sleep. This works in Node.js but requires --enable-source-maps or similar flags in some configurations, and SharedArrayBuffer requires specific security headers in browser contexts (though this is Node.js only). The approach is valid for test code — it's a well-known pattern for blocking the main thread in Node.js without busy-waiting. The retry logic in removeTempRoot (lines 33-47) is a reasonable fix for Windows file-locking issues during temp directory cleanup.
Was this helpful? React with 👍 or 👎 to provide feedback.
| - ${AGENT_HARNESS_HOST}:/workspaces/agent_harness:cached | ||
| - ${HARNESS_TARGET_HOST}:/workspaces/${HARNESS_TARGET_NAME}:cached | ||
| - ${PROMPT_NEXUS_HOST}:/workspaces/prompt-nexus:cached | ||
| - ${HOST_CONFIG_DIR}:/home/node/.config |
There was a problem hiding this comment.
🚩 HOST_CONFIG_DIR bind-mount gives container full write access to host's ~/.config
The compose volume mount ${HOST_CONFIG_DIR}:/home/node/.config (.devcontainer/docker-compose.yml:29) gives the container read-write access to the entire ~/.config directory on the host. This is intentional (for gh auth persistence), but it means any process in the container can read/modify all XDG config data on the host — not just gh/. A misconfigured or malicious MCP server running inside the container could access other tool configurations. The ADR acknowledges this is for persistence but doesn't discuss the blast radius. Consider using :ro (read-only) or mounting only specific subdirectories like gh/ if full write access isn't needed for all tools.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58e6f1a7a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,7 @@ | |||
| # Handoff | |||
There was a problem hiding this comment.
Remove invalid Windows-path debug files from repository
This commit adds multiple generated files whose names start with C:Users..., which include : characters that cannot exist in Windows filenames. On Windows checkouts, Git cannot materialize these paths, so the commit becomes unusable for contributors/CI on a supported platform. These should be removed from version control (and ignored) so cross-platform clones/checkouts keep working.
Useful? React with 👍 / 👎.
Adds an opt-in docker-compose flow so any sibling project can be opened inside a fresh agent_harness container, with prompt-nexus auto-attached as a sidecar workspace. The legacy single-container devcontainer.json remains the default for harness-on-harness work and is unchanged.
Phase 1 — compose + persistent fixes baked into the image
Phase 2 — environment validator + runbook
MCP topology is intentionally unchanged: MCPs remain stdio children of claude inside the harness service. The sidecar is a workspace artifact, not a protocol peer. ADR 0004's MCP rationale still holds for the protocol layer.
What Changed
Why This Change
Testing Done
node tests/run-all.js)Type of Change
fix:Bug fixfeat:New featurerefactor:Code refactoringdocs:Documentationtest:Testschore:Maintenance/toolingci:CI/CD changesSecurity & Quality Checklist
Documentation