feat: extract the cage; add codex-box as a second payload - #11
Merged
Merged
Conversation
Split the host-isolation and nested-engine machinery (the cage) out of the Claude-specific payload so another harness can reuse it, and add codex-box as the first proof. - Dockerfile.cage: payload-free cage-base image (engine + generic tooling, no harness; no claude, no ugrep grep-shadow, no bun). - entrypoint-cage.sh: promotes the generic-exec seam to the default, adds a payload-init hook and a cage-neutral host->box relay; --init on all postures. - libcage.sh: the engine block once (posture resolution, userns ladder, apparmor profile, device probes, ssh/colima relays, image build, exit-status contract, docker run assembly), driven by per-wrapper config + hooks. bash-3.2-safe. - codex-box + Dockerfile.codex: Codex CLI on cage-base, interactive with --dangerously-bypass-approvals-and-sandbox (the cage is the sandbox). Persists ~/.codex login; lifts Linear and other MCP OAuth logins out of the macOS Keychain into file-mode .credentials.json, read once then self-refreshed. claude-box is unchanged; Phase B migrates it onto the shared cage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These superpowers design/plan docs are working scratch, not part of the shipped tool. Ignore the directory and untrack the two files a prior PR had committed (kept on disk via --cached). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
box_stage rsynced all of host ~/.codex into the state dir, including
logs_2.sqlite, which a running host Codex keeps open in WAL mode. Copying it
in-flight produced a torn file ("file is not a database", code 26), so the box
rebuilt its database on nearly every startup. Exclude *.sqlite* and db-backups
from the seed: the box keeps its own runtime DB in the state dir across runs and
only takes auth.json / config.toml / profiles from the host.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/entrypoint Move claude-box off the monolithic Dockerfile/entrypoint.sh onto the extracted cage: Dockerfile.claude layers the Claude payload over the cage base, and payload-init-claude.sh carries the Claude-specific init that the old entrypoint.sh held. Update CI to bash -n the current script set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add docs/cage-engine-acceptance.md (the runbook README already linked to but that did not exist): proves the extraction — engine block exists once, cage base carries no harness, each payload composes the cage, the bounded nested engine runs in-box, host socket is refused, and the exec/exit contract holds per payload. All checks use the generic exec hook, so no harness auth needed. README: add an "Architecture: the cage" section (cage vs payload, the file map, codex-box as the second payload) and correct the two stale "rebuilds when the Dockerfile changes" notes to the two-stage cage-base + payload build. runbook-headless-exit.md: fix monolith-era references — entrypoint.sh -> libcage.sh + entrypoint-cage.sh, bare Dockerfile -> Dockerfile.claude, [entrypoint] log prefix -> [cage], and retarget the CAGE_DEBUG dump check (the cage lists the state mount and no longer cats credential contents). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ne none)
Two jobs. `static`: bash -n the script set, then assert the extraction
boundary without docker — engine_args is built only in libcage.sh, both
wrappers source the cage, and both payload Dockerfiles are FROM cage-base.
`acceptance`: build cage-base + both payload images through the real
launchers, then verify with the nested engine disabled (--engine none, the
one part hosted runners can't reliably provide):
- image boundary: cage-base carries the shared tooling but no harness;
claude-box has claude and not codex, codex-box the reverse
- exit-status contract: 0 / 7 / 130 propagate verbatim through each
wrapper, and stdin round-trips
- host-socket refusal: a mounted /var/run/docker.sock is refused before
the harness runs, for both payloads
The live nested-engine smoke (rootless dockerd running a container in-box)
is intentionally left out: it needs nested userns the hosted runners don't
reliably grant, so it stays a manual step in docs/cage-engine-acceptance.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
actions/checkout@v4 -> v5 (clears the Node 20 deprecation warning). Gate the workflow to pull_request events plus pushes to main, so the image-building acceptance job no longer runs on every feature-branch push. Feature branches are still covered: an open PR triggers on synchronize. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the acceptance job with the headless-exit runbook checks that don't
need the nested engine:
- exit-status matrix now also covers SIGTERM->143, SIGKILL->137, and an
in-box timeout->124 (on top of 0/7/130)
- launcher fault: a create-time failure yields exit 126 and a
fault=engine-start-failed stderr line
- stream cleanliness: redirected stdout is exactly the payload output,
no CR, no [cage]/[claude-box] log lines, no stderr leak
- debug dump is opt-in via CLAUDE_BOX_DEBUG, off by default
- invalid --name is rejected before anything starts; a headless box is
addressable via --name-file (publish, docker exec, docker stop, cleanup)
Left out on purpose: image-missing (125) needs a full failed build and
wipes the image cache, and the SIGINT-teardown check is signal-timing
flaky. Both stay manual in docs/runbook-headless-exit.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
What
Splits the host-isolation + nested-engine machinery (the cage) out of the Claude-specific payload so a second harness can reuse it, and adds codex-box as the first proof that the boundary is drawn in the right place.
The generic-exec seam already existed as a debug hook (
entrypoint.shCLAUDE_BOX_EXEC); this promotes it to the default and makes the harness one configured payload.New files
Dockerfile.cage— payload-freecage-baseimage: engine (rootless/rootful) + generic dev/deploy tooling, no harness. Deliberately excludes claude-code, the ugrep grep-shadow, and bun (all Claude-specific, moving to the Claude layer in Phase B).entrypoint-cage.sh— generic exec of whatever command the launcher supplies; adds a payload-init hook and a cage-neutral host→box relay (.cage-relay/, not~/.claude);--initon all four engine postures.libcage.sh— the engine block, once: posture resolution, the userns probe ladder, the apparmor profile, device probes, ssh/colima relays, image build, the exit-status contract, and thedocker runassembly. Driven by per-wrapper config vars + hooks (box_stage,box_sync_back,box_parse_arg). bash-3.2-safe (macOS/bin/bash).codex-box+Dockerfile.codex— Codex CLI oncage-base, interactive with--dangerously-bypass-approvals-and-sandbox(the cage is the sandbox). Persists~/.codexlogin; lifts Linear and other MCP OAuth logins out of the macOS Keychain into file-mode.credentials.json(read once, then Codex self-refreshes).Verified
On a rootless host:
cage-baseandcodex-boxbuild; the full exit-status contract propagates through the cage (0 / 7 / 42 / 130 / 143); user mapping, project mount, and the AGENTS.md relay all work; the real Codex payload runs interactively with Linear MCP logged in.Nested-engine postures beyond
--engine noneand real auth were validated on the author's macOS host, not in CI.Scope
claude-boxis unchanged in this PR. Follow-ups:claude-boxontocage-base+libcage.sh, deleting the monolith's copy of the engine block.docs/cage-engine-acceptance.md, which also fixes the danglingREADME.mdlink) + README updates.Also untracks
docs/superpowers/(local planning/spec scratch, now gitignored).🤖 Generated with Claude Code