LoopEngineBETA

Docs

Documentation

How LoopEngine works, from scaffold to durable approvals — agents, adapters, permission rules, and abilities.

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.