Permission Guard
Risk-based execution control with persisted approvals and a complete audit trail.
The Permission Guard sits between the model and every side effect. It classifies a call by the risk declared on the tool, applies policy, and — when the answer is "ask" — suspends the work instead of guessing.
Risk model
Four levels, each mapping to an action:
| Risk | Default action | Typical tools |
|---|---|---|
low | auto | Read, query, discover, code skeleton |
medium | notify | Create or update a task, add an article, save a review |
high | confirm | Delete, dispatch an agent, run a task graph |
critical | deny | Blocked unless an explicit grant exists |
Per-app overrides tighten or loosen a single level (evaluated when a guard is constructed with them; the daemon currently runs with the defaults):
appPermissions: {
"some-app": { write: false, execute: false }
}
// medium → confirm (write: false)
// high → deny (write: false)
// critical → deny (execute: false)Voice execution is stricter: only read-only actions are auto-allowed, everything else requires confirmation.
Why risk is declared, not inferred
Inference from tool names and arguments has failed in a predictable direction. dispatch_agent and run_task_graph contain no risky-sounding word but spawn runs that write files and execute commands; name inference rated them low, which the default policy auto-approves. The registry therefore requires an explicit risk level for every native tool.
Approval flow
Tool call or executor permission request
→ risk read from the tool definition
→ policy: auto / notify / confirm / deny
→ confirm: persist an approval request
- action digest (what exactly will run)
- risk, scope, expiry
→ suspend the run
→ UI decision: approve / deny / remember
→ resume service continues or fails the run
→ audit entry written either wayBecause approvals are persisted, a pending decision is not lost when the daemon restarts. POST /api/approvals/expire-stale closes requests that nobody answered, and batch endpoints exist for queues that accumulated during unattended work.
Permission memories
"Remember" records a decision against the action digest, so the identical action is not asked again. It is scoped: a different path, command, or parameter set is a different digest and is asked again.
Audit trail
Every decision is written with actor, action, risk, decision, and result. Entries are queryable through GET /api/audit with actor, action, riskLevel, since, limit, and offset filters.
The audit log is separate from the event log: the audit log answers "what was allowed or denied", the event log answers "what happened during this run" (GET /api/events).
Inspecting state
GET /api/approvals— pending and recent approvalsGET /api/approvals/:id— one requestPOST /api/approvals/:id/approve/deny/rememberGET /api/audit?riskLevel=high— high-risk decisions
Next Steps
- Permissions — configure policy and app overrides
- Security Model — the surrounding layers
- Review the Audit Trail — working with logs