How-to guides / Permissions and approvals
Start here
How-to guides
How-to guides
Permissions and approvals
Every tool call is checked against the agent’s permissions before it runs: allowed, sent to a person for approval, or denied.
Rules
{
"permissions": {
"default": "ask",
"rules": [
{ "tool": "fetch.*", "action": "allow" },
{ "tool": "crm.update_*", "action": "ask" },
{ "tool": "*.delete_*", "action": "deny" }
],
"approvalTimeout": "30m"
}
}
toolis an exact name or a glob (*matches anything, dots included). Names may be written as the agent registers them (crm.update_contact) or as the model sees them (crm_update_contact).- Rules are evaluated deny first, then ask, then allow; the first match in
that order wins, regardless of the order they are written in. Nothing
matches:
defaultapplies (askif unset). - Tools a rule denies are not offered to the model at all. A call that names one anyway gets an error saying it is not available, and the model can try something else.
agen deploy --validatewarns about a rule that names a server the bundle does not have.
Trust and taint
Every task has a trust: operator, trusted or untrusted. It is set
by the task’s source and only ever narrows, never raises, down a
delegation tree:
- An API token’s
trust(agen token create --trust), defaulttrusted. - A webhook trigger’s
trust(defaultuntrusted); cron tasks are alwaystrusted. - A child task started with
start_agentgets its caller’s trust at that moment — which may already be lower than the caller’s own task, through taint (below).
A tool marks its result untrusted output when it may carry content from outside the agent’s control — a page, a search result, an email:
{ "tools": { "search.web": { "untrustedOutput": true } } }
An MCP tool’s openWorldHint annotation sets this by default; the bundle
can turn it off for a server it trusts. The first time a run calls such a
tool, its trust drops to untrusted for the rest of the run (and for
anything it starts from then on); the trace gets an agen.trust.lowered
attribute naming the call.
A permission rule can condition on trust with when:
{ "tool": "*", "action": "ask", "when": { "trustBelow": "trusted" } }
This is the practical defence against the “lethal trifecta” (untrusted
input, access to private data, a way to act outward): a run that has read
something untrusted must ask before it does anything else. when.trust
matches an exact trust instead of a ceiling.
Approvals
An ask creates a durable approval and the run waits (status
waiting_approval) until someone decides, or approvalTimeout (default 1 h)
passes, which counts as denied. Approvals survive restarts: a run resumed on
another instance waits for the same approval.
agen approvals
agen approve <id>
agen deny <id>
Each approval shows the tool, its arguments (secrets redacted), the task and
run it belongs to, who asked and when it expires; a decided one shows who
decided and when. The web UI has the same list, and MCP clients use
list_approvals and decide_approval.
Nobody approves their own request. The principal whose work asked (the token that submitted the task, or the A2A caller) cannot decide its approvals. Give the approver a separate token:
agen token create --name alice-approver --scope approver
It prints the token once. With agen up, your own CLI uses the local admin
token, which submitted your tasks, so decide your agents’ approvals with the
approver token:
agen approvals
agen approve <id> --token <approver token>
A delegated run’s approvals name the person behind the root task, so delegation cannot be used to approve your own request either.
Feedback
Record a judgement on how a task went: a person’s rating, an automated
check, or a judge model’s score. RecordFeedback takes a score (0.0 worst
to 1.0 best), optional labels and a note; ListFeedback reads it back,
filtered by deployment, labels and time.
agen feedback record <task-id> --score 0.8 --note "mostly right" --label judge=rubric-v1
agen feedback ls researcher --label judge=rubric-v1 --since 24h
Every approval decision is recorded as feedback too, by: "approval":
approved is a score of 1.0, denied a score of 0.0. This gives a single place
to see how a deployment is doing, whether the signal came from a human
rating, a check, or an approval.
Notifications
To hear about an approval as soon as it is raised, give the deployment a URL:
{
"permissions": {
"default": "ask",
"notify": { "url": "https://hooks.example/agen-approvals", "secret": "APPROVAL_HOOK_KEY" }
}
}
The Hub POSTs each new pending approval as JSON
({"type": "approval.pending", "approval": {...}}, header
X-Agen-Event: approval.pending), retrying a few times on failure. With
secret, the body is signed with that platform secret:
X-Agen-Signature: sha256=<hex HMAC-SHA256 of the body>. Set the secret for
the deployment first:
printf %s "$HOOK_KEY" | agen secret set APPROVAL_HOOK_KEY --for my-agent
Failed notifications are written to the deployment’s logs (agen logs).
The Hub posts to whatever URL the bundle names, including internal
addresses, so only trusted operators should deploy bundles.
To send the approvals (and failures) of a whole namespace to one place, use a notification target instead.
Embedded agents
An agent embedded with an SDK decides approvals in your code: pass an
approval callback when you create the agent (see the SDK README). Without
one, ask is treated as deny.