Skip to content

feat: extract the cage; add codex-box as a second payload - #11

Merged
alechill merged 8 commits into
mainfrom
feat/cage-extraction-codex-payload
Aug 9, 2026
Merged

alechill merged 8 commits into
mainfrom
feat/cage-extraction-codex-payload

Conversation

@alechill

@alechill alechill commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

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.sh CLAUDE_BOX_EXEC); this promotes it to the default and makes the harness one configured payload.

New files

  • Dockerfile.cage — payload-free cage-base image: 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); --init on 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 the docker run assembly. 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 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 Codex self-refreshes).

Verified

On a rootless host: cage-base and codex-box build; 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 none and real auth were validated on the author's macOS host, not in CI.

Scope

claude-box is unchanged in this PR. Follow-ups:

  • Phase B — migrate claude-box onto cage-base + libcage.sh, deleting the monolith's copy of the engine block.
  • Phase C — a cage acceptance runbook (docs/cage-engine-acceptance.md, which also fixes the dangling README.md link) + README updates.

Also untracks docs/superpowers/ (local planning/spec scratch, now gitignored).

🤖 Generated with Claude Code

alechill and others added 8 commits August 9, 2026 12:58
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>
@alechill
alechill merged commit f9048a6 into main Aug 9, 2026
2 checks passed
@alechill
alechill deleted the feat/cage-extraction-codex-payload branch August 9, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant