ADR 0045 · Permissions v2 — TOML-primary config + richer rule schema
- Status: accepted
- Date: 2026-05-31
- Supersedes (partial): ADR 0026 (settings layering) — refines write format and per-rule schema.
Context
caliban shipped v1 permissions (ADR 0020), permission modes
(ADR 0029), and layered settings (ADR 0026) with JSON as the
canonical write format. Operator feedback and a security/UX review
surfaced four classes of problems: (1) the TUI Ask modal's "always
allow / always deny" never persisted, breaking the ADR 0020 promise;
(2) the JSON permissions.{allow,ask,deny} form lost source order
and comments; (3) JSON is the wrong primary format for a Rust
project where operators expect TOML and want hand-edited config that
ports between machines; (4) there was no full management surface
(CLI or in-TUI editor) for rules.
Decision
- Restore TOML as caliban's canonical config write format at
every scope; JSON is accepted on read as a legacy/import path
(with a WARN). All caliban-owned writes — modal,
/permissionseditor,caliban permsCLI — emit TOML. - Replace the three-bucket
permissions.{allow,ask,deny}form with an ordered[[permissions.rules]]array of objects carryingpattern,action, optionalcomment, optionalreason(deny-only, seen by the model), and reservedexpires_at. First match wins. The three-bucket form still loads (legacy compat) but normalizes into the ordered array on load. - Extend pattern grammar: globstar
**, path normalization for file-edit tools,Bash:~globanywhere-match, dotted-key MCP arg accessors. - Modal writeback (P1): y / n opens a sub-prompt with narrow-default suggestions, a scope picker, and an optional comment/reason. Atomic flock-protected TOML append.
- Active management surface:
/permissionsoverlay grows full editor capabilities;caliban permsCLI provides headlesslist / test / explain / add / remove / import / export / audit / lint. - Hardening:
permissions.enforcelockdown knob, append-only JSONL decision log under$XDG_STATE_HOMEwith size-based rotation, always-visible bypass-latch chip withctrl+shift+bdrop keybind.
Consequences
- Positive: matches Rust ecosystem norms; comments and source-order survive; the modal's promise is finally honored; operators have a complete management story (TUI + CLI); enforce + audit log close long-standing security gaps.
- Negative: doubles the schema surface during the compat window (legacy JSON + TOML buckets + v2 ordered rules coexist on read); the matcher gets a denser grammar (more to document).
- Compat window: legacy reads continue for two minor releases; writes deprecate immediately. After three minor releases only the canonical TOML schema loads.
Runtime application semantics
Rules added through the Ask modal's "Always allow/reject" are applied to
the running session immediately: the gate and the TUI share one
RuntimeRuleStore, so the just-added rule gates the next matching tool
call without re-prompting — regardless of which scope it is also persisted
to on disk.
Rule removals and out-of-band file edits are intentionally not
hot-reloaded into a running session. Deleting a file-scoped rule via the
/permissions overlay or caliban perms remove updates the on-disk file
but does not retroactively tighten the live gate; the change takes effect
on the next session start. Deleting a session (runtime) rule with [d]
in the overlay does take effect live, because it mutates the in-memory
store directly. This asymmetry keeps the gate cheap — no per-call disk
re-read or file watcher — while making the common "allow this now" gesture
feel instant.
Revisit if
- Operators report concrete cases where the
~globor dotted-key grammars are insufficient — next step would be a richer expression language or a classifier-graded gate (already deferred via ADR 0029 auto-mode). - The bypass-latch chip + drop keybind UX proves footgunny — could promote the drop to a confirmation dialog.