Back to Blog
Engineering

Limit Which Tools Each CrewAI Agent Can Call

Wrap that agent's tools with governed_tools. AgentTrustCallback runs after the task.

Limit which tools each CrewAI agent can call. governed_tools checks every call before it runs: add is approved and an undeclared divide is blocked.

October 27, 20266 min read
CrewAIgoverned_toolstool permissionspre-check
An undeclared divide tool call from a CrewAI agent reaches the governed_tools gate and is blocked; add is approved
One CrewAI tool call: divide is blocked and add is approved
TL;DR
  • Install agentrust-py[crewai] (CrewAI 1.x).
  • Wrap the agent's tools with that agent's agent_id.
  • Declare add and multiply. divide is refused.
  • Delegation tools are not wrapped. Use on_block raise to stop the crew.
Keep reading for the problem, per-agent checking, and the full walkthrough →

The governed_tools helper checks every CrewAI tool call with AgentTrust before the tool runs, so one agent does not inherit every tool in the crew. Wrap each agent's tools once, then run a small crew with and without AgentTrust and watch a call get refused.

Every tool an agent holds is a tool it can call. Declare which ones it should.
THE PROBLEM

The problem: a CrewAI agent calls whatever tools it holds

A crew is several agents, each handed tools. The model behind each agent decides which tool to call, and nothing stops a call you did not intend. In this guide's calculator crew, add and multiply are the intended tools. Run the crew with no check and the agent uses divide and nothing stops it. Swap divide for issue_refund or delete_user and it would run just the same.

Without a check divide runs though it is not declared. With governed_tools add is approved, divide is blocked and the crew still finishes
The same agent, without and with the check.
THE FIX

The fix: governed_tools checks each call before the tool runs

A CrewAI agent decides which tools to call, and nothing stops it from calling one it should not. The AgentTrust helper governed_tools wraps each tool so the call goes to AgentTrust first. By the end of this guide:

  • Every tool call is checked before it runs, and its result after.
  • A refused call never runs. The agent gets a plain Blocked by AgentTrust result as the tool's output, explains it, and the crew still finishes.
  • Every decision, allowed or refused, shows under Events in your console.

Prerequisites. Python 3.10 or later and a CrewAI 1.x crew whose agents use tools. The "See it in action" walkthrough also needs Ollama for a free local chat model. The local gateway needs no account; the console route does.

Package name. Install agentrust-py and import agentrust_sdk. The PyPI package agentrust-sdk belongs to another company.

HOW IT WORKS

Why tool permissions belong to each CrewAI agent

Five steps of one CrewAI tool call: the agent picks a tool, the wrapper asks the gateway, the gateway compares and records, then the tool runs or is refused
One tool call from request to decision.

A crew is several agents with different jobs, and each one is handed tools. Without a check, any tool an agent holds is a tool it can call. governed_tools ties the check to one agent: you wrap that agent's tools with that agent's agent_id, and the gateway judges each call against that agent's own declared tools.

What happens on one tool call

  1. The agent picks a tool. Your code has not run.
  2. The wrapper asks first. It sends the tool name and arguments, with the agent's agent_id, to the AgentTrust gateway. This is the pre-check.
  3. The gateway compares and records. It checks the call against the tools declared for that agent and writes an event either way. A tool that is not declared scores Tool Trust 0, a hard stop.
  4. Run or refuse. An allowed call runs and its result is checked afterwards, a second event. A refused call never runs. The agent receives Blocked by AgentTrust as the tool's output, explains it, and the crew still finishes. Use on_block raise if you want the crew to stop instead.

Why per-agent and not per-crew

One crew can hold an agent that only reads and another that moves money. If both inherit the same tool list, the reader can call the writer's tools. Declaring tools per agent means each agent can call only what its own manifest lists. In this guide's example the agent declares add and multiply, so divide is refused.

Before and after the task

governed_tools checks each tool call before it runs. AgentTrustCallback runs after the task, so it can record and review but cannot stop a tool that already ran. Delegation tools are not wrapped, so decide separately how delegation is handled.

MechanismWhen it runsCan it stop the tool?
governed_toolsBefore each tool callYes. A refused call never runs.
AgentTrustCallbackAfter the taskNo.
SET UP

