Security 1 - Human-in-the-loop approval
A capability grant says "the agent may call this without
asking". A capability deny says "never". Between the two sits
approve: the agent may try, but the call pauses until a
human (or a programmatic supervisor) authorises it. The pause is
synchronous - the agent loop blocks on the approval queue, the
turn waits, and the user sees a pending dialog in the client.
This is the default story for shell access, destructive operations, outbound network calls in regulated apps. The agent stays useful but can't act on its own when the consequences matter.
How it works
Three pieces compose:
tools.capabilities.approvedeclares which actions need permission.approval_timeout(seconds, 30-3600, default 300) caps how long the daemon waits before auto-denying.- The approval queue is exposed at
GET /api/apps/{app_id}/approvalswith a pairedPOST /api/apps/{app_id}/approvefor resolution.
When an approve-policy action fires, the security gate
gate4_policy raises ApprovalRequiredError. The daemon
enqueues the request, the agent loop suspends, and clients
listening on the /events Socket.IO room see an
approval_pending event. A subsequent approve (or deny) call
unfreezes the loop and the agent resumes.
The YAML
Save as approval-bot.yaml:
app:
app_id: approval-bot
name: Approval Bot
version: "1.0"
runtime:
mode: conversation
workdir_mode: auto
max_turns: 6
timeout: 120
agents:
- id: main
role: assistant
brain:
provider: deepseek
model: deepseek-chat
backend: openai_compat
credential:
ref: deepseek_main
scope: per_user
provider: deepseek
config:
api_key: "{{env.DEEPSEEK_API_KEY}}"
base_url: https://api.deepseek.com/v1
temperature: 0
max_tokens: 256
system_prompt: |
You can run Bash commands. Be concise. If a command is
approved and executes, summarise its output in one
sentence.
tools:
modules:
shell: {}
capabilities:
default_policy: auto # other actions auto-allowed
max_risk_level: high # accept high-risk actions
approve:
- module: shell
actions: [bash] # every Bash call needs OK
reason: "Shell commands need explicit approval before running."
approval_timeout: 60 # auto-deny after 60 s
The approve block is the only meaningful change. Everything else
is the standard chat scaffolding from the basic tutorials.
Live transcript
Sample transcript. The user asks the agent to run Bash; an SDK supervisor polls the approval queue and confirms.
Step 1 - the agent attempts Bash
> Run Bash to print "hello world".
The agent issues the tool call and the security gate intercepts:
# captured by GET /api/apps/approval-bot/approvals
{
"request_id": "81dc71da-b5d3-40e5-a49e-d11a441e882e",
"agent_id": "main",
"user_id": "e11e6e81e6864de9b654e02d309cc28a",
"app_id": "approval-bot",
"session_id": "<sid>",
"tool_name": "shell.bash",
"tool_params": {
"command": "echo \"hello world\"",
"description": "Print hello world"
},
"risk_level": "high",
"reason": "Shell commands need explicit approval before running."
}
The agent loop is paused on this request. No tokens get billed, no further LLM call happens until the queue resolves.
Step 2 - the supervisor approves
The Python testing SDK has a one-line helper:
client.approve("approval-bot", "81dc71da-b5d3-40e5-a49e-d11a441e882e")
# → True
A web client posts the equivalent JSON to
POST /api/apps/approval-bot/approve with
{"request_id": "...", "approved": true}. Either way the
daemon unfreezes the agent loop.
Step 3 - the action executes
# captured tool_call event after approval
{
"name": "Bash",
"params": {"command": "echo \"hello world\"", "description": "Print hello world"},
"success": true,
"result": {"stdout": "hello world\n", "exit_code": 0}
}
Final agent reply:
Printed "hello world" successfully.
tool_calls_count: 1, one Bash call, real output observed
end-to-end. The session went user → pending approval →
human-in-the-loop OK → action runs → reply with no other tool
calls.
Denying instead of approving
client.deny("approval-bot", request_id, reason="too risky in this session")
The agent receives a permission_denied error in place of the
tool result and the next turn picks an alternative path - or
gives up and tells the user it was blocked. Either response is
fine; the system prompt usually frames the fallback.
Timeout behaviour
Set approval_timeout to bound the wait. The daemon emits an
auto_denied event once the deadline lapses; the agent receives
the same denial response it would have got from an explicit
deny. Useful for unattended sessions: a cron-driven agent that
hits an approve-only tool at 03:00 fails fast instead of holding
a worker forever.
The minimum is 30 s, the maximum is 3600 s. Lower is better for interactive apps; higher is right for human-review flows where a real reviewer is on shift.
Mixing grant / approve / deny
The three policies compose by action. A typical production shape:
tools:
capabilities:
default_policy: block
grant:
- module: filesystem
actions: [read, glob, grep] # safe, auto-allowed
approve:
- module: filesystem
actions: [write, edit] # mutations need OK
- module: shell
actions: [bash] # any shell command needs OK
deny:
- module: filesystem
actions: [] # would deny everything
Resolution order is deny > approve > grant > default_policy.
The first match wins. An action listed in deny is unreachable
even if a grant row also names it. An action listed in
approve requires confirmation even if a wildcard grant would
have allowed it.
When to use which
grantfor everything read-only (filesystem read, http get, list / browse / search).approvefor everything that mutates state the user cares about (filesystem write/edit/delete, shell, http post/put/delete, network egress in sensitive apps).denyfor everything never legitimate in this app (workspace.delete on a builder, http on an offline app, shell on a research-only assistant).
The approve flow is the difference between a polished agent
and a runaway one. Adding it to a single dangerous action makes
the agent feel cooperative; adding it to everything makes it
feel paralysed - calibrate.
Going further
- The full security reference covering all seven gates: Security architecture.
- Programmatic approval workflows (channels module → approval queue, scheduled triggers + auto-approval logic): Channels.
- The behaviour engine adds a different kind of guardrail (rule-based, not approval-based): Advanced 4 - Behavior engine.