How to Implement Custom Tool Policies with Approval Workflows in AISuite
Use AISuite's RequireApprovalPolicy class with a custom callback function to create approval workflows that intercept tool execution and defer allow/deny decisions to external logic—whether human reviewers, Slack bots, or REST APIs.
AISuite provides a flexible tool-policy system that governs whether an AI agent may invoke a Python callable in a given context. This system centers on three abstractions defined in aisuite/agents/types.py and aisuite/agents/policies.py: policy classes that evaluate authorization, context objects that carry runtime state, and decision objects that standardize outcomes. For developers who need approval workflows—where a human or external service must sign off before sensitive operations run—the RequireApprovalPolicy class offers a plug-and-play mechanism.
Core Components of the Policy System
Understanding the building blocks helps you wire custom approval logic correctly.
ToolMetadata and the @tool Decorator
Every callable that AISuite treats as a tool is decorated with @tool, defined in aisuite/agents/policies.py. This decorator attaches ToolMetadata containing the tool's name, description, and optional policy override. The policy engine uses this metadata to look up which policy applies to a given invocation.
ToolPolicyContext and ToolPolicyDecision
The ToolPolicyContext dataclass (from aisuite/agents/types.py) carries runtime information:
tool_name: The identifier of the tool being invokedrequester: The user or principal who triggered the agentargs: The arguments passed to the toolkwargs: Keyword arguments passed to the tool
Policies return a ToolPolicyDecision with:
allowed: Boolean indicating authorizationreason: Optional string explaining the decision (useful for logging and debugging)
Policy Evaluation in the Agent Runner
aisuite/agents/runner.py implements the execution flow. Before any tool runs, the agent runner retrieves the policy (from the agent's policy attribute or the tool's metadata) and calls policy.evaluate(context). If allowed=False, the tool invocation aborts immediately with the provided reason.
Built-In Policies vs. Custom Approval Workflows
AISuite ships with four ready-made policies in aisuite/agents/policies.py:
| Policy | Behavior | Use Case |
|---|---|---|
AllowAllToolPolicy |
Allows every tool unconditionally | Development, trusted environments |
DenyAllToolPolicy |
Blocks every tool unconditionally | Maintenance mode, incident response |
AllowToolsPolicy |
Allows only tools in an explicit allow-list | Scoped agents with known toolsets |
RequireApprovalPolicy |
Defers to a user-provided callback | Approval workflows, human-in-the-loop, external authorization services |
The RequireApprovalPolicy is the extension point for custom approval workflows. Its constructor accepts a callback parameter—a callable that receives a ToolPolicyContext and returns either a boolean or a full ToolPolicyDecision.
Implementing a Human-in-the-Loop Approval Workflow
The simplest approval workflow prompts a human reviewer via the console. Replace this with your own UI, Slack integration, or ticketing system.
# my_custom_policy.py
from aisuite.agents.policies import RequireApprovalPolicy, ToolPolicyDecision
from aisuite.agents.types import ToolPolicyContext
def human_approval_callback(context: ToolPolicyContext) -> ToolPolicyDecision:
"""
Console-based approval prompt. Replace with Slack bot, email, or
external approval service in production.
"""
tool = context.tool_name
user = context.requester
print(f"\n🔒 APPROVAL REQUIRED")
print(f"User: {user}")
print(f"Tool: {tool}")
print(f"Arguments: {context.args}")
print("Approve? (y/n): ", end="")
answer = input().strip().lower()
approved = answer == "y"
return ToolPolicyDecision(
allowed=approved,
reason=f"Human reviewer {'approved' if approved else 'denied'} execution of {tool}"
)
# Export for agent configuration
approval_policy = RequireApprovalPolicy(callback=human_approval_callback)
Attach this policy to an agent by setting the policy class attribute:
# agent_definition.py
from aisuite.agents.runner import AgentRunner
from aisuite.agents.policies import tool
from my_custom_policy import approval_policy
@tool
def delete_user_account(user_id: str) -> str:
"""Permanently delete a user account. Irreversible."""
# Implementation omitted
return f"Deleted account {user_id}"
@tool
def search_documentation(query: str) -> str:
"""Search internal documentation. Read-only, safe."""
return f"Results for: {query}"
class SecureAdminAgent(AgentRunner):
"""
All tools require human approval before execution.
The policy is evaluated in aisuite/agents/runner.py before each call.
"""
policy = approval_policy
When SecureAdminAgent attempts to invoke delete_user_account, execution pauses at human_approval_callback until the reviewer responds. The search_documentation tool is equally protected—you can refine this with composite policies if needed.
Integrating External Approval Services
For production deployments, replace the console prompt with an HTTP call to an external authorization service. The callback pattern remains identical.
# external_policy.py
import requests
from aisuite.agents.policies import RequireApprovalPolicy, ToolPolicyDecision
from aisuite.agents.types import ToolPolicyContext
def service_now_approval_callback(context: ToolPolicyContext) -> ToolPolicyDecision:
"""
Submit approval request to ServiceNow or similar ITSM platform.
Expects JSON response: {"allowed": bool, "reason": str, "ticket_id": str}
"""
payload = {
"requester": context.requester,
"tool_name": context.tool_name,
"tool_args": context.args,
"tool_kwargs": context.kwargs,
"timestamp": context.timestamp.isoformat() if hasattr(context, 'timestamp') else None
}
response = requests.post(
"https://api.company.com/approvals/v1/submit",
json=payload,
headers={"Authorization": "Bearer " + get_service_token()},
timeout=30
)
response.raise_for_status()
result = response.json()
return ToolPolicyDecision(
allowed=result["allowed"],
reason=f"{result['reason']} (Ticket: {result.get('ticket_id', 'N/A')})"
)
production_policy = RequireApprovalPolicy(callback=service_now_approval_callback)
The external service can implement any business logic: manager approval chains, time-based restrictions, budget checks, or compliance verification. The policy system remains agnostic to these details—it simply awaits the boolean or ToolPolicyDecision return value.
Advanced Pattern: Per-Tool Policy Overrides
You can attach policies to individual tools via the @tool decorator's metadata, overriding the agent-wide default. This enables surgical approval requirements for dangerous operations while keeping safe tools unrestricted.
from aisuite.agents.policies import AllowAllToolPolicy, RequireApprovalPolicy, tool
from my_custom_policy import production_policy
# Safe tool: no approval needed
@tool
def list_active_sessions() -> list:
"""List currently active user sessions. Read-only."""
return fetch_sessions()
# Dangerous tool: requires external approval workflow
@tool(metadata={"policy": production_policy})
def terminate_session(session_id: str) -> str:
"""Forcibly terminate an active user session."""
# Implementation omitted
return f"Terminated {session_id}"
class MixedSecurityAgent(AgentRunner):
"""
Default policy allows all, but terminate_session has an override.
Policy resolution order: tool metadata → agent policy → fallback DenyAll.
"""
policy = AllowAllToolPolicy()
In aisuite/agents/runner.py, the agent runner checks for a policy in tool.metadata.policy before falling back to self.policy. This precedence ensures granularity without boilerplate.
Asynchronous and Non-Blocking Approval Patterns
The callback signature is synchronous, but you can bridge async approval workflows using blocking calls or by refactoring to use AISuite's async agent runners if available. For long-running approvals (hours or days), consider:
- Staging pattern: The callback creates an approval ticket, returns
ToolPolicyDecision(allowed=False, reason="Pending ticket PROD-1234"), and your application retries later with session resumption. - Notification pattern: The callback triggers a webhook and raises a specific exception that your orchestration layer catches for deferred execution.
These patterns extend the same callback interface without modifying AISuite internals.
Summary
- RequireApprovalPolicy in
aisuite/agents/policies.pyis the extension point for custom approval workflows. It accepts any callback matching theToolPolicyContext → ToolPolicyDecisionsignature. - ToolPolicyContext provides runtime invocation details: requester identity, tool name, and arguments. Use these to route approval requests to appropriate reviewers.
- Agent runner integration happens automatically in
aisuite/agents/runner.py—policies are evaluated before every tool invocation with no additional code required. - Per-tool overrides via
@tool(metadata={"policy": ...})enable fine-grained security without agent-wide restrictions.
Frequently Asked Questions
Can a single callback handle multiple approval tiers based on the tool or user?
Yes. Inspect context.tool_name and context.requester within your callback to implement routing logic. For example, route delete_* tools to senior reviewers and read_* tools to automated approval, or check organizational hierarchy via an external identity service before returning the ToolPolicyDecision.
What happens if the approval callback raises an exception?
The agent runner in aisuite/agents/runner.py propagates the exception, halting tool execution. Wrap external service calls in try/except blocks and return ToolPolicyDecision(allowed=False, reason="Approval service unavailable") for graceful degradation, or re-raise to fail fast depending on your reliability requirements.
Can approval policies be composed or chained?
AISuite does not provide a built-in composition operator, but you can implement composite logic in a single callback or nest policies manually. Define a callback that iterates through sub-policies (each itself a RequireApprovalPolicy or custom class) and aggregates decisions with AND/OR logic before returning the final ToolPolicyDecision.
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 →