Add governed_tools to each CrewAI agent

The helper works with any CrewAI 1.x Agent. These steps connect to your console. With no account, use the local gateway instead.

Install the adapter

Terminal: run this

pip install "agentrust-py[crewai]"

It needs CrewAI 1.x (crewai>=1.0,<2.0). CrewAI 0.x is not supported.

Register the agent

In your console, select New agent and copy its API Key and Agent ID. The key starts with at_live_ and is shown once. Select the agent, then under Manifest & fingerprint select Declare and add the tools it may use, with an optional limit for a tool that moves money, such as issue_refund up to 100.

Do not skip the manifest.

An agent with no declared tools is not restricted: every tool is allowed.

Point your app at the console

Terminal: run this

export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
export AGENTRUST_KEY=at_live_...
export AGENTRUST_AGENT_ID=support-agent

Wrap that CrewAI agent's tools once

Where: in the one place your agent's tools are defined, just before you build the Agent. Wrap the list once, then pass the wrapped list as the agent's tools. Agent itself does not change: it just receives the wrapped tools.

Agent(tools=...)

+ import os
+ from agentrust_sdk.adapters.crewai import governed_tools

  tools = [lookup_order, issue_refund]
+ tools = governed_tools(
+     tools, agent_id=os.environ["AGENTRUST_AGENT_ID"]
+ )

  agent = Agent(role="Support", goal="...", backstory="...", tools=tools)

Wrap every tool the agent has. A tool you pass to Agent(tools=...) without wrapping it is not checked. Give each agent its own agent_id so the audit trail says who acted.

Your tools do not change. @tool functions, BaseTool subclasses and plain functions with a docstring all work, and the agent sees the same names, descriptions and arguments as before.

When the agent calls issue_refund with an amount over 100, the tool does not run. The agent receives this as the tool's result and can explain or try something else:

Expected output

Blocked by AgentTrust: Tool-trust hard gate: tool_trust: 'issue_refund' amount 500.0 exceeds declared max_amount 100.0 (envelope …)

What is not checked. CrewAI's built-in delegation tools (an agent handing work to another agent of the same crew) are not wrapped. Calls to your own tools are. To stop the whole crew on a block instead of letting the agent read the refusal, pass on_block="raise".

No account? Connect to a local gateway

Skip steps 2 and 3 and start a gateway inside your program before the agent's first tool call:

Terminal: run this

pip install "agentrust-py[crewai,embedded]"

Python

from agentrust_sdk.embedded import embed_gateway

gateway = embed_gateway()
gateway.register_manifest("support-agent", {
    "declared_tools": [
        {"name": "lookup_order"},
        {"name": "issue_refund", "max_amount": 100},
    ],
})

Set AGENTRUST_AGENT_ID=support-agent (or pass the ID straight to governed_tools) and go to step 4. Decisions last only while the program runs, so use this to try AgentTrust, in tests and in CI.

Want the final answer checked too? Add guardrail(agent_id=...) to a Task: a blocked answer makes CrewAI ask the agent for a new one. See docs/framework-adapters/crewai.md. Not on CrewAI? See the LangGraph quickstart or wrap any Python function with @harness (@harness quickstart).

SEE IT BLOCKED

Run it and watch a CrewAI tool call get blocked

The same four steps, applied to a small calculator crew. You run it three ways and watch a tool call get refused. About 10 minutes. Each step has one thing to do and one thing you should see.

Before you start. Python 3.10 or later, and Ollama for a chat model that runs on your machine. Steps 1 to 4 need nothing else. Steps 5 to 7 use an AgentTrust console account.

Create the project

Make a folder with its own Python environment and install what the program needs:

Terminal: run this

mkdir agentrust-quickstart && cd agentrust-quickstart
python -m venv .venv && source .venv/bin/activate     # on Windows: .venv\Scripts\activate
pip install "agentrust-py[crewai,embedded]"
ollama pull llama3.1

Create a file named crewai_quickstart.py in that folder, paste the program below and save. It is a CrewAI crew with one agent, one task and three tools (add, multiply, divide). AgentTrust is one line, governed_tools, which you will get to in step 3. The three ways to run it are at the bottom.

The example crew. The agent names a tool; governed_tools asks the AgentTrust gateway before the tool runs; the gateway records every check as an event.

Diagram of a CrewAI calculator agent: governed_tools asks AgentTrust before the crew tool runs
The example crew. The agent names a tool. governed_tools asks the AgentTrust gateway before the tool runs.

Here is what happens when the crew is asked to add, then to divide. The tool names come from the program you are about to create.

  1. The agent names a tool. The LLM loop decides it needs add (or divide) and CrewAI calls that tool.
  2. The governed tool asks AgentTrust first. The wrapped tool sends the tool name and its arguments to the gateway. This is the pre-check, and your tool's code has not run.
  3. The gateway decides and records it. It compares the call with the agent's declared tools: add is declared, divide is not. Either way it writes an event.
  4. Allowed: the tool runs. Refused: it never does. add(3, 4) runs and its result goes back for a post-check, a second event. divide becomes a Blocked by AgentTrust result instead.
  5. The agent gets a result either way. It reads the answer or the refusal, replies, and the crew finishes.

So an allowed call leaves two events (before and after it ran) and a refused call leaves one. With --console they appear under Events in step 7; with --local the gateway lives inside the program and is gone when it exits.

See the whole program

crewai_quickstart.py

"""
crewai_quickstart.py — a CrewAI agent whose tool calls AgentTrust checks.

The agent is a calculator: one CrewAI agent with add, multiply and divide
tools, asked to do two sums.

AgentTrust is one line (marked "AgentTrust" below): the tools are wrapped with
governed_tools. Every call is checked before it runs, and its result after. A
tool the agent may not use is refused before its code runs; the agent is told
why as the tool's result and the crew still finishes. The agent may use add and
multiply, not divide.

RUN IT (in the folder that holds this file)
    pip install "agentrust-py[crewai,embedded]"
    ollama pull llama3.1                                  # a free model with tool calling

    python crewai_quickstart.py --without-agentrust       # AgentTrust off: every tool runs
    python crewai_quickstart.py --local                   # a gateway starts inside this program; no account
    python crewai_quickstart.py --console                 # your AgentTrust console keeps every decision

  --console needs three settings, from the agent you create in the console:
      export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech
      export AGENTRUST_KEY=at_live_...
      export AGENTRUST_AGENT_ID=...

ANOTHER MODEL
    CREWAI_QUICKSTART_MODEL=anthropic/claude-sonnet-4-6 (export ANTHROPIC_API_KEY).
    Any CrewAI model string for a model that follows the ReAct tool format works.
"""
from __future__ import annotations

import argparse
import os
import sys
import tempfile

from crewai import LLM, Agent, Crew, Task
from crewai.tools import tool

from agentrust_sdk.adapters.crewai import governed_tools

AGENT_ID = os.environ.get("AGENTRUST_AGENT_ID", "calculator-agent")
MODEL = os.environ.get("CREWAI_QUICKSTART_MODEL", "ollama/llama3.1")

# The tools this agent may call. With --local they are set here; with --console
# you declare the same list on the agent in the console. divide is left out on purpose.
MANIFEST = {"declared_tools": [{"name": "add"}, {"name": "multiply"}]}

@tool("multiply")
def multiply(a: int, b: int) -> int:
    """Multiply `a` and `b`."""
    return a * b

@tool("add")
def add(a: int, b: int) -> int:
    """Add `a` and `b`."""
    return a + b

@tool("divide")
def divide(a: int, b: int) -> float:
    """Divide `a` by `b`."""
    return a / b

# AgentTrust: every call to these tools is checked before it runs and after it
# returns. Give the result to the Agent as its tools.
tools = governed_tools([add, multiply, divide], agent_id=AGENT_ID)
tools_by_name = {t.name: t for t in tools}

def build_crew(llm) -> Crew:
    """One calculator agent and one task, `{request}` filled in at kickoff."""
    calculator = Agent(
        role="Calculator",
        goal="Do the arithmetic you are asked for, using your tools.",
        backstory="You perform arithmetic on a set of inputs, one tool call at a time.",
        tools=tools,
        llm=llm,
        allow_delegation=False,
        verbose=True,
    )
    task = Task(
        description="{request}",
        expected_output="The numeric answer, or a short note that the operation could not be done.",
        agent=calculator,
    )
    return Crew(agents=[calculator], tasks=[task], verbose=True)

def ask_crew(where: str) -> bool:
    """Ask the crew to add, then to divide. False if the model can't be reached."""
    print(f"AgentTrust: {where} · agent {AGENT_ID} · model {MODEL}")
    crew = build_crew(LLM(model=MODEL, temperature=0))
    try:
        for request in ["Add 3 and 4.", "Divide 10 by 2."]:
            print(f"\n=== {request} ===")
            print(f"\nResult: {crew.kickoff(inputs={'request': request}).raw}")
    except Exception as exc:  # noqa: BLE001 - any provider error ends the run
        print(f"\nThe model call failed ({MODEL}): {exc}\n"
              "Check the model provider is reachable (Ollama: `ollama serve` and `ollama pull llama3.1`).",
              file=sys.stderr)
        return False
    return True

# --without-agentrust: every tool runs. For comparison.
def run_without_agentrust() -> int:
    os.environ["AGENTRUST_ENABLED"] = "false"   # the SDK's off switch: wrapped tools pass every call through
    return 0 if ask_crew("off (every tool runs)") else 1

# --local: a gateway runs inside this program; no account needed.
def run_local() -> int:
    from agentrust_sdk.embedded import embed_gateway

    # SQLite in a temp dir, gone after the run. embed_gateway sets
    # AGENTRUST_GATEWAY_URL and AGENTRUST_KEY, which governed_tools reads.
    gateway = embed_gateway(db_path=os.path.join(tempfile.mkdtemp(), "gateway.db"))
    gateway.register_manifest(AGENT_ID, MANIFEST)
    try:
        ok = ask_crew(f"local gateway {gateway.url} (no account, nothing kept)")
    finally:
        gateway.stop()
    return 0 if ok else 1

# --console: the SDK sends every check to your console, which keeps the decisions.
def run_with_console() -> int:
    missing = [v for v in ("AGENTRUST_GATEWAY_URL", "AGENTRUST_KEY", "AGENTRUST_AGENT_ID") if not os.environ.get(v)]
    if missing:
        print(f"Set {', '.join(missing)} (from the agent you created in the console), or use --local.",
              file=sys.stderr)
        return 2
    console = os.environ["AGENTRUST_GATEWAY_URL"]
    ok = ask_crew(f"console {console}")
    print(f"\nOpen Events in the console ({console}) and filter by agent ID {AGENT_ID}.")
    return 0 if ok else 1

def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="The CrewAI calculator agent, governed by AgentTrust.")
    where = parser.add_mutually_exclusive_group()
    where.add_argument("--local", action="store_true", help="run a gateway inside this program, no account (the default)")
    where.add_argument("--console", action="store_true", help="send every check to your AgentTrust console")
    where.add_argument("--without-agentrust", action="store_true", help="for comparison: AgentTrust off, every tool runs")
    args = parser.parse_args(argv)
    if args.without_agentrust:
        return run_without_agentrust()
    return run_with_console() if args.console else run_local()

if __name__ == "__main__":
    sys.exit(main())

Check that the file works:

Terminal: run this

python crewai_quickstart.py --help

Expected output

usage: crewai_quickstart.py [-h] [--local | --console | --without-agentrust]

If Python cannot find a package, the environment from the first command is not active. Run its second line again.

Run it without AgentTrust

First, see what the crew does on its own:

Terminal: run this

python crewai_quickstart.py --without-agentrust

The crew is asked to add, then to divide. The run takes under a minute on a laptop, longer the first time while the model loads. CrewAI prints a box for each stage; these are the lines that matter, with the rest left out:

Expected output

AgentTrust: off (every tool runs) · agent calculator-agent · model ollama/llama3.1
=== Add 3 and 4. ===
...
  Tool: add
  Output: 7
...
Result: The numeric answer is 7.
=== Divide 10 by 2. ===
...
  Tool: divide
  Output: 5.0
...
Result: The numeric answer is 5.0.

The agent used divide and nothing stopped it. For a calculator that is harmless. Swap divide for issue_refund or delete_user and it would run just the same.

