# System atlas

Doberman sits between a coding agent and its tools and turns every meaningful action into a risk-based `PASS`, `AUTH`, or `BLOCK` decision. This page maps the whole system for anyone deciding whether to trust it or contribute to it.

## One path in, no path around it

Two front doors reach Doberman: host hooks running inside an agent harness (Claude Code, Codex, OpenClaw), and an MCP proxy that wraps any tool server for any MCP client. Both normalize the tool call into the same `SecurityObject` and hand it to one decision engine. There is no third path.

```mermaid
flowchart LR
  subgraph AGENTS["Protected agents"]
    CC["Claude Code"]
    CX["Codex CLI"]
    OC["OpenClaw"]
    MC["Any MCP client"]
  end
  subgraph FRONT["Front doors (mediation)"]
    HH["hosthooks<br/>spine.py"]
    PX["proxy<br/>serve.py / executor.py"]
  end
  subgraph CORE["Policy core (never imports proxy)"]
    SO["SecurityObject<br/>(models.py)"]
    ENG["engine<br/>decision_engine.py"]
    OBJ["objective rules"]
    SUBJ["subjective layer"]
    POL["policy<br/>modes / checklist / drift"]
  end
  AUTH2["auth<br/>challenge + prompters"]
  ST["storage<br/>redacted log + sinks"]
  SURF["surfaces<br/>cli / dash / tui"]
  TOOL[("real tool")]
  CC -- "PreToolUse hook" --> HH
  CX -- "hook shim" --> HH
  OC -- "hook" --> HH
  MC -- "MCP calls" --> PX
  HH -- "normalize" --> SO
  PX -- "normalize" --> SO
  SO --> ENG
  OBJ --> ENG
  SUBJ -- "raise-only" --> ENG
  POL -- "mode + checklist" --> ENG
  ENG -- "AUTH" --> AUTH2
  AUTH2 -- "approved / denied" --> ENG
  ENG -- "PASS" --> TOOL
  ENG -- "record (redacted)" --> ST
  ST --> SURF
  classDef amber fill:#b45309,color:#fff,stroke:#b45309
  classDef pass fill:#3e7d5a,color:#fff,stroke:#3e7d5a
  class AUTH2 amber
  class TOOL pass
```

Both front doors converge on one `SecurityObject`, then one engine; a `BLOCK` means the downstream tool never sees the call.

