# How to Implement Custom Tool Policies in aisuite for Approval Workflows

> Learn to implement custom tool policies in aisuite for approval workflows. Control tool execution with custom logic before LLM provider execution. Enhance your LLM applications today.

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

---

**aisuite provides an extensible tool-policy framework that lets you control tool execution through the `ToolPolicy` protocol, enabling approval workflows by evaluating each tool call against custom logic before the LLM provider executes it.**

aisuite is a Python library that standardizes interactions across multiple LLM providers while providing a robust agent framework with built-in safety mechanisms. Implementing custom tool policies in aisuite allows you to intercept tool calls and enforce granular approval workflows, ensuring high-risk operations require explicit authorization while low-risk tools execute automatically. The framework centers on the `ToolPolicy` protocol defined in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py), which processes `ToolPolicyContext` objects and returns structured `ToolPolicyDecision` results that determine whether a tool runs, is denied, or requires human intervention.

## Understanding the Tool Policy Architecture

The aisuite policy system consists of three core dataclasses and a protocol that together enable per-tool-call decision making.

### Core Components

Every policy evaluation receives a **ToolPolicyContext** containing metadata about the pending tool call, including the tool name, arguments, tool metadata, and run context. The policy must return a **ToolPolicyDecision** containing an `allowed: bool` field and an optional `reason: str` explaining the decision. These definitions live in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) alongside the **ToolPolicy** protocol, which specifies that any valid policy must implement an `evaluate(context) -> ToolPolicyDecision` method.

### Built-in Policy Classes

aisuite ships with several ready-made implementations in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) that cover common security patterns:

- **AllowAllToolPolicy** – Always returns `allowed=True`, permitting unrestricted tool access during development.
- **DenyAllToolPolicy** – Always returns `allowed=False`, useful for dry-run modes or completely disabling tool execution.
- **AllowToolsPolicy** – Enforces a whitelist approach, allowing only specific tool names while rejecting all others.
- **RequireApprovalPolicy** – Delegates the decision to a user-supplied callback function, enabling dynamic approval logic.

```python
from aisuite import AllowToolsPolicy, RequireApprovalPolicy

# Whitelist approach - only allow file operations

whitelist_policy = AllowToolsPolicy(
    allowed_tools=["read_file", "write_file"], 
    reason="restricted to filesystem tools"
)

# Callback approach - custom logic for approval

def approval_callback(context):
    if context.tool_name == "shell":
        return False  # Never allow shell commands

    return True

callback_policy = RequireApprovalPolicy(approval_callback)

```

## Creating Custom Tool Policies

When built-in policies are insufficient, you can implement the `ToolPolicy` protocol directly to encode complex business logic or risk assessments.

### Implementing the Protocol

A custom policy is any class that implements the `evaluate` method with the correct signature. This method receives a `ToolPolicyContext` instance and must return a `ToolPolicyDecision`. The context object provides access to `tool_name`, `arguments`, `tool_metadata`, and run-level information such as the agent name and message history.

```python
from aisuite import ToolPolicyContext, ToolPolicyDecision

class RiskBasedPolicy:
    """Auto-approve low-risk tools, deny high-risk operations."""
    def evaluate(self, context: ToolPolicyContext) -> ToolPolicyDecision:
        metadata = context.tool_metadata
        if metadata and metadata.risk_level == "high":
            return ToolPolicyDecision(
                allowed=False, 
                reason="high-risk tool requires manual review"
            )
        return ToolPolicyDecision(allowed=True, reason="low-risk auto-approved")

```

### Using Callable Shortcuts

For simple logic, you do not need to define a class. The `Runner` accepts a plain callable that accepts a `ToolPolicyContext` and returns either a `bool` or a `ToolPolicyDecision`. The runner automatically wraps this function in a temporary policy object before evaluation.

```python
def simple_callback(context: ToolPolicyContext) -> bool:
    # Deny any tool that modifies state

    return not context.tool_name.startswith("write_")

# Pass directly to run_sync - no class required

result = ai.Runner.run_sync(
    agent=my_agent,
    input="Update the configuration",
    tool_policy=simple_callback
)

```

## Wiring Policies into Agent Workflows

Policies are integrated at execution time through the `Runner` class in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py). When you invoke `Runner.run_sync` or `Runner.run`, the policy is injected into the request lifecycle and evaluated immediately before the tool executes.

### Runner Integration

