Read model input under the executing task's own scope

Merged
VA
8h ago
+288-74

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

  • FilesystemTaskCommands keeps the provider as ModelBinding::unbound. The only way to get a provider out is provider_for(&context), which binds the executing TaskContext, the task's own scoped ContentResidencyVerifier, with that scope's file limit. Both executor constructions (native and portable) use it.
  • LocalHarness::open and bind no 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.
  • ResolvedModel reader-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() and test_support::read_request_files: scripted providers read every BoundedFull/ProjectInstructions file 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. unbound is private to ModelBinding, and provider_for is 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, headless graphcoder -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/agents scenarios now read every projected file part through the bound reader. In the stack with #1013 they all pass, as does the graphcoder -p spawn/wait loopback regression in #1013.

  • Negative control:

    • agents::a_composition_wide_reader_cannot_read_a_childs_task first 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 from worker. Zero calls by themselves are insufficient: workspace-generation failures also produce them.
    • Production mutant: restoring the open-time bind in local/mod.rs and returning the unbound provider from provider_for fails both the graphcoder -p regression (indeterminate) and spawn-wait ("worker": 0).
  • Gaps: none. The real-HTTP graphcoder -p shipping 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_failures checks 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 returns Unauthorized, before consuming a scripted response.

Decisions

  • I bind the TaskContext itself, 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_route fails identically on main (items_examined 65537 > 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 subagents passes 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)

  1. 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 from worker and 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.
  2. Dependency on #1013 (resolved by coordinator): the graphcoder -p real-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 with parent: 1, worker: 0, and unauthorized 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:492 can 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 recorded Unauthorized read from worker. 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_task and reader_control_rejects_unrelated_failures gave 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.