fix(jail): seed writable codex/claude homes and acknowledge receipts on failed turns - #154
Merged
Merged
Conversation
…on failed turns A jailed codex or claude lead died at spawn: the jail bind-mounted the CLI home read-only, and both CLIs must write inside their home to start. The measured failure is 'codex exited 1: ... Read-only file system (os error 30)' on discovery-lab run strong-lead-smoke-a. The thrown error also dropped the computed materialization receipt from the stream. The runtime then reported 'completed without cli-bridge.profile-materialization.v2 (classified transient)' and retried a deterministic failure three times. Changes: - Add the seed-writable auth-source mode. The jail copies a small selected file set into a stable writable path under the jail root, refreshes it on each run, and retains it so a later turn can resume the CLI session. - Seed codex with auth.json and config.toml, redirect CODEX_HOME at the seed. This also stops exposing the host sessions tree inside the jail. - Seed claude with credentials, settings, and .claude.json at the exact HOME-relative paths the jailed CLI reads. - Dereference seed sources. A dotfiles-managed symlink target is not mounted inside an fs-jail, so the seed must carry real bytes. - Acknowledge the profile receipt on its own buffered event before an error or timeout terminal, in the route wrap and in Run.pump. The SSE writer renders an error terminal as a bare error frame, so a receipt folded into that delta never reached the caller. - Split the canonical wire id harness/provider/model for the codex and claude CLIs. Codex takes model_provider and model config separately; claude takes the bare model id. The qualified form was rejected by the API as a nonexistent model. Verified end to end on a local fs-jailed bridge: a claude lead completes and resumes across two turns with receipts on both, and a codex lead now reaches the API and returns its real account error with the receipt.
This was referenced Aug 12, 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.
Problem
discovery-lab run
strong-lead-smoke-a(profile harnesscodex, provideropenai) died after 3 driver attempts, ~7s, zero tokens, with:The bridge traces show the real failure, three times:
Three defects stacked:
~/.codex(and~/.claude+~/.claude.json) read-only. Codex must write PATH aliases, app-server state, and session rollouts insideCODEX_HOMEbefore it can run; claude rewrites.claude.jsonand persists transcripts under.claude/projects. Every jailed codex/claude lead died at spawn. Pi survived because its auth source iscopy-writable.{error}frame, so a receipt folded into that delta never reaches the caller.harness/provider/model. Codex received-c model="openai/..."and the API rejected it as a nonexistent model; claude received--model anthropic/....Fix
seed-writablejail auth-source mode: copy a small selected file set (only) into a stable writable path under the jail root, refresh per run, retain across turns soresumefinds the CLI's own session state. Codex seedsauth.json+config.tomlwithCODEX_HOMEredirected; claude seeds credentials,settings.json, and.claude.jsonat the exact HOME-relative paths. Seeds are dereferenced — a dotfiles-managed symlink target is not mounted inside an fs-jail. Side benefit: the host's 130GB~/.codex/sessionstree is no longer readable inside the jail.Run.pump(thrown failures). The runtime now sees the receipt and surfaces the real upstream error.-c model_provider=... -c model=...; claude stripsanthropic/. Unknown providers pass through and fail loud.Proof
Replayed the runtime's exact request shape against a local fs-jailed bridge built from this branch (
WORKER_FS_JAIL=1, same env as the serving bridge):codex exited 1: ... Read-only file system (os error 30), no receipt on the stream — byte-identical to the smoke failure.codex/openai/gpt-5.6-sol): boots in the jail, model accepted, receipt event precedes the terminal; the only remaining error is the real account condition ("You've hit your usage limit ... try again at Aug 18th"), now delivered verbatim to the caller instead of the masked transport fault.claude-code/anthropic/sonnet): turn 1"OK", turn 2"quokka"— a two-turn resume through the jailed CLI with the receipt on both turns and real usage reported.pnpm buildclean;pnpm test: 802 passed, 8 skipped, 43 files.Deploy notes (no restart performed)
claudefrom PATH to~/bin/claude, a wrapper that execs$HOME/.local/bin/claude— under the jail's HOME redirect that dereference breaks, and the fs-jail derives read binds from the wrapper's location, not the real install. SetCLAUDE_BIN=/home/drew/.local/bin/claudeon the serving bridge when deploying this.src/runtime/supervise/runtime.ts,streamDurableBridgeRun): ondone, the missing-receipt check throws beforependingUpstreamError, so a materialization-failure error can still be masked. Throw the pending upstream error first.🤖 Generated with Claude Code