Skip to main content

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.approve declares which actions need permission.
  • approval_timeout (seconds, 30-3600, default 300) caps how long Digitorn waits before auto-denying.
  • The Digitorn UI shows a pending approval dialog; the user approves or denies, then the agent continues.

When an approve-policy action fires, the turn pauses until a human resolves it in the client.

The YAML

Save as approval-bot.yaml:

app.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:
bash: {}
capabilities:
default_policy: auto # other actions auto-allowed
max_risk_level: high # accept high-risk actions
approve:
- module: bash
actions: [run] # every bash.run 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

text
> Run Bash to print "hello world".

The agent issues the tool call and the security gate intercepts:

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

In the Digitorn UI, open the pending approval dialog and click Approve (or Deny). The agent loop then continues.

Step 3 - the action executes

Final agent reply:

text
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

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:

yaml
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: bash
actions: [run] # 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

  • grant for everything read-only (filesystem read, http get, list / browse / search).
  • approve for everything that mutates state the user cares about (filesystem write/edit/delete, shell, http post/put/delete, network egress in sensitive apps).
  • deny for everything never legitimate in this app (filesystem.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.