Human-in-the-Loop Approvals
Route sensitive AI agent tool calls to human reviewers for approval before execution.
Human-in-the-Loop Approvals
For sensitive operations, Bastio can pause tool execution and route the request to human reviewers. This provides an additional layer of security for high-risk operations while maintaining a smooth user experience.
Overview
When a policy triggers require_approval, Bastio:
- Pauses the tool call
- Notifies configured reviewers (email, Slack, webhook)
- Waits for approval, rejection, or timeout
- Returns the decision to your agent
Your agent can then execute the tool (if approved) or return an appropriate message to the user.
Setting Up Approvals
1. Create a Policy with Approval
curl -X POST https://api.bastio.com/v1/guard/{proxyId}/policies \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Approve Financial Operations",
"tool_pattern": "payment_*",
"action": "require_approval",
"approval_config": {
"timeout_minutes": 30,
"notification_channels": ["email"],
"approval_group_id": "group_finance"
}
}'2. Configure Notification Channels
From the dashboard, configure how reviewers are notified:
- Email: Sends approval request with one-click approve/reject buttons
- Slack: Posts to a channel with interactive buttons
- Microsoft Teams: Posts adaptive card with action buttons
- Webhook: Sends JSON payload to your custom endpoint
3. Set Up Approval Groups
Create groups of reviewers with escalation rules:
curl -X POST https://api.bastio.com/v1/guard/{proxyId}/approval-groups \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Finance Team",
"members": [
{"email": "alice@company.com", "role": "primary"},
{"email": "bob@company.com", "role": "backup"}
],
"escalation_minutes": 15
}'Approval Flow
┌─────────────────────────────────────┐
│ Agent: "Process refund for $500" │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Bastio Policy Match: │
│ tool: payment_refund │
│ action: require_approval │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Notification Sent: │
│ 📧 Email to finance@company.com │
│ 💬 Slack message to #approvals │
└─────────────────────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Approved │ │ Rejected │
│ (or timeout)│ │ │
└───────────────┘ └───────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Execute Tool │ │ Block Tool │
│ Return result│ │ Return error │
└───────────────┘ └───────────────┘API Reference
Checking Approval Status
When a tool call requires approval, you receive an approval_id:
{
"action": "require_approval",
"approval_id": "apr_abc123",
"message": "This operation requires approval",
"expires_at": "2024-01-15T10:30:00Z"
}Poll for the decision:
curl https://api.bastio.com/v1/guard/{proxyId}/approvals/{approvalId} \
-H "Authorization: Bearer YOUR_API_KEY"Response:
{
"approval_id": "apr_abc123",
"status": "approved",
"approved_by": "alice@company.com",
"approved_at": "2024-01-15T10:25:00Z",
"comment": "Verified with customer"
}Approval Statuses
| Status | Description |
|---|---|
pending | Awaiting reviewer decision |
approved | Reviewer approved the operation |
rejected | Reviewer rejected the operation |
expired | Timeout reached without decision |
cancelled | Request was cancelled |
Programmatic Approval
Reviewers can also approve/reject via API:
# Approve
curl -X POST https://api.bastio.com/v1/guard/{proxyId}/approvals/{approvalId}/approve \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"comment": "Verified with customer support ticket #1234"
}'
# Reject
curl -X POST https://api.bastio.com/v1/guard/{proxyId}/approvals/{approvalId}/reject \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Suspicious activity detected"
}'Configuration Options
Timeout Settings
Configure how long to wait for approval:
| Setting | Range | Default | Description |
|---|---|---|---|
timeout_minutes | 1-1440 | 30 | Minutes before auto-expiration |
default_on_timeout | string | reject | Action if timeout: reject or approve |
Escalation Rules
Set up automatic escalation:
{
"approval_group_id": "group_finance",
"escalation_rules": [
{
"after_minutes": 15,
"escalate_to": "group_managers"
},
{
"after_minutes": 25,
"escalate_to": "group_executives"
}
]
}Approval Routing
Route different requests to different groups:
{
"routing_rules": [
{
"condition": { "risk_score_min": 0.8 },
"approval_group": "group_security"
},
{
"condition": { "tool_pattern": "payment_*" },
"approval_group": "group_finance"
},
{
"condition": { "tool_pattern": "*" },
"approval_group": "group_general"
}
]
}Code Examples
Handling Approvals in Your Agent
import asyncio
import httpx
async def validate_and_wait_for_approval(
proxy_id: str,
tool_call: dict,
max_wait_seconds: int = 1800
) -> dict:
"""Validate tool call and wait for approval if needed."""
async with httpx.AsyncClient() as client:
# Initial validation
response = await client.post(
f"https://api.bastio.com/v1/guard/{proxy_id}/tool",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"session_id": "session_123", "tool_call": tool_call}
)
result = response.json()
if result["action"] != "require_approval":
return result
# Poll for approval
approval_id = result["approval_id"]
poll_interval = 5 # seconds
elapsed = 0
while elapsed < max_wait_seconds:
response = await client.get(
f"https://api.bastio.com/v1/guard/{proxy_id}/approvals/{approval_id}",
headers={"Authorization": f"Bearer {API_KEY}"}
)
approval = response.json()
if approval["status"] == "approved":
return {"action": "allow", "approved_by": approval["approved_by"]}
if approval["status"] in ["rejected", "expired"]:
return {"action": "block", "reason": approval.get("reason", "Approval denied")}
await asyncio.sleep(poll_interval)
elapsed += poll_interval
return {"action": "block", "reason": "Approval timeout"}
# Usage
result = await validate_and_wait_for_approval("proxy_xyz", tool_call)
if result["action"] == "allow":
execute_tool(tool_call)
else:
return f"Operation not permitted: {result.get('reason', 'Approval required')}"async function validateAndWaitForApproval(
proxyId: string,
toolCall: object,
maxWaitMs: number = 1800000
): Promise<{ action: string; reason?: string }> {
// Initial validation
const response = await fetch(
`https://api.bastio.com/v1/guard/${proxyId}/tool`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
session_id: 'session_123',
tool_call: toolCall,
}),
}
);
const result = await response.json();
if (result.action !== 'require_approval') {
return result;
}
// Poll for approval
const approvalId = result.approval_id;
const pollInterval = 5000;
let elapsed = 0;
while (elapsed < maxWaitMs) {
const approvalResponse = await fetch(
`https://api.bastio.com/v1/guard/${proxyId}/approvals/${approvalId}`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const approval = await approvalResponse.json();
if (approval.status === 'approved') {
return { action: 'allow' };
}
if (['rejected', 'expired'].includes(approval.status)) {
return { action: 'block', reason: approval.reason || 'Denied' };
}
await new Promise(r => setTimeout(r, pollInterval));
elapsed += pollInterval;
}
return { action: 'block', reason: 'Approval timeout' };
}Graceful User Experience
Inform users when waiting for approval:
async def handle_tool_with_approval(tool_call, user_message_callback):
result = await validate_tool_call(proxy_id, tool_call)
if result["action"] == "require_approval":
# Inform user
await user_message_callback(
"This operation requires approval from a team member. "
"You'll be notified once it's reviewed."
)
# Wait for decision
result = await wait_for_approval(result["approval_id"])
if result["action"] == "allow":
await user_message_callback("Operation approved! Proceeding...")
return await execute_tool(tool_call)
else:
return f"Operation was not approved: {result.get('reason')}"
elif result["action"] == "allow":
return await execute_tool(tool_call)
else:
return f"Operation blocked: {result['message']}"Email Notifications
Email notifications include:
- Tool name and arguments (sanitized)
- Risk score and threats detected
- One-click buttons for approve/reject
- Link to dashboard for more context
Example email:
Subject: Approval Required: payment_refund
An AI agent has requested approval to execute:
Tool: payment_refund
Arguments: {"amount": 500, "customer_id": "cust_123"}
Risk Score: 0.65
Session: session_abc123
[Approve] [Reject] [View in Dashboard]
This request will expire in 30 minutes.Audit Trail
All approval actions are logged:
curl https://api.bastio.com/v1/guard/{proxyId}/approvals/history \
-H "Authorization: Bearer YOUR_API_KEY"{
"approvals": [
{
"approval_id": "apr_abc123",
"tool_name": "payment_refund",
"status": "approved",
"requested_at": "2024-01-15T10:00:00Z",
"decided_at": "2024-01-15T10:25:00Z",
"decided_by": "alice@company.com",
"comment": "Verified with customer"
}
]
}Best Practices
Next Steps
- Policies - Configure which operations require approval
- Agent Identity - Authenticate agents for trust-based routing
- Chain Analysis - Detect suspicious operation sequences