# Implementing Tool Policies in aisuite: RequireApprovalPolicy and Allow/Deny Lists

> Learn to implement tool policies in aisuite using RequireApprovalPolicy and allow/deny lists. Enforce access control and dynamic approval logic with this pluggable framework.

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

---

**aisuite provides a pluggable tool policy framework that lets you enforce allow-lists, deny-lists, or dynamic approval logic via the `RequireApprovalPolicy` class and the `ToolPolicyContext` evaluation protocol.**

aisuite is Andrew Ng's Python framework for building LLM agents that execute Python functions as tools. Implementing tool policies in aisuite allows developers to intercept tool calls before execution, enforcing security boundaries through static allow-lists or dynamic callbacks that inspect arguments and metadata.

## Tool Policy Architecture and the Evaluation Protocol

Before any tool runs, aisuite constructs a **`ToolPolicyContext`** containing the full execution context. According to the source code in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py), this context includes:

- **`agent_name`** – The agent invoking the tool
- **`tool_name`** – The registered tool identifier
- **`arguments`** – Parsed arguments as a dictionary
- **`tool_metadata`** – Static metadata attached to the tool (e.g., `requires_approval`)
- **`run_name`**, **`trace_id`**, **`parent_run_id`**, **`group_id`** – Execution-tracking identifiers
- **`tags`**, **`metadata`**, **`messages`** – Optional caller-supplied data

Policy objects implement a simple protocol defined in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py) (lines 43-61):

```python
class SomePolicy:
    def evaluate(self, context: ToolPolicyContext) -> ToolPolicyDecision:
        ...

```

Policies may also be plain callables returning a **bool** or a **`ToolPolicyDecision`** directly. The decision object signals the outcome:

```python
ToolPolicyDecision(
    allowed: bool,                # True permits execution, False blocks

    reason: Optional[str] = None,
    metadata: Optional[dict] = None,
)

```

When a decision denies execution, the tool is not invoked and the framework returns an error message `"Tool call denied by policy"` to the model.

## Built-in Policy Classes in aisuite

The framework ships with four concrete implementations in [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py):

- **`AllowAllToolPolicy`** – Unconditionally permits every tool call
- **`DenyAllToolPolicy`** – Blocks all calls with an optional reason
- **`AllowToolsPolicy`** – Permits only tools listed in the `allowed_tools` whitelist
- **`RequireApprovalPolicy`** – Delegates decisions to a user-supplied callback function

The **`AllowToolsPolicy`** constructor accepts a list of tool names and rejects all others immediately:

```python
allow_policy = ai.AllowToolsPolicy(["read_file", "list_directory"])

```

## Deep Dive: RequireApprovalPolicy Implementation

The **`RequireApprovalPolicy`** offers maximum flexibility by accepting a callback that receives the full `ToolPolicyContext`. This callback can return a simple boolean or a complete `ToolPolicyDecision` with reasoning and metadata.

In [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py), the implementation wraps your callback and normalizes its output. A typical implementation inspects the tool name and arguments:

```python
def my_approval_callback(context):
    # Deny dangerous shell commands

    if context.tool_name == "run_shell" and "rm -rf" in context.arguments.get("command", ""):
        return ai.ToolPolicyDecision(
            allowed=False,
            reason="dangerous command detected",
            metadata={"policy": "deny_rm_rf"}
        )
    return True  # Approve everything else

approval_policy = ai.RequireApprovalPolicy(my_approval_callback)

```

Callbacks can also maintain state to implement temporary approvals or session-based allow-lists by capturing variables from the enclosing scope.

## How Policies Are Evaluated During Execution