Where governed_tools sits in the CrewAI file

This is the only change AgentTrust needs, and it is already in your file, so there is nothing to edit. Find the governed_tools line in the program, near the top.

Where: right after the tools are defined and before the Agent is built. How: wrap the list once, then pass the wrapped list as the agent's tools. On your own crew it looks like this:

crewai_quickstart.py

+ from agentrust_sdk.adapters.crewai import governed_tools

- tools = [add, multiply, divide]
+ tools = governed_tools([add, multiply, divide], agent_id=AGENT_ID)

  calculator = Agent(..., tools=tools)   # the wrapped list

Names, descriptions and argument types stay the same, so the agent sees what it saw before. A tool you pass unwrapped is not checked. Every tool call now goes through AgentTrust first. Allowed calls run and their results are checked. Refused calls never run.

Run it locally

Now run the same crew with AgentTrust on:

Terminal: run this

python crewai_quickstart.py --local

The program starts a small AgentTrust gateway inside itself and allows only the tools in MANIFEST: add and multiply. divide is not on the list. No account needed.

Expected output

AgentTrust: local gateway http://127.0.0.1:8765 (no account, nothing kept) · agent calculator-agent · model ollama/llama3.1
=== Add 3 and 4. ===
...
  Tool: add
  Output: 7
...
Result: The numeric answer is 7.
=== Divide 10 by 2. ===
...
  Tool: divide
  Output: Blocked by AgentTrust: Tool-trust hard gate: action 'divide' is
  not in this agent's declared_tools (envelope
  358eb898-bc4b-4a1e-b587-69e382e54ebd)
...
Result: The operation could not be done.

add ran. divide was refused before its code ran, and the agent was told why, so the crew still finished. Nothing is kept after the program exits.

Create the agent in your console

To keep decisions and manage the agent from the console, register it there. In your AgentTrust console:

  1. Open Agents, select New agent, name it crewai-quickstart, and select Create.
  2. Copy the API Key and the Agent ID before you select Done. The key starts with at_live_ and is shown only once.
  3. Select the agent in the list. In the panel that opens, under Manifest & fingerprint, select Declare. Under Declared tools, select + Add tool twice, type add and multiply, and select Save manifest.

Step 3 is the same list MANIFEST holds in the program, now kept in the console.

An agent with no declared tools is not restricted. Every tool is allowed, so divide would run. In a run against the hosted console with an agent that had no declared tools, divide returned 5.0.

Run it against the console

Give the program the three values from step 5, then run it with --console:

Terminal: run this

export AGENTRUST_GATEWAY_URL=https://console.agent-trust.tech   # your console's address
export AGENTRUST_KEY=at_live_...                                # the API key
export AGENTRUST_AGENT_ID=crewai-quickstart                     # the Agent ID

python crewai_quickstart.py --console

The first line names the console, and the run should match step 4: add returns 7 and divide is refused with the same Blocked by AgentTrust text. The last line tells you where to look. This output is from a run against the hosted console with an agent whose ID was crewai-test; yours shows your own Agent ID:

Expected output

