How to Add Human Approval to AI Agent Actions
Source: Dev.to
Introduction
Your AI agent might perform dangerous actions—like deleting a production database table—without any confirmation.
If your agent can send emails, write files, or call APIs, you should add a human‑approval gate for risky operations. Below is a pure‑Python implementation of such a gate.
Approval Gate Implementation
from enum import Enum
from datetime import datetime
class Risk(Enum):
READ = "read"
WRITE = "write"
DESTRUCTIVE = "destructive"
TOOL_RISK = {
"search_docs": Risk.READ,
"list_files": Risk.READ,
"send_email": Risk.WRITE,
"create_file": Risk.WRITE,
"delete_file": Risk.DESTRUCTIVE,
"run_sql": Risk.DESTRUCTIVE,
"deploy": Risk.DESTRUCTIVE,
}
def approve(tool_name: str, args: dict) -> bool:
"""Return True if the action is allowed, otherwise ask for human approval."""
risk = TOOL_RISK.get(tool_name, Risk.DESTRUCTIVE)
if risk == Risk.READ:
return True
if risk == Risk.WRITE:
print(f"[LOG] {datetime.now():%H:%M:%S} | {tool_name}({args})")
return True
# Destructive: require human approval
print(f"\n{'='*50}")
print(f"APPROVAL REQUIRED: {tool_name}")
print(f"Arguments: {args}")
print(f"Risk level: {risk.value}")
print(f"{'='*50}")
response = input("Execute this action? (y/n): ").strip().lower()
return response == "y"
def safe_tool_call(tool_name: str, args: dict, tool_fn):
"""Wrap a tool function with the approval gate."""
if not approve(tool_name, args):
return {"status": "blocked", "reason": "Human denied execution"}
return tool_fn(**args)
# Example tool: delete a file
def delete_file(path: str) -> dict:
# os.remove(path) # uncomment in production
return {"status": "deleted", "path": path}
# Run the example
result = safe_tool_call(
tool_name="delete_file",
args={"path": "/data/user_exports.csv"},
tool_fn=delete_file,
)
print(result)Running the Example
python approval_gate.pySample output when the agent tries to delete a file:
==================================================
APPROVAL REQUIRED: delete_file
Arguments: {'path': '/data/user_exports.csv'}
Risk level: destructive
==================================================
Execute this action? (y/n): n
{'status': 'blocked', 'reason': 'Human denied execution'}Typing n blocks the action; typing y allows it to proceed. Read‑only operations skip the prompt entirely.
Risk Tiers
| Tier | Description | Default handling |
|---|---|---|
| READ | Non‑destructive queries (e.g., search_docs, list_files) | Auto‑approved silently |
| WRITE | Actions that modify state but are not destructive (e.g., send_email, create_file) | Auto‑approved and logged |
| DESTRUCTIVE | Operations that can cause data loss or major changes (e.g., delete_file, run_sql, deploy) | Require explicit human approval |
Tools not listed in TOOL_RISK default to the DESTRUCTIVE tier, providing a safe‑by‑default fallback.
Integrating the Gate with an Agent
If your agent selects tools from a dictionary, wrap each call with safe_tool_call:
TOOLS = {
"search_docs": search_docs,
"send_email": send_email,
"delete_file": delete_file,
# add other tools here
}
for step in agent.run(task):
tool_fn = TOOLS[step.tool_name]
result = safe_tool_call(step.tool_name, step.args, tool_fn)
agent.receive(result)All tool invocations now pass through the approval gate:
- READ tools execute immediately.
- WRITE tools execute and produce a log entry.
- DESTRUCTIVE tools pause for human confirmation.
Production Considerations
- Replace
input()with a webhook, Slack message, or email notification that pauses execution until an authorized user approves. - The core pattern remains unchanged; only the transport mechanism for the approval changes.
Related Tip
If you prefer a ready‑made solution, Nebula provides built‑in gates for destructive actions and sends you an email notification before execution.
Part of the AI Agent Quick Tips series. Previous tip: How to Add Retry Logic to LLM Calls in 5 Min.