The execution pipeline lives in **[`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py)** (lines 398-431). The function **`_prepare_tool_call`** orchestrates the flow:

1. Parses the incoming tool call and validates arguments using Pydantic
2. Builds the `ToolPolicyContext` with all execution identifiers
3. Invokes **`_evaluate_tool_policy`** which:
   - Wraps the caller-supplied policy (or defaults to `AllowAllToolPolicy`)
   - Calls `policy.evaluate(context)` if the method exists, otherwise calls the policy directly
   - Normalizes the result to a `ToolPolicyDecision`

If the decision is **denied**, `_prepare_tool_call` records a `tool.denied` event, sets `ctx["denied"] = True`, and populates `ctx["result"]` with an error payload. If **allowed**, it emits `tool.allowed` and proceeds to invoke the tool function. All policy events are captured in `self.last_policy_events` for tracing and audit purposes.

## Leveraging Tool Metadata for Risk-Based Approvals

Tools can declare static metadata via the `@tool` decorator, commonly using the `requires_approval` flag for write operations. The framework exposes this metadata through `context.tool_metadata` inside policy callbacks.

This integration appears in [`platform/coworker/connectors/tool_defs.py`](https://github.com/andrewyng/aisuite/blob/main/platform/coworker/connectors/tool_defs.py), where the catalog classifies tools as "read-only" or "write" based on this flag. You can exploit this pattern to auto-approve safe operations while gating dangerous ones:

```python
def approve_if_low_risk(context):
    # Auto-approve read-only tools

    if not context.tool_metadata.requires_approval:
        return True
    # Require manual approval for high-risk tools

    return ai.ToolPolicyDecision(
        allowed=False,
        reason="high-risk tool – manual approval required",
        metadata={"policy": "require_approval"}
    )

```

## Complete Implementation Examples

### Example 1: Simple Allow-List Policy

```python
import aisuite as ai

@ai.tool
def read_file(path: str) -> str:
    """Read and return file contents."""
    with open(path) as f:
        return f.read()

@ai.tool
def write_file(path: str, content: str) -> str:
    """Write content to a file."""
    with open(path, "w") as f:
        f.write(content)
    return "written"

# Only allow read operations

allow_policy = ai.AllowToolsPolicy(["read_file"])

agent = ai.Agent(
    name="assistant",
    model="openai:gpt-4o",
    tools=[read_file, write_file],
)

result = ai.Runner.run_sync(
    agent,
    "Read /etc/passwd and then write to /tmp/test.txt",
    tool_policy=allow_policy,
)

# The write operation will be blocked with "Tool call denied by policy"

```

### Example 2: Dynamic RequireApprovalPolicy with State

```python
import aisuite as ai

# Temporary one-time approvals

temporary_grants = set()

def dynamic_approval(context):
    key = (context.tool_name, tuple(sorted(context.arguments.items())))
    if key in temporary_grants:
        temporary_grants.remove(key)  # Consume the one-time grant

        return True
    
    # Auto-approve safe tools

    if not context.tool_metadata.requires_approval:
        return True
        
    return ai.ToolPolicyDecision(
        allowed=False,
        reason="requires temporary grant",
    )

# Grant permission for a specific call before execution

temporary_grants.add(("delete_file", (("path", "/tmp/temp.txt"),)))

approval_policy = ai.RequireApprovalPolicy(dynamic_approval)

agent = ai.Agent(
    name="assistant", 
    model="openai:gpt-4o",
    tools=[read_file, write_file, delete_file]
)

```

## Summary

- **ToolPolicyContext** encapsulates all execution details including tool name, arguments, and metadata, passed to every policy evaluation
- **RequireApprovalPolicy** enables dynamic decision-making via callbacks that return booleans or `ToolPolicyDecision` objects
- **AllowToolsPolicy** provides static allow-list functionality for simple access control
- Policy evaluation occurs in [`aisuite/utils/tools.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/utils/tools.py) during `_prepare_tool_call`, emitting `tool.allowed` or `tool.denied` events
- The `requires_approval` tool metadata flag integrates with policies to distinguish read-only from high-risk operations
- Denied calls return `"Tool call denied by policy"` to the LLM without executing the underlying function

## Frequently Asked Questions

### How do I create a simple allow-list for specific tools in aisuite?

Use the **`AllowToolsPolicy`** class from [`aisuite/agents/policies.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/policies.py). Pass a list of permitted tool names to the constructor and supply the instance to `Runner.run_sync()` via the `tool_policy` parameter. Any tool not in the list receives a denial decision before execution begins.

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

**`AllowToolsPolicy`** performs static name-based filtering against a whitelist and cannot inspect arguments or context. **`RequireApprovalPolicy`** delegates to a callback function that receives the full `ToolPolicyContext`, enabling dynamic decisions based on argument values, metadata flags, or external state.

### How can I access tool arguments inside a RequireApprovalPolicy callback?

The callback receives a `context` object of type `ToolPolicyContext` where **`context.arguments`** contains the parsed dictionary of parameters. For example, `context.arguments.get("command")` retrieves the command string for a shell tool, allowing you to filter based on argument content rather than just tool name.

### Where are tool policy decisions logged in the execution trace?

Policy decisions are recorded as events in `self.last_policy_events` within the tool execution context. According to [`aisuite/client.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/client.py) (lines 295-369), these events attach to the request/response payloads and are captured by tracing sinks like `InMemoryTraceSink`, enabling audit logs and UI approval cards.