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
- Confirm the planner's bash policy explicitly permits bounded read commands (cat of files within the workspace context).
- Ensure `allow_shell` is true for the planner runtime profile even though other roles (Scout) may have it off.
- Verify the sentinel file exists (fixture write at line ~8588 succeeded) and that registry.context points at the same tmp path.
- 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
- Keep an explicit allowlist of bounded read commands in the planner policy rather than a blanket bash ban.
- Add a canary assertion that the sentinel fixture exists before dispatching reads.
- When editing role ceilings, re-run the planner dispatch test to catch over-narrowing.
- Set allow_shell per role, not globally, so planner probes survive scout restrictions.
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
- fleet task ' ' is write-capable but declares no…
- only Codewhale managed skills can be removed
- only CodeWhale managed skills can be trusted
- read-only roles may revise their private working notes
- Refusing from agent ' ' to ' '; a child may control only…
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)