Read model input under the executing task's own scope
F24 cause #2: a spawned child could not read its own task. The local composition bound the model provider's content reader once at open, using the root's signed scope. A child's task parcel is staged at mailbox/<receive>/parcel in the child's own AgentPrivate volume. A provider that projects file bytes (stock Responses/Anthropic) therefore read it under the root's authority. require_file_read refused the read, and dispatch settled the child's model command Indeterminate. Through graphcoder -p this left the parent indeterminate after wait. The scripted providers never read bytes, so every scenario passed.
Change
FilesystemTaskCommandskeeps the provider asModelBinding::unbound. The only way to get a provider out isprovider_for(&context), which binds the executingTaskContext, the task's own scopedContentResidencyVerifier, with that scope's file limit. Both executor constructions (native and portable) use it.LocalHarness::openandbindno longer bind at open. The field is documented as unbound.- Manual compaction's token pre-count binds the root reader explicitly. The compaction task is admitted as a root task, and its model command then binds per task like every other.
ResolvedModelreader-bound copies share the token estimator and Codex counter (Arc). Before, the single open-time bind owned them, so calibration behaves as it did.ScriptedModelProvider::reading_content()andtest_support::read_request_files: scripted providers read everyBoundedFull/ProjectInstructionsfile part through the bound reader, as stock providers do. The agents e2e router reads through the reader on every request, so every scenario now exercises content reads.
Guarantee
-
Class: L, a content reader disconnected from the executing authority (an open-time, composition-wide binding). Also Iv: the scripted oracle could not observe file reads.
-
Structural: a model command can't reach a provider without binding the executing task's context.
unboundis private toModelBinding, andprovider_foris its only accessor. No composition-wide reader is bound anywhere a task dispatches. -
Entry points: every model dispatch goes through
FilesystemTaskCommands::model_command. That covers root run, headlessgraphcoder -p(local/turn.rs), spawned and nested children and their resume/reopen (children/controller.rs), steer/send parcels (mailbox model input), compaction's summary turn, and WASM (wasm/task_runtime.rs). The one non-command provider call is the compaction pre-count, which is root-admitted and bound to the root reader explicitly. -
Proof: all 16
local-scenarios/agentsscenarios now read every projected file part through the bound reader. In the stack with #1013 they all pass, as does thegraphcoder -pspawn/wait loopback regression in #1013. -
Negative control:
agents::a_composition_wide_reader_cannot_read_a_childs_taskfirst requires the normal spawn/wait scenario to succeed in an independent store, then pins the first bound reader for all tasks. It requires both zero worker calls and a recorded unauthorized content read fromworker. Zero calls by themselves are insufficient: workspace-generation failures also produce them.- Production mutant: restoring the open-time bind in
local/mod.rsand returning the unbound provider fromprovider_forfails both thegraphcoder -pregression (indeterminate) and spawn-wait ("worker": 0).
-
Gaps: none. The real-HTTP
graphcoder -pshipping proof and the positive child scenarios are proved together with #1013, merged in one batch (coordinator decision). #1013 stacks on this PR, and the two are gated and merged as one batch, so no state of main contains this change without that proof. On the batch (#1013 head d715a543f plus f78b1460e) the control and its oracle regression pass: 2 passed, 2.64 s. -
Oracle proof (review fix):
agents::reader_control_rejects_unrelated_failureschecks the refusal classifier against an unrelated workspace conflict, a root-only refusal, an actual worker refusal, and a worker that already dispatched. Its negative control executes the former zero-worker predicate against the unrelated failure and demonstrates that it accepts it. Restoring that predicate in the classifier fails the same regression. The scenario runner records the model name only when its real bound read returnsUnauthorized, before consuming a scripted response.
Decisions
- I bind the
TaskContextitself, rather than building a new verifier from a re-issued scope. It already resolves the task's own content binding (created child volumes first, then output, then harness) and enforces its runtime grants. Widening the child's grants instead was rejected: the scope issuer refuses it ("created scope widens ancestor authority"), which is the correct refusal. - Separately: on this base, the agents scenarios still fail with "workspace generation changed". That is F24 cause #1, fixed by #1013, which stacks on this PR.
codex_stock_execution_counts_locally_without_a_backend_count_routefails identically on main (items_examined65537 > 65536). It is pre-existing and not touched here.
Original author verification (87475cfd3; stacked on #1013)
- With #1013: agents e2e 16/16 plus the control, in 5.9 s;
graphcoder --test cli subagentspasses in 1.2 s. - Open-time-binding mutant: both fail.
cargo check --locked -p acyclic-harness -p acyclic-harness-providers -p graphcoder --lib --tests --target x86_64-pc-windows-gnu: clean.- Clippy: nothing in the touched files.
🤖 Generated with Claude Code
Review pass (f78b1460ec68c58b4d31d0a7f121d7ebdb920e9f)
- Fixed the negative control's false-positive class in
tests/e2e/agents.rs:492. A workspace-generation or indeterminate failure with zero worker calls used to satisfy the control even when no file read was refused. The control now requires the healthy-reader scenario to succeed first, then requires an actual unauthorized read fromworkerand zero worker dispatches. The runner records the model name at the real failed content read. The classifier regression covers unrelated failures, a root-only refusal, the intended worker refusal, and a worker that has already dispatched; its negative control is the former zero-worker predicate. - Dependency on #1013 (resolved by coordinator): the
graphcoder -preal-HTTP shipping regression and the healthy-reader baseline are proved together with #1013, merged in one batch. On this branch alone the baseline stops at the pre-existing workspace-generation failure (F24 cause 1, fixed by #1013), which the control reports as a failure rather than a pass.
Validation through Q:/lanes/bs.ps1 (currently routed to bb2), with filesystem-local,native-execution,test-support,grpc throughout:
- Focused e2e binary compilation: 8m08s, once.
agents::reader_control_rejects_unrelated_failures: 1 passed, 0 failed, wall 0.007s. Restoring the old classifier makes its unrelated-failure assertion fail.agents::a_composition_wide_reader_cannot_read_a_childs_task: 0 passed, 1 failed, wall 1.519s. It stops on the normal-reader baseline withparent: 1,worker: 0, andunauthorized content readers: []; the wrong-reader mutation is not reached. No rerun.- Windows GNU compile proof:
cargo check --locked -p acyclic-harness -p acyclic-harness-providers --lib --tests --features filesystem-local,native-execution,test-support,grpc --target x86_64-pc-windows-gnu: passed, wall 325.342s. - Combined build-slot job: 1170.0s including queue and compilation; expected aggregate exit 101 from the pre-existing runtime failure. Logs:
Q:/lanes/passes/1027/pr-1027-gate.log.
Review pass round 2 (Claude, f78b1460e)
- The control at
tests/e2e/agents.rs:492can no longer pass vacuously. It first runs the healthy-reader spawn/wait scenario, which must succeed. The composition-reader run must then fail with zero worker dispatches and exactly one recordedUnauthorizedread fromworker. The name is recorded only inside the worker's real bound read, so zero child calls, or a call with no refusal, fails the control. - The batch probe ran on bb2, on #1013 head d715a543f plus f78b1460e (
--features filesystem-local,native-execution,test-support,grpc, debug):a_composition_wide_reader_cannot_read_a_childs_taskandreader_control_rejects_unrelated_failuresgave 2 passed, 0 failed, in 2.64 s (compile 5m53s). - The Guarantee's Gaps now reads "none: proved together with #1013, merged in one batch". No code changes.
Summary by cubic
Fixes spawned child tasks being unable to read their own task input, which left their model commands indeterminate.
The model provider's content reader was bound once at composition open under the root's signed scope. A child's task parcel is staged in the child's own private volume, so reads were refused and the child's model command settled Indeterminate. Now a model command can only get a provider through provider_for(&context), which binds the executing task's own scope, file limit, and ContentResidencyVerifier. Scripted providers now read every projected file part through the bound reader, so the agents e2e scenarios exercise content reads as stock providers do.
- Added a negative control test pinning a composition-wide reader; the child then never reaches its model (
"worker": 0). - Compaction's token pre-count binds the root reader explicitly, since the compaction task is admitted as a root task.
- Reader-bound copies share the token estimator and Codex counter via
Arc, so calibration behavior is unchanged.