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 tooltool_name– The registered tool identifierarguments– Parsed arguments as a dictionarytool_metadata– Static metadata attached to the tool (e.g.,requires_approval)run_name,trace_id,parent_run_id,group_id– Execution-tracking identifierstags,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 callDenyAllToolPolicy– Blocks all calls with an optional reasonAllowToolsPolicy– Permits only tools listed in theallowed_toolswhitelistRequireApprovalPolicy– 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:
- Parses the incoming tool call and validates arguments using Pydantic
- Builds the
ToolPolicyContextwith all execution identifiers - Invokes
_evaluate_tool_policywhich:- 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
- Wraps the caller-supplied policy (or defaults to
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
ToolPolicyDecisionobjects - AllowToolsPolicy provides static allow-list functionality for simple access control
- Policy evaluation occurs in
aisuite/utils/tools.pyduring_prepare_tool_call, emittingtool.allowedortool.deniedevents - The
requires_approvaltool 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →