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

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, 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 (lines 43-61):

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:

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:

  • 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:

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, the implementation wraps your callback and normalizes its output. A typical implementation inspects the tool name and arguments:

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 (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, 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:

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

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

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 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. 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →