Bastio
Agent Security

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:

  1. Pauses the tool call
  2. Notifies configured reviewers (email, Slack, webhook)
  3. Waits for approval, rejection, or timeout
  4. 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

StatusDescription
pendingAwaiting reviewer decision
approvedReviewer approved the operation
rejectedReviewer rejected the operation
expiredTimeout reached without decision
cancelledRequest 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:

SettingRangeDefaultDescription
timeout_minutes1-144030Minutes before auto-expiration
default_on_timeoutstringrejectAction 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