Hmbown/CodeWhale · error

planner must dispatch a bounded read

Error message

planner must dispatch a bounded read

What it means

This panic is `.expect("planner must dispatch a bounded read")` on `registry.execute("agent_planner", "bash", json!({"command": "cat sentinel.txt"}))` in crates/tui/src/tools/subagent/tests.rs:8596. It asserts that the planner role, after refusing dangerous commands, still allows a proven read-only bash probe whose output must equal the sentinel. If the planner's bash policy rejects even the bounded read (or the command output differs), the expect panics.

Solutions

  1. Confirm the planner's bash policy explicitly permits bounded read commands (cat of files within the workspace context).
  2. Ensure `allow_shell` is true for the planner runtime profile even though other roles (Scout) may have it off.
  3. Verify the sentinel file exists (fixture write at line ~8588 succeeded) and that registry.context points at the same tmp path.
  4. Log the refusal envelope (envelope_refusal) to see which rule rejected the command and adjust the policy, not the test.

Example fix

// before: planner bash ceiling blocks every bash invocation
if !command.starts_with("git ") { return refuse("bash", "not allowed"); }
// after: allow bounded read-only probes
if is_bounded_read(command) { return execute_read(command); }
if !command.starts_with("git ") { return refuse("bash", "not allowed"); }
Defensive patterns

Strategy: validation

Validate before calling

// pre-check the planner policy before dispatching:
fn allows_bounded_read(policy: &PlannerBashPolicy, cmd: &str) -> bool {
    policy.is_bounded_read(cmd)
}
assert!(allows_bounded_read(&policy, "cat sentinel.txt"),
    "planner policy must permit bounded read probes");

Type guard

fn is_bounded_read(cmd: &str) -> bool {
    let (bin, _) = cmd.split_once(' ').unwrap_or((cmd, ""));
    matches!(bin, "cat" | "head" | "tail" | "ls")
}

Try / catch

if let Some(refusal) = registry.envelope_refusal("bash", &payload) {
    panic!("bounded read unexpectedly refused: {refusal:?} — inspect planner bash ceiling");
}
let output = registry.execute("agent_planner", "bash", payload).await
    .unwrap_or_else(|e| panic!("bounded read dispatch failed: {e:?}"));

Prevention

When it happens

Trigger: Executing a benign `cat sentinel.txt` through an `agent_planner` registry whose bash allow/deny policy misclassifies the command as unbounded, when `allow_shell` is false for the planner profile, or when the sentinel file was not created at tmp.path().

Common situations: Tightening the planner bash ceiling so it drops previously allowed read probes, forgetting to seed the sentinel file, or running with allow_shell disabled globally instead of per-role.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/e8e4d96fcc99fe94. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/subagent/tests.rs:8596

    }
    for command in ["rm -rf crates", "git push origin main", "bash -lc 'id'"] {
        assert!(
            registry
                .envelope_refusal("bash", &json!({"command": command}))
                .is_some(),
            "planner must refuse {command}"
        );
    }
    let sentinel = "PLANNER_PROBE_SENTINEL";
    std::fs::write(tmp.path().join("sentinel.txt"), sentinel).expect("sentinel");
    let output = registry
        .execute(
            "agent_planner",
            "bash",
            json!({"command": "cat sentinel.txt"}),
        )
        .await
        .expect("planner must dispatch a bounded read");
    assert_eq!(output, sentinel);
}

#[tokio::test]
async fn scout_shell_respects_parent_shell_and_network_ceilings() {
    let tmp = tempdir().expect("tempdir");
    let mut shell_off =
        stub_runtime().with_agent_tool_surface_options(enabled_agent_surface_options());
    shell_off.context = ToolContext::new(tmp.path().to_path_buf());
    shell_off.allow_shell = false;
    shell_off.worker_profile = WorkerRuntimeProfile::for_role(FleetRole::Scout);
    let shell_off = SubAgentToolRegistry::new(
        shell_off,
        FleetRole::Scout,
        None,
        crate::tools::todo::new_shared_todo_list(),
        crate::tools::plan::new_shared_plan_state(),
    );

View on GitHub (pinned to 433685b202)