Skip to content

ADR-0004: Tiered authority T0-T3 made load-bearing

Field Value
Status Accepted
Date 2026-06-07
Authors Roman Mednitzer

Context

In the prototype, "graduated autonomy" was a design note: a human read a tier label and decided. Nothing in code enforced it, so an irreversible action could run with the same ceremony as a read. STPA hazard H-3 (tier under-rating) and loss L-1 (unauthorized privileged command) trace directly to this gap.

Decision

  1. Four tiers, ordered, each with a fixed gate:
  2. T0 observe: read-only, no approval.
  3. T1 reversible: act, log, notify; no human confirm.
  4. T2 stateful: human confirm with a rollback plan recorded.
  5. T3 irreversible: two-step confirm with a typed confirmation token plus before/after evidence; one target at a time.
  6. classify(tool, command) is conservative and rounds up: on ambiguity it returns the higher tier. Any command containing sudo, doas, or pkexec is at least T2. The classifier lives in execution/patterns.py, the sole security-review file, behind a PATTERNS_VERSION counter.
  7. Three modes gate which tiers may run: open (all tiers, subject to their gates), guarded (T0-T2; T3 refused), readonly (T0 only). The mode is set at server start; tools cannot raise their own ceiling.
  8. The deny list is global and unconditional. It is checked first, before tier gating, and applies in every mode including open. A denied pattern is never executed regardless of approval.
  9. Tier classification, mode gating, and the deny list are enforced inside the single execution path (ADR-0005), so no tool can opt out.

Consequences

Positive: autonomy is enforced, not advised; ambiguity fails safe (up, not down); a misclassified-low action cannot slip a gate; an operator can drop the whole server to readonly or guarded without editing tools.

Negative: conservative rounding produces occasional false-high classifications that ask for confirmation when strictly unnecessary; that is the intended trade-off.

Neutral: the tier of a given command can change as patterns.py evolves; PATTERNS_VERSION makes that change reviewable and auditable.

Alternatives considered and rejected

  • Per-tool static tier labels only. Rejected: the same tool (a shell) spans tiers depending on the command; classification must read the command, not just the tool name.
  • Allow-list instead of deny-list-first. Rejected: an allow-list is the right shape for capabilities but cannot express "never, in any mode"; the global deny list is the unconditional floor beneath the mode gates.

Revisit triggers

  • A fifth tier or a sub-tier is needed.
  • Mode semantics must vary per client (multi-operator; out of v0 scope).

Audit note (2026-06-08, ADR-0015)

Decision 2 states that classification rounds up. In the v0 code this round-up only raises the tier when the command string matches a deny or tier pattern; an unmatched command stays at its declared base tier. Because the SSH adapter accepts a free-form remote command at base_tier=T1, a destructive command the patterns do not recognise can execute at T1 without approval, so the round-up is not a completeness guarantee for free-form shell. Flooring arbitrary execution at T2 and widening the patterns is proposed in ADR-0015 and tracked as BL-073. This note records the gap; it does not amend the decision.