Tool Approval
Dangerous tools require explicit user approval before execution. This prevents the agent from running destructive commands, overwriting files, or sending messages without the user's consent.
Gated Tools
| Tool | Function | What's gated |
|---|---|---|
| BashTool | bash_execute | All executions |
| WriteFileTool | write_file | All writes |
| EditFileTool | edit_file | All edits |
| SocialMessageTool | social_message | Sending messages (listing contacts is ungated) |
User Experience
When a gated tool is invoked, the chat UI pauses and shows an approval dialog:
- Allow — approve this single execution, resume the tool
- Always Allow — approve and remember for this chat (no further prompts for this tool in this chat)
- Deny — block execution, the tool returns
[Tool execution denied by user.]
The agent sees the denial message and can adjust its approach (e.g. ask the user for an alternative).
Chat-Scoped Memory
Clicking "Always Allow" sets a chat-scoped policy:
tool_approval_policy["bash_execute"] = "always_allow"
This persists for the lifetime of the current chat. Subsequent calls to the same tool skip the approval dialog entirely. The policy:
- Persists across multiple messages within the same chat
- Persists across page refreshes (stored in database per-chat)
- Resets when you create a new chat or switch to a different chat
- Is isolated per chat — approvals in Chat A don't affect Chat B
Each chat maintains its own independent set of tool approval policies, giving you fine-grained control over which tools are auto-approved in different contexts.
How It Works
Architecture
The HITL system uses a queue-based streaming architecture:
Agent Task (background)
│
├── Tool calls _require_approval()
│ │
│ ├── Check session policy → auto-approve/deny if set
│ ├── Push "tool_approval_required" to SSE queue
│ └── await asyncio.Event (blocks tool execution)
│
SSE Generator (drains queue)
│
├── Yields regular stream events
└── Yields tool_approval_required event → Frontend
│
User clicks
│
POST /chat/approve-tool
│
asyncio.Event.set() → Tool resumes
The agent runs in a background asyncio.Task. When a tool needs approval, it pushes a request to the SSE queue and waits on an asyncio.Event. The SSE generator delivers the approval request to the frontend. When the user responds, the HTTP endpoint sets the event, unblocking the tool.
SSE Event
{
"type": "tool_approval_required",
"data": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"tool_name": "bash_execute",
"args_preview": {
"content": "rm -rf /tmp/old-data",
"language": "command"
}
}
}
API Endpoint
POST /chat (with resume_approvals)
{
"chat_id": "...",
"message": "",
"config": {
"tool_approval_policy": {
"bash_execute": "always_allow" // Updated when remember: "session" is set
}
},
"resume_approvals": [
{
"request_id": "...",
"tool_call_id": "...",
"approved": true,
"remember": "session", // or null for one-time approval
"tool_name": "bash_execute"
}
]
}
Note: The /chat/approve-tool endpoint is deprecated. Approvals are now batched and sent via the main /chat endpoint with the updated config.
Timeout & Cancellation
- Timeout: If the user doesn't respond within 5 minutes, the tool is auto-denied.
- Cancel: If the user stops the stream while approval is pending, all pending approvals are auto-denied.
- Cleanup: On stream end, the
active_depsregistry is cleared.
Adding HITL to a New Tool
To gate a new tool behind approval:
- Add the tool name to
TOOLS_REQUIRING_APPROVALintool_functions.py:
TOOLS_REQUIRING_APPROVAL = frozenset({
"bash_execute",
"write_file",
"edit_file",
"social_message",
"my_new_tool", # ← add here
})
- Make the tool function
asyncand call_require_approval():
async def my_new_tool(ctx: RunContext[AgentDeps], param: str) -> str:
"""Tool description."""
if not await _require_approval(ctx, "my_new_tool", {"param": param}):
return "[Tool execution denied by user.]"
# ... proceed with tool logic ...
The frontend automatically handles the approval UI for any tool that emits a tool_approval_required event — no frontend changes needed.
Non-Streaming Mode
In contexts where HITL is not available (cron jobs, social messaging responses, headless mode), tools auto-approve. This is detected by the absence of sse_queue on AgentDeps.