Agen
How-to guides / Permissions and approvals

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"
  }
}
  • tool is 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: default applies (ask if 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 --validate warns 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), default trusted.
  • A webhook trigger’s trust (default untrusted); cron tasks are always trusted.
  • A child task started with start_agent gets 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.