The runner attaches the policy and a constructed context dictionary to the request kwargs passed to the model provider. According to the source code in [`runner.py`](https://github.com/andrewyng/aisuite/blob/main/runner.py), this includes the agent name, run name, and a deep copy of the message history, ensuring the policy has full visibility into the conversation state.

```python
if tool_policy is not None:
    request_kwargs["tool_policy"] = tool_policy
    request_kwargs["tool_policy_context"] = {
        "agent_name": agent.name,
        "run_name": effective_run_name,
        "messages": copy.deepcopy(messages),
    }

```

After evaluation, the runner emits trace events—`tool.allowed`, `tool.denied`, or `tool.completed`—based on the policy decision, enabling full observability of approval workflows through the tracing system.

## Building Interactive Approval Workflows

For production applications requiring human-in-the-loop verification, aisuite provides a reference implementation in [`examples/cli/dev.py`](https://github.com/andrewyng/aisuite/blob/main/examples/cli/dev.py) demonstrating how to build interactive approval controllers.

### The ApprovalController Pattern

The `ApprovalController` class implements the `ToolPolicy` protocol to prompt users for confirmation when high-risk tools are invoked. It checks `context.tool_metadata.requires_approval` to determine whether to auto-allow low-risk operations or pause for interactive input.

```python
import aisuite as ai

class ApprovalController:
    def evaluate(self, context: ai.ToolPolicyContext) -> ai.ToolPolicyDecision:
        # Auto-approve safe tools

        if not context.tool_metadata or not context.tool_metadata.requires_approval:
            return ai.ToolPolicyDecision(allowed=True, reason="low risk")
        
        # Interactive prompt for risky operations

        print(f"\nTool: {context.tool_name}")
        print(f"Args: {context.arguments}")
        choice = input("Approve? (y/n): ")
        
        if choice.lower() == "y":
            return ai.ToolPolicyDecision(allowed=True, reason="approved by user")
        return ai.ToolPolicyDecision(allowed=False, reason="denied by user")

# Usage

approver = ApprovalController()
result = ai.Runner.run_sync(
    agent=my_agent,
    input="Delete the temporary directory",
    tool_policy=approver
)

```

This pattern allows seamless integration with external ticketing systems or Slack workflows by replacing the `input()` call with API requests to your approval infrastructure.

## Policy Decision Lifecycle and Observability

Understanding how decisions flow through the system helps with debugging and compliance auditing. When `Runner.run_sync` executes with a policy attached, the evaluation occurs in the following sequence:

1. The runner constructs a `ToolPolicyContext` containing the pending tool call details.
2. The policy's `evaluate` method is invoked, returning a `ToolPolicyDecision`.
3. If `allowed=False`, the runner skips tool execution and records a denial event.
4. If `allowed=True`, the runner proceeds to execute the tool and emits a completion event.

All decisions are stored in the trace payload via `Runner._emit_tool_event`, allowing you to retrospectively analyze which tools were blocked, approved, or escalated during agent runs. This tracing integration makes the policy framework suitable for regulated environments requiring audit trails.

## Summary

- **aisuite/agents/types.py** defines the core protocol: implement `evaluate(context: ToolPolicyContext) -> ToolPolicyDecision` to create custom policies.
- **Built-in policies** in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) provide whitelist, blacklist, and callback-based approaches for common scenarios.
- **Runner integration** allows passing either a policy instance or a plain callable to `run_sync` via the `tool_policy` parameter.
- **Trace events** capture every policy decision, enabling comprehensive logging and compliance auditing of approval workflows.
- **Interactive workflows** can be built by implementing the protocol to prompt users or integrate with external approval systems, as demonstrated in [`examples/cli/dev.py`](https://github.com/andrewyng/aisuite/blob/main/examples/cli/dev.py).

## Frequently Asked Questions

### What is the difference between AllowToolsPolicy and RequireApprovalPolicy?

**AllowToolsPolicy** uses a static whitelist of permitted tool names and automatically denies any tool not on the list, making it suitable for strict sandboxing. **RequireApprovalPolicy** delegates the decision to a dynamic callback function you provide, enabling conditional logic based on runtime context, user identity, or external risk scores rather than just tool names.

### Can I use a lambda function or simple method as a custom policy?

Yes. While the `ToolPolicy` protocol requires a class with an `evaluate` method, the `Runner` also accepts plain callables (functions, lambdas, or bound methods) via the `tool_policy` argument. The runner automatically wraps these callables in a temporary policy object that adapts boolean returns to `ToolPolicyDecision` instances.

### How do I access tool metadata when evaluating a policy?

The `ToolPolicyContext` object passed to your `evaluate` method contains a `tool_metadata` attribute populated from the tool's decorator definition in the toolkit. This metadata can include custom fields like `risk_level`, `requires_approval`, or `category`, allowing your policy to make decisions based on semantic tags rather than just tool names.

### Where are policy decisions logged for security auditing?

According to the implementation in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py), policy decisions are emitted as trace events through the `Runner._emit_tool_event` method. These events include the decision result (`allowed` or `denied`), the reason string, and the full context of the tool call, enabling you to capture a complete audit trail in your observability platform.