AgentTrust: console https://console.agent-trust.tech · agent crewai-test · model ollama/llama3.1
Open Events in the console (https://console.agent-trust.tech) and filter by agent ID crewai-test.

See the decisions in the console

Open Events in the console and type your Agent ID into Filter by Agent ID. You will see one row per decision: add approved before and after it ran, and divide blocked. Select a row to see the scores behind the decision.

Console events. A CrewAI agent's decisions appear under Events the same way: add approved, divide blocked. The capture below is from the LangGraph post, filtered to agent langgraph-quickstart, because the Events table looks the same for both.

AgentTrust Events filtered to langgraph-quickstart, with one Block row and two Approve rows
Events. Filtered to langgraph-quickstart. One Block row and two Approve rows.

You are done. Your crew's tool calls are now checked, refused when they should not run, and recorded.

Blocked is a result, not a crash: the agent explains it and the crew finishes.
LIMITS

What the CrewAI check covers and what it doesn't

The same crew, without and with the one change: wrapping its tools.

Your agent can…WithoutWith
Be limited to the tools you allow you ran this✗✓
Have a call stopped before it runs you ran this✗✓
Tell the agent why a call was refused, and keep running you ran this✗✓
Leave a record of every decision, with scores✗✓
Have a tool's result checked before the agent sees it✗✓
Cap risky amounts, such as a refund over 100✗✓
Hold a risky call until a person approves it✗✓
Be stopped instantly with a kill switch✗✓
Refuse every call if AgentTrust can't be reached (optional)✗✓
Have a task's final answer checked, and retried if blocked✗✓

✗ means CrewAI does not provide it, so you would build it yourself. The first three rows are what you just ran (steps 2 and 4). The others are covered by the SDK's tests (a conformance suite runs real CrewAI crews against a real gateway) and the CrewAI adapter guide (docs/framework-adapters/crewai.md), not run here.

NEXT STEPS

Next steps after the CrewAI tool check

The same check on a LangGraph tool edge is in Block Unsafe LangGraph Tool Calls Before They Run. On plain Python functions, use @harness. The full walkthrough is in the CrewAI integration docs.

HOW THIS WAS CHECKED

How this CrewAI example was checked

Steps 1 to 4 were run on 27 October 2026 on a laptop with Llama 3.1 on Ollama, agentrust-py 0.0.6 from PyPI and CrewAI 1.15.30, on Python 3.12. The outputs above are copied from those runs, with CrewAI's decorative boxes cut to the lines that matter.

Not yet checked: the console route end to end. --console was run against the hosted console with an agent key (the program connected and finished), but with an agent that had no declared tools, so the refusal in step 6 and the Events view in step 7 are described from the local run and the LangGraph page, not run here. The console's button names come from its source code.

FREQUENTLY ASKED QUESTIONS

Common questions

The tools you pass to it for one agent, with that agent's agent_id. Delegation tools are not wrapped, so decide separately how delegation is handled.

A plain Blocked by AgentTrust result as the tool's output. The agent explains it and the crew still finishes. Use on_block raise to stop the crew instead.

AgentTrustCallback runs after the task, so it can record and review but cannot stop a tool that already ran. governed_tools checks before each call.

CrewAI 1.x. Install agentrust-py[crewai].

The Events table looks the same for both. The capture is filtered to agent langgraph-quickstart, so the CrewAI post links to it instead of repeating it.

READY TO GOVERN YOUR AGENTS?

No AI agent enters production without AgentTrust

Check every tool call before it runs, and keep a record of every decision.

See Pricing & Start Free →

More from the blog

EngineeringOctober 27, 2026EngineeringOctober 27, 2026EngineeringOctober 27, 2026AI ComplianceJuly 22, 2026AI ComplianceJuly 22, 2026AI GovernanceJuly 22, 2026AI GovernanceJuly 22, 2026AI ArchitectureJuly 22, 2026AI SecurityJuly 16, 2026MLOpsJuly 10, 2026AI ImplementationJuly 8, 2026AI Agent ArchitectureJuly 5, 2026AI Agent ArchitectureJune 30, 2026EngineeringJune 23, 2026AI Agent ArchitectureJuly 2, 2026AI Agent ArchitectureJuly 1, 2026IntegrationsJuly 1, 2026IntegrationsJuly 1, 2026IntegrationsJuly 2, 2026AI SecurityJuly 3, 2026AI SecurityJuly 1, 2026AI ComplianceJuly 2, 2026AI ComplianceJuly 3, 2026AI StrategyJuly 2, 2026AI StrategyJuly 3, 2026AI StrategyJuly 3, 2026AI StrategyJuly 3, 2026AI GovernanceJuly 28, 2026Healthcare AIJuly 28, 2026ArchitectureJuly 29, 2026ArchitectureJuly 29, 2026AI StrategyJuly 29, 2026AI StrategyJuly 30, 2026AI SecurityJuly 30, 2026AI ComplianceJuly 30, 2026EngineeringJuly 30, 2026AI GovernanceAugust 4, 2026EngineeringAugust 4, 2026EngineeringAugust 4, 2026Agent SecurityAugust 19, 2026AI ComplianceSeptember 4, 2026EngineeringSeptember 4, 2026AI StrategySeptember 4, 2026AI GovernanceSeptember 4, 2026