# How to Implement Custom Tool Policies for Agent Tool Execution Control in aisuite

> Learn to control agent tool execution in aisuite with custom Tool Policies. Implement the evaluate method to decide which tool calls to execute, deny, or approve.

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

---

**aisuite provides a flexible `ToolPolicy` protocol that allows you to control whether agent tool calls are executed, denied, or require additional approval by implementing an `evaluate` method that receives a `ToolPolicyContext` and returns a `ToolPolicyDecision` or boolean.**

The **aisuite** framework offers a robust mechanism to implement custom tool policies for agent tool execution control, enabling per-call security checks, rate limiting, and human-in-the-loop approval workflows. By leveraging the `ToolPolicy` protocol and related types defined in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py), developers can inject custom logic at the precise moment before a tool executes. This architecture supports both stateful class-based policies and lightweight callable functions, giving you complete control over tool execution safety and compliance.

## Understanding the Tool Policy Architecture

The tool policy system in aisuite revolves around three core abstractions that work together to standardize execution control across the framework.

### Core Components

**`ToolPolicyContext`** is a dataclass defined at `aisuite/agents/types.py#L27-L40` that bundles runtime information passed to every policy evaluation. It contains the agent name, tool name, arguments, tool metadata, and run-specific details, providing the policy with complete visibility into the pending execution context.

**`ToolPolicyDecision`** (located at `aisuite/agents/types.py#L84-L90`) standardizes the return format for policy evaluations. This dataclass includes an `allowed` boolean, an optional `reason` string for denials, and optional `metadata` for audit trails.

**`ToolPolicy`** is a Protocol defined at `aisuite/agents/types.py#L42-L45` that requires a single `evaluate(context)` method returning `bool | ToolPolicyDecision`. This design allows both class-based implementations and plain callables to satisfy the interface.

### Built-in Policy Classes

aisuite ships with several ready-made policies in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) for common scenarios:

- **`AllowAllToolPolicy`** – Permits every tool call unconditionally
- **`DenyAllToolPolicy`** – Blocks every tool call unconditionally  
- **`AllowToolsPolicy`** – Whitelists specific tool names
- **`RequireApprovalPolicy`** – Delegates decisions to an external approval function

### Policy Evaluation Flow

When a tool call is pending, the engine invokes `Tools._evaluate_tool_policy` (implemented at `aisuite/utils/tools.py#L399-L438`). This method:

1. Constructs a `ToolPolicyContext` from the active run context
2. Invokes the policy via `policy.evaluate(context)` or direct callable invocation
3. Normalizes the result: `bool` values become `ToolPolicyDecision(allowed=bool)`, while existing `ToolPolicyDecision` objects pass through unchanged
4. If `allowed` is `False`, the engine blocks execution and emits a tool-policy event containing the denial reason

Both the **`Runner`** class (`aisuite/agents/runner.py#L75-L93`) and the high-level **`Client`** (`aisuite/client.py#L497-L527`) accept a `tool_policy` argument that propagates to this evaluation engine.

## Implementing a Custom Tool Policy

You can implement custom tool policies for agent tool execution control by creating a class with an `evaluate` method or by providing a simple callable function.

### Class-Based Implementation

For stateful policies requiring configuration, persistent counters, or external service integration, implement the `ToolPolicy` protocol as a class:

```python
from aisuite.agents.types import ToolPolicyDecision, ToolPolicyContext

class RiskBasedToolPolicy:
    """Deny high-risk tools while allowing low-risk operations."""
    
    def evaluate(self, context: ToolPolicyContext) -> ToolPolicyDecision:
        # Access tool metadata populated during tool registration

        meta = context.tool_metadata
        
        if meta and hasattr(meta, 'risk_level') and meta.risk_level == "high":
            return ToolPolicyDecision(
                allowed=False,
                reason="High-risk tools are prohibited in production environments"
            )
        
        return ToolPolicyDecision(allowed=True, reason="Risk assessment passed")

```

Attach this policy when running an agent:

```python
from aisuite.agents.runner import Runner
from aisuite.agents.types import Agent

def delete_database():
    return "Database deleted"

agent = Agent(name="dba", model="openai:gpt-4o", tools=[delete_database])

# High-risk tool calls will be blocked

result = Runner.run_sync(
    agent, 
    "Delete the database", 
    tool_policy=RiskBasedToolPolicy()
)

```

### Using Simple Callables

For straightforward allow/deny logic without state management, pass a lambda or function directly:

```python

# Allow only the 'echo' tool

allow_only_echo = lambda ctx: ctx.tool_name == "echo"

result = Runner.run_sync(
    agent,
    "Say hello",
    tool_policy=allow_only_echo
)

```

When a callable returns `False`, the engine automatically generates a denial with the default reason "Tool call denied by policy".

### Extending Built-in Policies

Combine built-in policies with custom logic by wrapping them in composite policies:

```python
from aisuite.agents.policies import AllowToolsPolicy
from datetime import datetime

class BusinessHoursPolicy:
    """Allow specific tools only during business hours."""
    
    def __init__(self):
        self.whitelist = AllowToolsPolicy(["read_file", "search_docs"])
    
    def evaluate(self, context: ToolPolicyContext) -> ToolPolicyDecision:
        # First check the whitelist

        decision = self.whitelist.evaluate(context)
        if not decision.allowed:
            return decision
            
        # Additional time-based restriction

        if datetime.now().hour >= 18 or datetime.now().hour < 9:
            return ToolPolicyDecision(
                allowed=False,
                reason="Tool execution restricted to business hours (9 AM - 6 PM)"
            )
            
        return ToolPolicyDecision(allowed=True)

```

## Practical Custom Policy Examples

The following implementations demonstrate common real-world scenarios for controlling tool execution.

### Human Approval Policy

Implement a human-in-the-loop workflow by delegating decisions to an external approval interface:

```python

# policies/human_approval.py

from aisuite.agents.types import ToolPolicyDecision, ToolPolicyContext
from typing import Callable

class HumanApprovalPolicy:
    """
    Routes each tool call through a human approval function.
    """
    def __init__(self, approve_fn: Callable[[ToolPolicyContext], bool]):
        self.approve_fn = approve_fn
    
    def evaluate(self, context: ToolPolicyContext) -> ToolPolicyDecision:
        approved = self.approve_fn(context)
        return ToolPolicyDecision(
            allowed=approved,
            reason="human approved" if approved else "human denied"
        )

```

Usage with the aisuite Client:

```python
from aisuite.client import Client
from policies.human_approval import HumanApprovalPolicy

def request_ui_approval(ctx: ToolPolicyContext) -> bool:
    # In production: Open a modal dialog or send to Slack

    print(f"Approve {ctx.tool_name} with args {ctx.arguments}?")
    return input("Enter y/n: ").lower() == 'y'

client = Client()
response = client.run(
    agent="assistant",
    prompt="Delete the temp files",
    tool_policy=HumanApprovalPolicy(request_ui_approval)
)

# Inspect the audit trail

print(response.tool_policy_events)

```

### Rate Limiting Policy

Prevent abuse by limiting calls per minute using internal state:

```python

# policies/rate_limit.py

import time
from collections import defaultdict
from aisuite.agents.types import ToolPolicyDecision, ToolPolicyContext

class RateLimitPolicy:
    """Enforce maximum calls per minute per tool."""
    
    def __init__(self, calls_per_minute: int = 10):
        self.limit = calls_per_minute
        self.history = defaultdict(list)  # tool_name -> timestamps

    
    def evaluate(self, ctx: ToolPolicyContext) -> ToolPolicyDecision:
        now = time.time()
        tool_name = ctx.tool_name
        calls = self.history[tool_name]
        
        # Remove timestamps older than 60 seconds

        recent_calls = [t for t in calls if now - t < 60]
        self.history[tool_name] = recent_calls
        
        if len(recent_calls) >= self.limit:
            return ToolPolicyDecision(
                allowed=False,
                reason=f"Rate limit exceeded: {self.limit} calls/minute for {tool_name}"
            )
        
        recent_calls.append(now)
        return ToolPolicyDecision(allowed=True, reason="Within rate limit")

```

### Metadata-Based Filtering

Use tool metadata to enforce categorization policies inline:

```python
from aisuite.agents.runner import Runner

# Allow only tools tagged as 'safe' in their metadata

safe_only = lambda ctx: (
    ctx.tool_metadata is not None and 
    getattr(ctx.tool_metadata, 'category', None) == 'safe'
)

result = Runner.run_sync(
    agent,
    "Perform operation",
    tool_policy=safe_only
)

```

## Integrating Policies with Runners and Clients

Both execution entry points in aisuite accept custom tool policies through the `tool_policy` parameter.

**Using the Runner** ([`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py)):

```python
from aisuite.agents.runner import Runner

result = Runner.run_sync(
    agent=agent,
    prompt="Analyze data",
    tool_policy=RiskBasedToolPolicy(),
    tool_policy_context={"user_tier": "premium"}  # Optional context

)

```

**Using the Client** ([`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py)):

```python
from aisuite.client import Client

client = Client()
response = client.run(
    agent="data_analyst",
    prompt="Generate report",
    tool_policy=RateLimitPolicy(calls_per_minute=5)
)

```

The optional `tool_policy_context` dictionary allows you to inject additional runtime data (such as user tiers, authentication levels, or session IDs) into the `ToolPolicyContext` available to your policy's `evaluate` method.

## Monitoring Policy Decisions in Run Traces

All policy evaluations generate auditable events stored in the run trace. When a policy denies a tool call, the engine records the decision via `Tools.last_policy_events` (maintained in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py)). 

Both the **Runner** and **Client** surfaces these events:

- Access `result.tool_policy_events` on Runner results
- Access `response.tool_policy_events` on Client responses

Each event contains the tool name, decision boolean, reason string, and timestamp, enabling compliance auditing and debugging of policy behavior.

## Summary

- **aisuite** implements custom tool policies through the `ToolPolicy` protocol requiring an `evaluate(context)` method that accepts a `ToolPolicyContext` and returns a `ToolPolicyDecision` or boolean.
- **Core types** are defined in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py), including `ToolPolicyContext` (runtime data) and `ToolPolicyDecision` (standardized return format).
- **Policy evaluation** occurs in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) via `_evaluate_tool_policy`, which normalizes boolean returns and blocks execution when `allowed=False`.
- **Integration points** include the `Runner` class ([`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py)) and `Client` class ([`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py)), both accepting `tool_policy` and optional `tool_policy_context` parameters.
- **Implementation styles** range from simple lambdas for basic filtering to stateful classes for rate limiting, human approval workflows, and composite logic.
- **Audit trails** are automatically generated and accessible via `tool_policy_events` on execution results for compliance monitoring.

## Frequently Asked Questions

### What is the ToolPolicy protocol in aisuite?

The `ToolPolicy` protocol, defined in `aisuite/agents/types.py#L42-L45`, is a structural interface that requires any implementing object to provide an `evaluate(context: ToolPolicyContext) -> bool | ToolPolicyDecision` method. This protocol allows aisuite to treat both class instances and simple functions uniformly during tool execution control.

### How do I deny specific tools using a custom policy?

Create a policy that inspects `context.tool_name` or `context.tool_metadata` and returns `ToolPolicyDecision(allowed=False, reason="...")` for prohibited tools. For simple name-based filtering, you can use the built-in `AllowToolsPolicy` with an allowlist, or implement a custom class that checks `ctx.tool_name` against a denylist.

### Can I combine multiple policies in aisuite?

Yes. While aisuite does not provide a built-in policy combinator, you can implement a composite policy class that instantiates multiple policies (such as `AllowToolsPolicy` for whitelisting and a custom policy for time restrictions) and chains their `evaluate` methods. The composite policy returns the first denial encountered, or an allowed decision only if all sub-policies permit the action.

### Where are policy decisions logged in aisuite?

Policy decisions are recorded in the run trace via the `tool_policy_events` list available on response objects from both the **Client** and **Runner**. Each event dictionary contains the tool name, the `allowed` boolean, the `reason` string (if provided), and execution metadata. These events originate from `Tools._evaluate_tool_policy` in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) and propagate through the execution context to the final response.