Concepts › Verdicts, risk and reason codes
Verdicts, risk and reason codes
This page covers the vocabulary a Doberman decision is made of: the verdict, the risk level, and the reason codes and explanation that justify anything short of a plain pass.
Every decision resolves to one of three verdicts: PASS, AUTH, BLOCK. These aren't independent flags. They form a total order, PASS < AUTH < BLOCK, and that order is the whole safety model in one sentence: nothing in the decision path is allowed to move a verdict down it. combine() takes the higher of two verdicts every time it merges guardrail results; a guardrail can escalate an action another guardrail passed, never quietly wave through one that another flagged.
Risk is a separate, parallel scale, low, medium, high, critical, carried on both the individual guardrail result and the normalized action, and raised the same way the verdict is: by taking the max. Risk isn't the whole picture of how dangerous an action is, though. Two more dimensions travel alongside it: reversibility (low/medium/high) describes how easily the action's effects can be undone, and blast radius (single/few/many/mass/unknown), part of the action's algebra classification, describes how many entities it touches. The subjective layer weighs these when it decides whether an action deserves a step-up.
A GuardrailResult is what one guardrail hands back for one action: a verdict, a risk level, a list of reason codes, and a one-line explanation. It's immutable, and it's validated the moment it's built: any verdict other than PASS is rejected unless it carries at least one reason code and a non-empty explanation. There's no code path where an AUTH or BLOCK reaches you unexplained.
Reason codes are shared constants, not free text: secret_exfiltration, destructive_command, unusual_for_deployment, lethal_trifecta, 57 in total, each snake_case, each documented with the module that raises it and what it means. See the full reason codes catalogue. The explanation string attached to a code is written to describe the rule, not the payload: "path is under a protected directory," never the path itself, and never the matched secret or command. That is a hard rule for anyone writing a guardrail.
Paths get one more layer of care before any rule compares them. A single canonicalize helper resolves ., .., and symlinks, and confines the result to the repo root. Every path-matching rule in Doberman goes through it, so there's no second comparison path a .. or a symlink could slip past.
flowchart TB SO["SecurityObject<br/><i>normalized action</i>"] AT["ActionType<br/>file_write · shell_exec ·<br/>network_request · …"] RK["Risk<br/>score + BlastRadius +<br/>Reversibility"] EC["EvalContext<br/>repo root · role · mode ·<br/>session metadata"] GR["GuardrailResult<br/>verdict + reason_codes +<br/>explanation"] V["Verdict<br/>PASS < AUTH < BLOCK<br/><i>total order</i>"] RC["ReasonCode<br/>shared constants<br/>(57 documented)"] D["Decision<br/>final record, redacted"] TO["TurnObject<br/>(turn gate input)"] AL["Algebra<br/>action classification"] SO --- AT SO --- RK SO --- AL GR --- V GR --- RC EC -- "context for" --> GR GR -- "combined into" --> D TO -. "parallel vocabulary<br/>for turns" .- SO classDef god fill:#a97e4f,color:#fff,stroke:#a97e4f class SO,V god
SecurityObject and Verdict are the most-connected symbols in the codebase; a change to either is a whole-system change.