# How to Control Agent Tool Execution with Tool Policies in aisuite

> Learn how to control agent tool execution in aisuite using flexible tool policies. Intercept and approve or deny every tool call with simple checks or structured decisions.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: how-to-guide
- Published: 2026-08-03

---

**aisuite provides a flexible policy system that lets developers intercept and approve or deny every tool call before execution, using either simple boolean checks or structured `ToolPolicyDecision` objects.**

Every time an aisuite agent attempts to invoke a tool, the framework evaluates a **tool policy** to determine whether the call should proceed. This policy-based gatekeeper mechanism, implemented in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) and [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py), gives you centralized control over safety, compliance, and operational constraints without modifying individual tool implementations.

## What Is a Tool Policy in aisuite?

A tool policy is a callable or class that receives a `ToolPolicyContext` object and returns either:

- A boolean (`True` to allow, `False` to deny)
- A `ToolPolicyDecision` instance containing an `allowed` flag and optional `reason` string

If denied, the agent receives the error message `Tool call denied by policy` and execution continues without the tool running.

The core data structures are defined in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py):

| Structure | Purpose |
|-----------|---------|
| `ToolPolicyContext` | Holds pending tool call details: tool name, arguments, metadata, agent state |
| `ToolPolicyDecision` | Structured response with `allowed: bool` and `reason: str` |

## Built-In Tool Policy Types

### AllowPolicy and DenyPolicy

The simplest policies use bare functions. **AllowPolicy** logic returns `True` only for explicitly permitted tools:

```python
import aisuite as ai

def allow_read_file(context: ai.ToolPolicyContext) -> bool:
    """Permit only the read_file tool."""
    return context.tool_name == "read_file"

result = ai.run_sync(
    agent,
    "Read the first line of /etc/hosts",
    tool_policy=allow_read_file,
)

```

**DenyPolicy** inverts this logic, returning `False` for blacklisted tools.

### AllowToolsPolicy

For whitelisting multiple tools without writing custom logic, use the built-in `AllowToolsPolicy` class defined in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py):

```python
import aisuite as ai

policy = ai.AllowToolsPolicy(["read_file", "search_web"])

result = ai.run_sync(
    agent,
    "Search the web for the latest AI news",
    tool_policy=policy,
)

```

### RequireApprovalPolicy

For human-in-the-loop or external approval workflows, the class-based **RequireApprovalPolicy** accepts a callback function:

```python
import aisuite as ai

class RequireApprovalPolicy:
    def __init__(self, approve_cb):
        self.approve_cb = approve_cb

    def evaluate(self, context: ai.ToolPolicyContext) -> ai.ToolPolicyDecision:
        approved = self.approve_cb(context)
        return ai.ToolPolicyDecision(
            allowed=approved,
            reason="User approved" if approved else "User denied"
        )

def ask_user(context):
    # Replace with actual UI prompt in production

    return True

policy = RequireApprovalPolicy(ask_user)

result = ai.run_sync(
    agent,
    "Execute `ls -l /var/log`",
    tool_policy=policy,
)

```

## Custom Class-Based Policies

For complex logic like rate-limiting, per-user quotas, or time-based restrictions, implement a class with an `evaluate(self, context)` method:

```python
class RateLimitedPolicy:
    def __init__(self, max_calls_per_minute: int):
        self.max_calls = max_calls_per_minute
        self.call_times = []

    def evaluate(self, context: ai.ToolPolicyContext) -> ai.ToolPolicyDecision:
        # Custom rate-limiting logic here

        pass

```

## How Tool Policy Evaluation Works

The execution flow in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) follows these steps, as implemented in `_prepare_tool_call` and `_evaluate_tool_policy`:

1. The agent emits a tool call request
2. `_prepare_tool_call` builds a `ToolPolicyContext` with complete call metadata
3. `_evaluate_tool_policy` invokes the configured policy (default: allow-all)
4. If allowed, the tool function executes; otherwise, an error is recorded and the call is skipped

All policy evaluation events are stored in `last_policy_events` for tracing and auditing. The test suite in [`tests/agents/test_tool_policy.py`](https://github.com/andrewyng/aisuite/blob/main/tests/agents/test_tool_policy.py) demonstrates allowed and denied behaviors with concrete assertions.

## Tool Policies vs. Other Access Control Methods

| Approach | Granularity | Use Case |
|----------|-------------|----------|
| **Tool policy** | Per-call evaluation | Dynamic, context-aware decisions |
| Tool decorators | Tool definition time | Static capability restrictions |
| Agent configuration | Runtime parameter selection | Broad policy assignment |

Tool policies excel when decisions depend on runtime context—user identity, time of day, or external service availability—that cannot be determined when tools are defined.

## Complete Example: Blocking Dangerous Commands

From [`examples/cli/create_demo_trace.py`](https://github.com/andrewyng/aisuite/blob/main/examples/cli/create_demo_trace.py), here's a policy that blocks shell execution and returns explanatory reasons:

```python
import aisuite as ai

class SafeExecutionPolicy:
    BLOCKED_TOOLS = {"shell", "exec", "eval"}

    def evaluate(self, context: ai.ToolPolicyContext) -> ai.ToolPolicyDecision:
        if context.tool_name in self.BLOCKED_TOOLS:
            return ai.ToolPolicyDecision(
                allowed=False,
                reason=f"Security policy prohibits {context.tool_name}"
            )
        return ai.ToolPolicyDecision(allowed=True)

policy = SafeExecutionPolicy()

```

## Summary

- aisuite tool policies gate every tool call through a configurable `evaluate` function or callable
- Return `True`/`False` for simple cases, or `ToolPolicyDecision` for structured responses with reasons
- Built-in policies in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) cover common whitelist and approval workflows
- The evaluation pipeline in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) provides full audit trails via `last_policy_events`
- Class-based policies support complex logic like rate-limiting and contextual access control

## Frequently Asked Questions

### What happens when a tool policy denies a call?

The tool does not execute, and the agent receives the string `Tool call denied by policy` as the tool result. The agent can then respond to this error message or attempt alternative approaches. The denial is recorded in `last_policy_events` for later inspection.

### Can I combine multiple tool policies?

aisuite accepts a single policy per agent run. To combine rules, implement a composite policy class that evaluates sub-policies in sequence and returns the first denial or final approval. The `ToolPolicyDecision` structure supports detailed reasoning about which sub-policy triggered a block.

### Where are tool policy decisions logged?

Policy evaluation events are stored in the `last_policy_events` list on the agent or runner object. This enables programmatic inspection after execution and supports compliance auditing without external logging infrastructure.

### How do I migrate from function-based to class-based policies?

Replace your boolean-returning function with a class implementing `evaluate(self, context: ToolPolicyContext) -> ToolPolicyDecision`. The method signature change is backward-compatible through aisuite's detection logic—both styles are supported simultaneously in the same codebase.