See the full lifecycle of one tool call in [the decision path](https://docs.trydoberman.dev/concepts/decision-path/), and the verdict order in [verdicts](https://docs.trydoberman.dev/concepts/verdicts/).

## The policy core never imports a front door

Inside the core package, the safety-critical modules (the decision engine, roles, policy, storage, auth, the subjective layer, and the egress broker) never import the proxy, the host hooks, the dashboard, or the turn gate. Adapters depend on the engine; the engine never depends on an adapter. An import-linter contract enforces this in CI, so a pull request that adds a stray import from the engine into the proxy fails the build before anyone has to catch it by eye.

That is why a rule, an auth flow, or a redaction fix can be read and tested without knowing which harness triggered it, and why no host-specific shortcut can quietly change what a decision means.

> **Fail closed** Any error, uncertainty, or unhandled case in this path denies. The agent can never reach a real tool around Doberman.

## The front doors, in detail

Host hooks (`doberman/hosthooks/`) share one spine (`spine.py`) across every adapter: `claude_code.py`, `codex.py`, `openclaw.py`. The spine checks the project exclusion list first, so an uninstalled project's hooks skip cleanly, then runs `evaluate_pre` / `evaluate_post` against the engine. `hookio.py` owns the shape of a deny response and resolves AUTH; a broad exception anywhere in that path denies rather than passing the call through. `singleflight.py` collapses duplicate hook invocations so one tool call isn't evaluated twice.

The MCP proxy (`doberman/proxy/`) does the same job for any MCP client: `serve.py` runs the stdio server, `normalize.py` turns the MCP call into a `SecurityObject`, and `executor.py` (`decide_and_execute`, `_handle_auth`) drives the same engine call the host hooks use.

Both front doors also protect their own configuration. An agent cannot edit `.doberman/`, a host's settings files, or the hook exclusion list to route around the guard; those paths are confined the same way any other protected path is.

```mermaid
flowchart TB
  subgraph HOSTS["hosthooks"]
    CCA["claude_code.py"]
    CXA["codex.py<br/>(CC-compat shim)"]
    OCA["openclaw.py"]
    SP["spine.py<br/>1. is_excluded? (uninstalled projects)<br/>2. evaluate_pre / evaluate_post"]
    IO["hookio.py<br/>deny shape · resolve_auth<br/>broad-except → deny"]
    SF["singleflight.py<br/>duplicate-hook collapse"]
    CCA & CXA & OCA --> SP --> IO
    SP --- SF
  end
  subgraph PROXY["proxy (MCP)"]
    SV["serve.py<br/>MCP stdio server"]
    EX["executor.py<br/>decide_and_execute · _handle_auth"]
    NM["normalize.py"]
    SV --> NM --> EX
  end
  SP -- "SecurityObject" --> ENG2["engine.decide"]
  EX -- "SecurityObject" --> ENG2
  ENG2 -- "BLOCK" --> NOTH["downstream server<br/>records nothing"]
  INST["install.py / install_codex.py<br/>hook registration + gated uninstall"] --- SP
  classDef block fill:#a83a32,color:#fff,stroke:#a83a32
  class NOTH block
```

One spine for every host adapter, one executor for the MCP proxy, both handing a `SecurityObject` to the same engine.

Wiring a new host adapter is covered in [the host adapter guide](https://docs.trydoberman.dev/guides/host-adapter/); day-one setup for either front door is in [setup](https://docs.trydoberman.dev/setup/).

## Surfaces: where a human looks

The CLI (`doberman/cli/`, built on Typer) is the control room: `mode` and `setup` change the strength mode, `tune` reads friction telemetry, `doctor` runs health checks, and `log` / `memory` / `policy-history` read the decision history. The dashboard (`doberman/dash/`, a local Starlette app) mirrors the live feed and mode control behind a bearer token printed to the terminal that launched it. Every mutating endpoint checks that token the same way, so a local agent cannot curl its own way to a looser policy. The TUI (`tui.py`) browses the redacted log with a plain-language "why" pane (`explain.py`): a deterministic, offline narrator whose wording can optionally be reworded by an LLM, but the wording can never change the verdict it explains.

```mermaid
flowchart LR
  subgraph CLI3["cli/main.py (Typer)"]
    MODE["mode / setup<br/>→ policy.apply_mode_change"]
    TUNE["tune<br/>friction telemetry, --accept<br/>routes the weaken chokepoint"]
    DOC["doctor.py<br/>health checks"]
    LOGC["log / memory / policy-history<br/>(compact --json contract)"]
  end
  subgraph DASH["dash/app.py (Starlette)"]
    TOK["bearer token (per-run,<br/>printed to launching terminal)"]
    FEED["live feed + stats + pending"]
    RESV["/api/resolve — approve/deny"]
    MODE2["/api/mode — mode control<br/>same gate as CLI"]
  end
  TUI2["tui.py — log browser"]
  EXPL["explain.py — offline 'why',<br/>LLM narrator opt-in, never the verdict"]
  ST2["storage (redacted)"] --> LOGC & FEED & TUI2
  EXPL --- TUI2
  TOK --- RESV & MODE2 & FEED
  MODE & MODE2 --> GATE2["policy drift gate"]
  classDef amber fill:#b45309,color:#fff,stroke:#b45309
  class GATE2 amber
```

The CLI's `mode` and the dashboard's `/api/mode` both call the one shared `apply_mode_change` path, so changing the strength mode from either surface goes through the same gate.

The full command reference is in [the CLI reference](https://docs.trydoberman.dev/reference/cli/).

## Discovery

`doberman/discovery/` scans the MCP configurations already on the machine and reports what agents could reach: a read-only local risk map. It never modifies a config it reads.

## Extension seams

Core declares interfaces and a registry; anyone can ship a package that registers implementations through their own `pyproject.toml` entry points: a rule (`doberman.rules`), a detector (`doberman.detectors`), an audit sink (`doberman.audit_sinks`), an auth provider (`doberman.auth_providers`), a policy source (`doberman.policy_sources`), or a cost observer (`doberman.cost_observers`). Core never imports a plugin by name; it discovers whatever is installed at runtime. With nothing but `doberman` installed, the built-in rules and behavior run in full. Plugins add capability; they aren't required for the base guarantee. CI proves this: one job builds and tests core with no plugin package installed at all.

```mermaid
flowchart LR
  subgraph CORE2["doberman (public, Apache-2.0)"]
    R1["Rule seam<br/>entry point: doberman.rules"]
    R2["Detector seam<br/>doberman.detectors"]
    R3["AuditSink seam<br/>doberman.audit_sinks"]
    R4["AuthProvider seam"]
    R5["PolicySource seam"]
    R6["CostObserver seam"]
    REG2["registry — entry-point discovery"]
    R1 & R2 & R3 & R4 & R5 & R6 --- REG2
  end
  subgraph PLUG["Any installed package"]
    EX1["example plugin<br/>(examples/ tutorial)"]
  end
  EX1 -- "registers via entry points" --> REG2
  CI2["CI guards:<br/>import-linter contracts<br/>standalone install+test job"] --- CORE2
```

Any installed package can register through the same entry-point groups core itself defines.

Read [the plugin guide](https://docs.trydoberman.dev/guides/plugins/) to write one. Each subsystem this page only summarizes (the objective guardrail, the subjective layer, the turn gate, policy and drift defense, storage and audit, and the auth flow) has its own concept page; [the decision path](https://docs.trydoberman.dev/concepts/decision-path/) is the place to start. [Parity](https://docs.trydoberman.dev/parity/) shows which guarantees are proven on which host.
