# The decision path

This page walks through what happens to a single tool call, from the moment your agent issues it to the moment it either runs or doesn't, and the two rules that path can never break.

Doberman sits between your agent and every tool it can reach. Two front doors exist: a host hook inside the agent's harness, and an MCP proxy in front of a tool server. Whichever one intercepts the call, it normalizes the raw arguments into one shared record, a `SecurityObject` (paths canonicalized, the action classified by type). From that point on, both doors run the same path through one decision engine.

The engine evaluates the `SecurityObject` in a fixed order. The [objective guardrail](https://docs.trydoberman.dev/concepts/objective-guardrail/) runs first: a set of deterministic rules over the action (protected paths, destructive commands, external destinations, and more). If it returns anything other than `PASS`, that verdict is final and the [subjective layer](https://docs.trydoberman.dev/concepts/subjective-layer/) never runs, so it can neither weaken nor even observe an objective `AUTH` or `BLOCK`. Only when the objective guardrail passes does the subjective layer score the action for how unusual it is in this deployment, and that score can only push the verdict up, never down.

Once both layers have spoken, a small set of hard floors run: a taint check for a secret read earlier in the session that's now leaving in this call, a lethal-trifecta check for private data, untrusted input, and an outbound channel occurring together, and a tool-pin check for a tool whose contract changed since Doberman first saw it. None of these floors can be muted by an approval, a scope token, or learned familiarity.

- `PASS`: the call goes to the real tool. Its output is still scanned after the fact.
- `AUTH`: you get a challenge, GUI first, falling back to a terminal prompt, then the CLI. Approval is single-use and bound to this one action. A denial, a timeout, or a broken channel all deny by default.
- `BLOCK`: the agent gets a refusal carrying reason codes and a one-line explanation. The tool never runs, and nothing downstream records the attempt.

Every decision, whatever the verdict, is written to the local decision log: redacted, fingerprints only. Logging never changes a decision and never crashes the call it's logging.

```mermaid
sequenceDiagram
  autonumber
  participant A as Agent
  participant F as Front door<br/>(hook / proxy)
  participant E as Decision engine
  participant O as Objective rules
  participant S as Subjective layer
  participant H as Human (auth)
  participant L as Storage (log)
  participant T as Tool
  A->>F: tool call
  F->>F: normalize → SecurityObject<br/>(canonicalize paths, classify action)
  F->>E: decide(SecurityObject)
  E->>O: evaluate each rule
  O-->>E: GuardrailResult × N
  E->>S: abnormality score
  S-->>E: raise-only adjustment
  E->>E: combine (never lowers a verdict)<br/>+ floors: taint · trifecta · tool-pin
  alt PASS
    E->>T: execute
    T-->>A: result (output scanned post-hoc)
  else AUTH
    E->>H: challenge (GUI → TTY → CLI fallback)
    alt approved (single-use, action-bound)
      H-->>E: approve
      E->>T: execute
    else denied / timeout / channel error
      H-->>E: deny (fail closed)
      E-->>A: refusal + reason codes
    end
  else BLOCK
    E-->>A: refusal + reason codes + explanation
    Note over T: downstream records nothing
  end
  E->>L: record decision (redacted, fingerprints only)
```
One tool call, end to end. The agent never proceeds until the verdict resolves.

Two invariants hold this whole path together. Guardrails and the subjective layer can tighten automatically, but nothing on this path can loosen a verdict on its own; any deliberate loosening goes through the human-approved [drift gate](https://docs.trydoberman.dev/concepts/policy-and-drift/) instead.

> **Fail closed** Any error, any uncertainty, any case Doberman doesn't recognize denies the action. There is no route from the agent to a real tool that skips the decision engine.

## What combine() guarantees

`combine()` is the function that turns two guardrail opinions into one. It takes the higher of the two verdicts (`PASS < AUTH < BLOCK`), the higher of the two risk levels, and the union of their reason codes. Nothing in it can hand back a verdict or a risk lower than either input: the only operations it uses are `max()` over an ordering and a set union.

The engine calls `combine()` once, on the objective result and, if reached, the subjective one. A failure is treated as its own signal rather than skipped: an objective guardrail that errors fails to `BLOCK`; a subjective guardrail that errors fails upward to `AUTH`. A subjective `BLOCK` gets one more check before it's honored: it has to carry a reason code on a short hard-block allowlist, currently just the lethal-trifecta floor, or it's clamped down to `AUTH`. The idea is that a learned score shouldn't lock you out on a false positive the way a deterministic floor can. Any rule or detector installed as a plugin is discovered through the registry's entry points, so `combine()` never needs to know a plugin's name, and a plugin that crashes during evaluation yields `BLOCK`, not a silently skipped rule.

```mermaid
flowchart LR
  SO["SecurityObject"] --> DE["decision_engine<br/>.decide()"]
  subgraph GUARDS["Guardrails"]
    OG["ObjectiveGuardrail<br/>(deterministic rules)"]
    SG["SubjectiveGuardrail<br/>(behavioral, raise-only)"]
  end
  REG["registry.py<br/>entry-point discovery:<br/>doberman.rules · doberman.detectors"]
  REG -- "loads installed plugins" --> OG
  REG -- " " --> SG
  DE --> OG --> CMB
  DE --> SG --> CMB
  CMB["combine()<br/><b>never returns a verdict<br/>lower than any input</b>"]
  subgraph FLOORS["Hard floors (cannot be muted)"]
    TF["taint_floor<br/>secret-egress taint"]
    TRI["trifecta<br/>lethal-trifecta co-occurrence"]
    COR["correlator<br/>cross-session linkage"]
  end
  CMB --> FLOORS --> OUT["Decision<br/>PASS / AUTH / BLOCK"]
  ADJ["adjudicator seam<br/>(optional LLM judge, shadow-first)"] -. "advisory only" .-> CMB
  classDef amber fill:#b45309,color:#fff,stroke:#b45309
  class CMB amber
```
An optional adjudicator can attach a shadow recommendation to an `AUTH` decision for later comparison, but it never touches the verdict itself.
