Implementing Allow and Deny Tool Policies in aisuite: A Complete Security Guide

aisuite’s tool-policy framework lets you control which Python functions an LLM may invoke by implementing whitelist, blacklist, or human-in-the-loop approval workflows through a simple protocol-based interface.

The aisuite library provides a unified abstraction for building LLM agents across multiple providers, and implementing allow and deny tool policies in aisuite gives developers granular control over autonomous tool execution. By leveraging the ToolPolicy protocol and built-in security classes, you can enforce strict boundaries on agent capabilities without modifying underlying model configurations.

Core Tool Policy Architecture

The policy system centers on three components defined in aisuite/agents/types.py: the context object carrying runtime metadata, the decision object containing the verdict, and the protocol that binds them together.

ToolPolicyContext and ToolPolicyDecision

The ToolPolicyContext dataclass encapsulates everything a policy needs to evaluate a request, including the tool_name, args, and caller information. The ToolPolicyDecision dataclass returns the evaluation result with an allowed boolean and an optional reason string that communicates block decisions back to the LLM.

The ToolPolicy Protocol

Any object implementing the ToolPolicy protocol must provide an evaluate(context) method that accepts a ToolPolicyContext and returns either a boolean or a ToolPolicyDecision. This contract, defined in aisuite/agents/types.py, ensures that custom policies integrate seamlessly with the runner execution flow.

Built-in Policy Implementations

The concrete security policies reside in aisuite/agents/policies.py and are re-exported through aisuite/__init__.py for convenient access. These cover the four primary access control patterns:

AllowToolsPolicy (Whitelist)

The AllowToolsPolicy class enforces strict whitelisting by accepting an allowed_tool_names list. Only tools explicitly named in this array receive execution permission, making it ideal for read-only or limited-capability agents.

import aisuite as ai

# Whitelist only the `read_file` tool

policy = ai.AllowToolsPolicy(allowed_tool_names=["read_file"])

agent = ai.Agent(
    name="file-helper",
    model="openai:gpt-4o",
    instructions="Help the user read files but never write them.",
    tools=[*ai.toolkits.files(root=".")],   # provides read_file, write_file, …

)

result = ai.Runner.run(agent, "Show me the README", tool_policy=policy)

print(result.final_output)      # succeeds – read_file is allowed

DenyAllToolPolicy (Sandbox Mode)

For completely restricted environments, DenyAllToolPolicy blocks every tool invocation. It accepts a customizable reason parameter that propagates to the LLM when calls are rejected, effectively creating a sandboxed chat mode.

import aisuite as ai

policy = ai.DenyAllToolPolicy(reason="Tool calls are disabled for this run")

result = ai.Runner.run_sync(
    agent=ai.Agent(
        name="no-tools",
        model="anthropic:claude-3.5-sonnet",
        instructions="Answer questions without invoking any tools.",
        tools=[*ai.toolkits.files(root=".")],
    ),
    prompt="What is the size of this repository?",
    tool_policy=policy,
)

print(result.final_output)   # The LLM will be told that tool calls are blocked

RequireApprovalPolicy (Human-in-the-Loop)

The RequireApprovalPolicy delegates decisions to a user-provided callback, enabling dynamic approval workflows. The callback receives the ToolPolicyContext and must return a boolean or ToolPolicyDecision, allowing for console prompts, GUI dialogs, or external approval services.

import aisuite as ai

def approve(context: ai.ToolPolicyContext) -> ai.ToolPolicyDecision:
    # Simple console prompt – in a real UI you could pop up a dialog.

    answer = input(f"Allow tool `{context.tool_name}` with args {context.args}? (y/n) ")
    allowed = answer.lower().startswith("y")
    return ai.ToolPolicyDecision(allowed=allowed,
                                 reason="user approved" if allowed else "user denied")

policy = ai.RequireApprovalPolicy(callback=approve)

result = ai.Runner.run(
    agent=ai.Agent(
        name="interactive-agent",
        model="openai:gpt-4o",
        instructions="You may call tools, but the user must approve each call.",
        tools=[*ai.toolkits.files(root=".")],
    ),
    prompt="Summarize the contents of LICENSE.txt",
    tool_policy=policy,
)

print(result.final_output)

AllowAllToolPolicy (Default)

When no policy is specified, Runner implicitly uses AllowAllToolPolicy, which permits all tool calls without restriction. Explicitly passing this policy makes the default explicit in your code, useful for prototyping before implementing stricter controls.

import aisuite as ai

result = ai.Runner.run_sync(
    agent=ai.Agent(
        name="quick-demo",
        model="openai:gpt-4o-mini",
        instructions="Feel free to call any provided tools.",
        tools=[*ai.toolkits.files(root=".")],
    ),
    prompt="List all .py files in the repo",
    tool_policy=ai.AllowAllToolPolicy(),
)

print(result.final_output)

Policy Integration in the Runner

The enforcement point occurs in aisuite/agents/runner.py, where the Runner.run and Runner.run_sync methods accept an optional tool_policy argument. The execution flow follows this sequence:

  1. The LLM generates a tool call request during agent execution.
  2. The runner constructs a ToolPolicyContext containing the tool name, arguments, and caller metadata.
  3. The policy's evaluate() method receives this context and returns a decision.
  4. If the decision's allowed flag is True, the tool executes and its result feeds back to the model.
  5. If False, the runner raises a ToolPolicyError and the LLM receives a blocking notification.
  6. The final RunResult includes complete intermediate_messages containing any blocked attempts for audit purposes.

This architecture ensures that policy checks occur immediately before function execution, preventing unauthorized operations without affecting the agent's reasoning loop.

Summary

  • The ToolPolicy protocol in aisuite/agents/types.py requires a single method: evaluate(context) -> bool | ToolPolicyDecision.
  • Four built-in policies in aisuite/agents/policies.py cover whitelist (AllowToolsPolicy), blacklist (DenyAllToolPolicy), human approval (RequireApprovalPolicy), and unrestricted (AllowAllToolPolicy) access patterns.
  • The Runner class automatically consults policies before executing any tool function, raising ToolPolicyError for blocked attempts.
  • Blocked tool calls appear in RunResult.intermediate_messages, providing complete audit trails for security compliance.
  • Custom policies require only protocol adherence, making it trivial to implement business-specific logic such as time-based restrictions or argument validation.

Frequently Asked Questions

What happens when a tool policy denies a function call?

When a policy returns allowed=False, the runner raises a ToolPolicyError and injects a blocking notification into the conversation history. The LLM receives a message indicating the tool was rejected, and the blocked attempt appears in the intermediate_messages of the final RunResult for complete auditability.

Can I implement custom business logic in a tool policy?

Yes. Any class implementing the evaluate(context) method that returns a boolean or ToolPolicyDecision satisfies the ToolPolicy protocol defined in aisuite/agents/types.py. The framework passes a ToolPolicyContext containing the tool name, arguments, and caller metadata, enabling complex decisions based on user roles, time of day, or argument content validation.

How do I apply different policies to different agent runs?

Pass the desired policy instance to the tool_policy parameter of Runner.run() or Runner.run_sync(). Each execution operates independently, allowing you to use DenyAllToolPolicy for untrusted inputs and AllowToolsPolicy for verified workflows without modifying the underlying Agent definition.

Where can I find reference implementations and tests?

The test suite in tests/agents/test_tool_policy.py demonstrates each built-in policy's behavior, while tests/agents/test_tool_metadata_policy.py covers edge cases for context inspection. These files provide concrete examples of policy instantiation and expected ToolPolicyDecision structures.

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 →