Hmbown/CodeWhale · error · anyhow::Error

[role.posture.denied] Tool

Error message

[role.posture.denied] Tool {name} is not permitted for the read-only Fleet role `{role}`. Use an `implement` or `general` role (or `custom` with an explicit allowed_tools list) to mutate the workspace or run shell commands.

What it means

The sub-agent's Fleet role is read-only and the tool it called is categorically not permitted by the role posture — unlike error 2610, the tool is not even a candidate for bounded read-only shell grammar. The posture check (#3217) hard-denies mutation and shell for read-only roles regardless of parent-session auto-approval.

Solutions

  1. Use an `implement` or `general` role for the sub-agent, or define the role as `custom` with an explicit `allowed_tools` list including the tools it needs.
  2. Keep mutation work out of read-only children: have them report findings and let a write-capable member apply changes.
  3. Verify agent_type.as_str() for the dispatched child matches the intended role in your Fleet configuration.

Example fix

// before
{"subagent":{"role":"review","tools":["edit"]}}
// after
{"subagent":{"role":"implement"}}
Defensive patterns

Strategy: validation

Validate before calling

if !role_allows_mutation(role) && write_family_tools.iter().any(|t| requested_tools.contains(t)) {
    return Err("role {} cannot run tool {}".format(role, tool));
}

Try / catch

if let Err(e) = child.execute_tool(name, input) {
    if e.to_string().contains("[role.posture.denied]") {
        // re-dispatch with an `implement`/`general`/custom-allowed_tools role
    }
}

Prevention

When it happens

Trigger: A child agent with a read-only Fleet role calls any mutating tool (write/edit/patch style tools) or a shell tool, and `allows_bounded_readonly_bash(name)` returns false, so the generic posture denial branch fires.

Common situations: Configuring a sub-agent with a review/analysis role but the model attempts file edits or command execution; mislabeling a `custom` agent without an `allowed_tools` list so it defaults to read-only posture.

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/4e7939d32cea9cc1. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/subagent/mod.rs:17536

                .as_ref()
                .is_none_or(|list| list.iter().any(|allowed| allowed == name))
        };
        if !self.is_tool_allowed(name) || !family_action_allowed {
            return Err(anyhow!("Tool {name} not allowed for this sub-agent"));
        }
        // #3217: authoritative per-role posture — read-only roles cannot mutate
        // and non-`Full`-shell roles cannot run shell, regardless of whether
        // the parent session is auto-approved. This closes the auto-approve
        // bypass where a read-only child could quietly write or shell out.
        if !self.posture_permits_tool(name, Some(&input)) {
            if self.allows_bounded_readonly_bash(name) {
                return Err(anyhow!(
                    "[shell.readonly.command] Tool {name} input did not match the bounded read-only shell grammar for Fleet role `{role}`. {guidance}",
                    role = self.agent_type.as_str(),
                    guidance = codewhale_execpolicy::command_safety::readonly_command_help()
                ));
            }
            return Err(anyhow!(
                "[role.posture.denied] Tool {name} is not permitted for the read-only Fleet role `{role}`. Use an `implement` or `general` role (or `custom` with an explicit allowed_tools list) to mutate the workspace or run shell commands.",
                role = self.agent_type.as_str()
            ));
        }
        // Denied network capability cannot be expanded by answering a prompt.
        if self.network_is_denied() {
            reject_network_reaching_input(name, &input)?;
        }
        // The session's permission posture, applied to this child exactly as
        // it is applied to the parent turn: the deterministic Auto-Review
        // floor first, then (Auto-Review) the model guardian for holds it
        // could not prove safe, or (Ask) a prompt raised in the parent's UI.
        // Full Access still fails closed on the non-bypassable safety floor.
        // Role posture and the execution envelope below stay authoritative:
        // this gate can only decide whether a call the role permits also
        // clears the session's approval boundary.
        if let ChildGateVerdict::Deny(reason) =
            self.gate_held_call(agent_id, tool_id, name, &input).await

View on GitHub (pinned to 433685b202)