How to Implement Custom Tool Policies in aisuite for Approval Workflows
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, 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 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 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.
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.
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.
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. 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, 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.
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 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.
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:
- The runner constructs a
ToolPolicyContextcontaining the pending tool call details. - The policy's
evaluatemethod is invoked, returning aToolPolicyDecision. - If
allowed=False, the runner skips tool execution and records a denial event. - 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) -> ToolPolicyDecisionto create custom policies. - Built-in policies in
aisuite/agents/policies.pyprovide whitelist, blacklist, and callback-based approaches for common scenarios. - Runner integration allows passing either a policy instance or a plain callable to
run_syncvia thetool_policyparameter. - 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.
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, 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →