ActAuth & Permission
This page is the internal module map — permission gating and its human-in-the-loop escalation. For the design itself — live vs. durable approvals, notification channels, how resumption actually works — see Human in the Loop instead; this is where those pieces live in the source tree.
Governed by actauth, an external dependency — allow/ask/deny rules
with scope matching, not something hand-rolled per project. A paused
turn resumes from a durable checkpoint (core/durable-approvals.ts,
file-backed for local dev, Redis for multi-instance), mirroring the
session store's own load-run-persist shape — see Sessions &
Knowledge.
core/http-notifier.ts resolves how a pending approval actually
reaches a human — see HTTP Notifier for the six
channel classes it dispatches to.
The actauth.yml fields
default_decision: deny
rules:
- name: refund-acme-corp-production-allowed
scope: acme-corp/production
tool: issue_refund
decision: allow
- name: send-email-tracking-info-allowed
scope: "*/*"
tool: send_email
when: { field: intent, op: eq, value: tracking_info }
decision: allow
| Field | Where | Meaning |
|---|---|---|
default_decision |
Top-level | allow/ask/deny — applies when no rule below matches at all. Optional: defaults to 'ask' for an inline AgentConfig.rules array, but 'deny' when an agent has no actauth.yml file at all (a stricter fallback — an unwritten permission story shouldn't quietly behave like a written, permissive one). |
rules |
Top-level | An ordered list of rules, most-specific-scope-first regardless of declaration order (see Specificity below); ties keep declaration order. |
name |
Per rule | Optional label — shows up as matchedRule in an evaluation result, and disambiguates two same-specificity rules in a comment/log, nothing more. |
scope |
Per rule | A tenant/environment glob pattern (loopengine auto-appends /<agentName> for the per-agent actauth.yml file form — see Multi Tenant). * matches any run of characters, ? matches exactly one — same semantics as Python's fnmatch. |
tool |
Per rule | The exact tool name this rule governs — no globbing here, unlike scope. |
decision |
Per rule | allow, ask, or deny. (A fourth value, pending, exists on the Decision type but is only ever produced at runtime by a DurableApprover — never something you write into a rule yourself.) |
when |
Per rule, optional | A condition on the tool call's own args. Omitted means the rule always applies once scope/tool match. |
when.field |
Inside when |
Dot-path into args (e.g. intent). A field missing from args evaluates the condition to false — it can't be proven true about data that isn't there. |
when.op |
Inside when |
One of eq, ne, gt, gte, lt, lte, in, contains. |
when.value |
Inside when |
The comparison value. |
when.all / when.any |
Inside when |
Compose several conditions instead of one leaf {field, op, value} — every/any of a nested Condition[] must hold. |
Specificity: a rule's rank is how many of its scope segments aren't * — acme-corp/production (2) always beats */production (1), which always beats */* (0), regardless of file order. resolve() walks rules most-specific-first and returns the first one where tool matches exactly, the glob-matched scope matches this call's actual tenant/environment/agent, and when (if present) evaluates true against args — falling through to default_decision if nothing matched at all.