You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Closes#64. Lane F item: move Ask onto lane D's invocation contract. This ends the interim R1 exception that #52 recorded. Base: main (retargeted after #52 merged).
Summary
Ask no longer runs the claude or codex CLI on the host. It runs in lane D's container, in the read-only questions phase.
Before
After
Where the agent runs
Host process, temporary folder, your environment
Lane D container: read-only root, no capabilities, bounded storage
Code it can see
Only the context in the prompt
That context, plus a read-only clone of the reviewed snapshot head at /work
Commands
Turned off by CLI flags
questions phase: read, list and search only, enforced by lane D's policy
Network
Host network
Vendor-only proxy
Sign-in
Host CLI login or keychain
CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token) or Codex auth.json, never stored
How it works
runner/question-container.ts builds the image once per worker and clones the reviewed head. It allocates storage and captures the invocation (no approved argv), then starts the adapter. Storage is removed only after settled.
runner/question-worker.ts: lane D's entry points are synchronous (Docker and Git calls), so one worker thread owns them and the review server stays responsive.
runner/question-agent.ts (QuestionWorker) settles a question only when the worker reports that its container and storage are gone.
There is no host fallback. A missing token, missing auth.json or missing Docker fails with a message that names what is missing.
Fail-closed ownership stopgaps (added during review). Lane D does not yet label resources with their owner, bound its clone, or recover leftovers after a restart (#51 items 1, 3 and 4). Until it does, Ask owns and records its own leftovers in runner/question-leftovers.ts, next to the review database:
Result identity: the invocation reuses the persisted answer attempt, and referencedCodeHash is the note's contextId. Results with another attempt or context, a missing exit code or any signal are rejected.
Durable cleanup:
Storage whose removal Docker did not confirm is retained and retried, then recorded at shutdown.
Each worker gets one Ask root (<tmp>/codeboost-ask-*), used as its TMPDIR and recorded before the worker starts. It holds the clone, lane D's input and the Codex auth copy. Each root carries an owner stamp, so it can be found without the record.
A crash, a missing release report, or a question that does not settle 30 s after its deadline abandons the worker once. Waiters are released only after the thread stops (bounded), and unknown leftovers are recorded.
Admission:
The first question of each process scans Docker for every lane D label (io.codeboost.allocation, io.codeboost.invocation, io.codeboost.egress). The scan runs single-flight.
Ask stays off while recorded or unidentified leftovers remain, and the error names removal commands.
Shutdown stops question admission in the same turn as the server's stopping flag.
Exclusion: one process per review database, via an exclusive SQLite (OS) lock keyed by the database file's device and inode. The record is keyed by the canonical path. Hard-linked databases are refused.
Isolation:
The worker's environment is an allowlist (PATH, DOCKER_HOST, TMPDIR); credentials reach it as workerData.
The Git size check before cloning uses lane D's hardened invocation and refuses repositories larger than the 512 MiB allocation.
Record entries and owner stamps are validated by exact location, and the record is written through an exclusive random temporary file.
Behavior changes for users
Ask, including npm run demo, needs Docker plus a token or auth.json. The first question builds the agent image. If that exceeds the two-minute question deadline, the question fails, and a retry reuses the finished layers.
A repository larger than the 512 MiB storage allocation is refused before anything is copied.
After a crash or kill, Ask may stay off until labelled leftovers are removed. The error message gives the commands.
Scope decision for the reviewer
About 400 lines of the stopgaps above exist only because lane D lacks owner labels, a bounded clone and scoped recovery (#51). The per-review lock overlaps with F1d's single-runner lock (#59). The options are: merge now and replace these parts when #51 and F1d land (tracked in #65), or hold this PR for #51.
Ownership and coordination
This PR edits files owned by the integration owner: runner/questions.ts, web/server.ts, web/public/app.js, web/cli.ts, .github/workflows/ci.yml and .github/workflows/agent-isolation.yml. It needs that owner's review.
Validated head: 64ac028 (current main merged in at 63a11cf).
npm run typecheck: passed.
Unit tests with CI's excludes: 486/486 passed (18 files).
npm run test:browser: 58/58 passed (on 88bb96f; later commits touch only Ask's leftover module, its tests and docs).
CI (test, real-docker) on 64ac028 is the final exact-head check; on the earlier head 6f047ac both passed.
test/agent-question.test.ts (real Docker) ran in CI's real-docker job. The live Claude case has not run; it needs a real CLAUDE_CODE_OAUTH_TOKEN and CODEBOOST_RUN_AUTH_PROBES=1.
Each review fix has a regression test. For each, I broke the fix on purpose and confirmed the test failed before restoring it.
Review rounds
About 20 Copilot rounds produced 60 inline findings. All 60 threads are resolved; the reply in each thread cites the fixing commit and its regression test. Rounds that reviewed an older head re-reported findings that were already fixed; those threads cite the earlier fix. As AGENTS.md requires, summary-only concerns without a location were reproduced where possible, or filed as follow-ups: #64 (stop reason passed as message text; Copilot later pinpointed it, and this PR now fixes and closes it) and #65 (shared-daemon isolation; open, depends on #51 item 3). One finding did not reproduce (startup marker, 4112551858). A regression for the reported case is included.
Review-lesson audit
Every finding is classified below. Rule citations refer to AGENTS.md in this head.
Covered by existing rules
"Never apply a background response without proving it is still current": result identity and context (4110696969).
"Validate coupled lifecycle fields as allowed combinations": null exit code or signal accepted as success (4110993674).
"Do not release a concurrency slot when cancellation is requested…" and "A deadline must abort and await the underlying operation before releasing its in-flight ownership": abandon before termination, the timer race, concurrent abandon, close during abandon, failed terminate treated as stopped (4112271725, 4112295157, 4112361784, 4112456414, 4112728747).
"Bound the HTTP drain during shutdown…" and "Track an in-flight irreversible subprocess as part of server shutdown…": unbounded D settlement, shutdown deadlock, unbounded release-timeout terminate, scan outliving close(), Docker children outliving an abandoned thread (the lock is now kept until process exit), pending questions released when the termination wait expires (answered: the durable handoff is in place; no code change) (4112245838, 4112245880, 4112311246, 4112484482, 4113923388, 4113971096).
"Begin shutdown by rejecting new work … then cancel and await jobs, then close storage": a failed Ask cleanup skipped closing the store (4113923423).
"Begin shutdown by rejecting new work at the outer admission boundary" and "Set the shutdown admission flag before snapshotting active work…": admission after shutdown began (4112645321).
"Batch and briefly cache read-only status probes, and give the combined operation an overall deadline": unbounded per-resource cleanup gate (4112207416).
"A bounded safety scan must fail closed when its limit is exceeded. Never truncate evidence": record truncation, root overflow (4112207449, 4112376096).
"When startup acquires … resource before later dependency construction, close that resource on every construction failure": worker construction failure (4112376078).
Captured by new rules in this head (AGENTS.md, "Owned host and Docker resources")
Durable cleanup ownership: worker crash, lost removal handles, shutdown discarding retained storage, crash treated as clean release, setup failure without a handle, abandoned staging paths, the Codex auth copy, missing record at startup, failed final record write, lock kept after failed write, rename losing the record, unnamed labelled resources, the seeder missed by labels (4110696991, 4110697011, 4112180878, 4112180891, 4112207386, 4112207433, 4112219113, 4112245818, 4112245859, 4112282859, 4112282891, 4112376104, 4112519512, 4112645299, 4112708625).
Allowlisted environments and hardened Git: credential leakage to setup subprocesses, the heuristic scrub, an unhardened Git size check (4112271713, 4112334205, 4112334222, 4112551830).
Untrusted paths and exclusive writes (extended in this head to forbid following links and to require a private directory for plantable files): basename-only path validation, an untrusted owner stamp opened as a file, a predictable temporary file, a symlinked lock file in the shared temp directory, deleting unauthenticated lookalike folders, a linked owner stamp, a linked record file (4112282910, 4112551842, 4112609105, 4113923407, 4113933100, 4113971119, 4113971130).
Cross-process OS locks and single-flight startup: no admission lock, unserialized record writes, reclaiming a live root, PID reuse, stale-lock takeover race, a path-keyed lock, a lock not surviving a rename, the double startup scan (4112311217, 4112311270, 4112323715, 4112345944, 4112345961, 4112361825, 4112484508, 4112432968).
Summary-only findings with a location (no inline thread)
4112551858: did not reproduce; a regression for the reported case is included.
4113992060, worker started with the full environment: did not reproduce. workerEnvironment() allowlists PATH, DOCKER_HOST and TMPDIR, and the existing integration test covers it. The suggested this.env is the credential source and would leak the token.
Record where the code differs from the approved plan and update stale status:
- Record Ask as an interim exception to R1: it runs the vendor CLI on the
host with tools off until lane F moves it into the lane D container.
- Amend D20: there is no "Merge anyway"; to override a blocker, merge on
GitHub. Matches docs/implementation/guarded-merge.md.
- Tick T1, T2, T4, T5, T10, T13, T14 with test evidence; point Files lines
at core/linking.ts and core/approvals.ts instead of never-created modules.
- Mark increment 1 merged; add a lane status table (C, D, K done; E, F, H
progress); record decided open questions (issue ranking, AgentDiff).
- Add a verified status note for design tasks DT2-DT15; none newly ticked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guarded merge gate doc still said merge-queue branches stay blocked.
#46 (closing #24) added queue lifecycle support. Point to merge-queue.md,
and state that adapters without queue inspection still fail closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ask used to run the claude/codex CLI on the host with each CLI's own
restrictions, an interim exception to R1. It now uses lane D's invocation
boundary in the read-only "questions" phase: a clone of the reviewed
snapshot head at /work, no commands, vendor-only network, and no other
host files. There is no host fallback.
- runner/question-container.ts: build image, clone, allocate bounded
storage, capture, start the Claude/Codex adapter; release storage only
after the invocation settles. Deps are injectable for unit tests.
- runner/question-worker.ts: lane D setup is synchronous, so a worker
thread owns it and the review server stays responsive.
- runner/question-agent.ts: QuestionWorker bridge; a question settles only
when the worker reports the container and storage are gone.
- Credentials come from the environment only: CLAUDE_CODE_OAUTH_TOKEN for
Claude, CODEBOOST_CODEX_AUTH_FILE or CODEX_HOME/auth.json for Codex.
- Provider failures include the vendor's short message (e.g. a 401).
- test/agent-question.test.ts runs the path on real Docker (Agent
isolation workflow); its live case needs the auth-probe credentials.
- Plan, README, Settings copy and implementation docs updated; the R1
exception is closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge main. Record the F1 lifecycle contract (#49) and open F1a-F1c
(#53, #56, #57); record the ranked Issues screen (H4a, #55) with H4b's
trust action remaining; Issues is now a menu link, not a placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README no longer says merge queues block merging; it describes the
enqueue-then-confirm behaviour. The plan's wave-3 note records the old
block as history instead of a live instruction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reuse the persisted answer attempt as the invocation attempt, and the
note's contextId as referencedCodeHash. Accept a result only when its
attempt and context match the captured invocation and the worker reply
carries the same attempt.
- Treat a missing exit code or any signal as a failure, not an answer.
- Keep task storage whose removal Docker did not confirm, retry removal
before the next question, and refuse Ask while any remains.
- After a worker crash, fail closed instead of starting a replacement:
its containers and storage may still exist, and reclaiming them needs
lane D's scoped recovery (#51 item 4).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a failed release-report write the worker thread has stopped and
the root is already recorded, so let go of the root in this process:
it stays on disk and in the record for the next check, and close() can
release the per-review lock instead of holding it for the process.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rejected terminate() no longer counts as a stopped thread: the Ask
root stays on disk and in the record, and the lock stays held, instead
of being removed while the worker may still be alive. Later cleanup runs
only after a termination that actually settled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves README and plan conflicts: keeps this branch's text wherever
main still describes Ask as a host-CLI exception, and takes main's
merge-queue retry wording and planned-vs-current install requirements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four rules for owned host and Docker resources: durable cleanup
ownership, allowlisted subprocess environments and credential channels,
untrusted record and on-disk paths, and cross-process OS locks keyed by
stable identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transport typed stop reasons instead of inferring them from messages
runner/question-container.ts:209
The D StopReason is reconstructed from user-facing error-message text (and the worker bridge transports only that text). A wording change, or an unrelated message containing timed out or Server stopped, can turn a timeout into shutdown/cancellation or vice versa, losing the lifecycle reason required by the invocation contract. Carry a typed reason separately from the display message through the worker boundary.
Lane-D resource scan is not scoped to this review
runner/question-leftovers.ts:293
This scan treats every resource with a lane-D label as belonging to this review and marks any unlisted resource as a blocker. Since the current labels have no runner identity, a different codeboost review sharing the Docker daemon makes this review refuse all Ask until the other review's resources disappear. Scope the scan by a runner-owner label/recovery API before removing this stopgap, or keep the feature gated on that prerequisite.
… abandon
- web/server.ts closes the review store in a finally block when Ask's
cleanup fails, and the CLI exits non-zero instead of hanging.
- Ask lock files live in a private per-user directory under the temp
directory (mode 0700, checked ownership); a lock path that is a
symlink or not a plain file is refused, never opened.
- After any abandonment the review lock is kept until the process
exits: Docker CLI children the terminated thread started can outlive
it and cannot be awaited until lane D exposes process groups (#51).
- AGENTS.md: the untrusted-path rule also forbids following links and
requires a private directory for plantable files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pass typed lane-D stop reason to cancellation handler
runner/question-container.ts:209
The lane-D StopReason is still reconstructed from user-facing error wording here. Any wording change, localization, or non-Error abort reason silently maps timeout/shutdown to cancelled, so the adapter and persisted answer lose the original lifecycle reason. Carry a typed reason through the worker request and pass it directly to handle.cancel(); keep the message only for display.
…alone
- Closes#64: the stop reason (timeout, shutdown, cancelled) now travels
as a StopError value from Questions through the worker message to
handle.cancel(), instead of being rebuilt from message wording.
- The orphan-root scan deletes only folders this user owns that carry a
valid createAskRoot stamp naming a lock in the private lock directory
whose owner is gone; unstamped, tampered or foreign folders stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The leftover record and each folder's .owner stamp are now read through
O_NOFOLLOW and accepted only as regular, single-link files within a size
limit. A linked record makes the ledger unreadable (Ask fails closed and
never acts on the record it points to); a linked stamp leaves the folder
alone.
Co-Authored-By: Claude Opus 5.5 <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
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.
Closes #64. Lane F item: move Ask onto lane D's invocation contract. This ends the interim R1 exception that #52 recorded. Base:
main(retargeted after #52 merged).Summary
Ask no longer runs the
claudeorcodexCLI on the host. It runs in lane D's container, in the read-onlyquestionsphase./workquestionsphase: read, list and search only, enforced by lane D's policyCLAUDE_CODE_OAUTH_TOKEN(fromclaude setup-token) or Codexauth.json, never storedHow it works
runner/question-container.tsbuilds the image once per worker and clones the reviewed head. It allocates storage and captures the invocation (no approved argv), then starts the adapter. Storage is removed only aftersettled.runner/question-worker.ts: lane D's entry points are synchronous (Docker and Git calls), so one worker thread owns them and the review server stays responsive.runner/question-agent.ts(QuestionWorker) settles a question only when the worker reports that its container and storage are gone.auth.jsonor missing Docker fails with a message that names what is missing.Fail-closed ownership stopgaps (added during review). Lane D does not yet label resources with their owner, bound its clone, or recover leftovers after a restart (#51 items 1, 3 and 4). Until it does, Ask owns and records its own leftovers in
runner/question-leftovers.ts, next to the review database:referencedCodeHashis the note'scontextId. Results with another attempt or context, a missing exit code or any signal are rejected.<tmp>/codeboost-ask-*), used as itsTMPDIRand recorded before the worker starts. It holds the clone, lane D's input and the Codex auth copy. Each root carries an owner stamp, so it can be found without the record.io.codeboost.allocation,io.codeboost.invocation,io.codeboost.egress). The scan runs single-flight.stoppingflag.PATH,DOCKER_HOST,TMPDIR); credentials reach it asworkerData.Behavior changes for users
npm run demo, needs Docker plus a token orauth.json. The first question builds the agent image. If that exceeds the two-minute question deadline, the question fails, and a retry reuses the finished layers.Scope decision for the reviewer
About 400 lines of the stopgaps above exist only because lane D lacks owner labels, a bounded clone and scoped recovery (#51). The per-review lock overlaps with F1d's single-runner lock (#59). The options are: merge now and replace these parts when #51 and F1d land (tracked in #65), or hold this PR for #51.
Ownership and coordination
runner/questions.ts,web/server.ts,web/public/app.js,web/cli.ts,.github/workflows/ci.ymland.github/workflows/agent-isolation.yml. It needs that owner's review.runner/questions.tsand the server's shutdown order. Whichever of Run Ask in the lane D agent container #54 and F1c: shutdown wiring and /api/runner #57 lands second must fitQuestions.stopAdmission()→ drain →Questions.close()→QuestionWorker.close()into F1c's shutdown sequence.Validation
Validated head:
64ac028(currentmainmerged in at63a11cf).npm run typecheck: passed.npm run test:browser: 58/58 passed (on88bb96f; later commits touch only Ask's leftover module, its tests and docs).test,real-docker) on64ac028is the final exact-head check; on the earlier head6f047acboth passed.test/agent-question.test.ts(real Docker) ran in CI'sreal-dockerjob. The live Claude case has not run; it needs a realCLAUDE_CODE_OAUTH_TOKENandCODEBOOST_RUN_AUTH_PROBES=1.Review rounds
About 20 Copilot rounds produced 60 inline findings. All 60 threads are resolved; the reply in each thread cites the fixing commit and its regression test. Rounds that reviewed an older head re-reported findings that were already fixed; those threads cite the earlier fix. As AGENTS.md requires, summary-only concerns without a location were reproduced where possible, or filed as follow-ups: #64 (stop reason passed as message text; Copilot later pinpointed it, and this PR now fixes and closes it) and #65 (shared-daemon isolation; open, depends on #51 item 3). One finding did not reproduce (startup marker,
4112551858). A regression for the reported case is included.Review-lesson audit
Every finding is classified below. Rule citations refer to AGENTS.md in this head.
Covered by existing rules
4110696969).4110993674).4112271725,4112295157,4112361784,4112456414,4112728747).close(), Docker children outliving an abandoned thread (the lock is now kept until process exit), pending questions released when the termination wait expires (answered: the durable handoff is in place; no code change) (4112245838,4112245880,4112311246,4112484482,4113923388,4113971096).4113923423).4112645321).4112207416).4112207449,4112376096).4112376078).Captured by new rules in this head (AGENTS.md, "Owned host and Docker resources")
4110696991,4110697011,4112180878,4112180891,4112207386,4112207433,4112219113,4112245818,4112245859,4112282859,4112282891,4112376104,4112519512,4112645299,4112708625).4112271713,4112334205,4112334222,4112551830).4112282910,4112551842,4112609105,4113923407,4113933100,4113971119,4113971130).4112311217,4112311270,4112323715,4112345944,4112345961,4112361825,4112484508,4112432968).Summary-only findings with a location (no inline thread)
question-container.ts): covered by the existing rule "Preserve the original timeout, cancellation, and shutdown reason through every layer". Fixed in this PR; closes Ask: pass the stop reason as a typed value across the worker boundary #64.One-off (recorded why)
4110993691,referencedCodeHashhashed the prompt: a wrong field choice, specific to this contract.4112168277, workflow path filter missedrunner/questions.ts: a file-naming slip.4112207464,&&in the displayed removal command: a message-formatting bug.4112271730, credential check ran after the Docker scan: step ordering in one entry point.4112361805, unbounded host clone: a lane D API gap. The size check before cloning is a stopgap until D provides a bounded clone (D follow-ups required by the F1 runner lifecycle contract #51).4112551858: did not reproduce; a regression for the reported case is included.4113992060, worker started with the full environment: did not reproduce.workerEnvironment()allowlistsPATH,DOCKER_HOSTandTMPDIR, and the existing integration test covers it. The suggestedthis.envis the credential source and would leak the token.🤖 Generated with Claude Code