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.


Routine work. The tool runs — zero friction.
Held until you approve.approved → runs · denied → nothing runs
Never reaches the tool. The agent gets the reason instead.
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.
Held before it ran. The approval reaches you in your editor, the dashboard, or the terminal:
Never forwarded. The agent gets the verdict, the reason codes, and a one-line explanation — your machine gets nothing.
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.
Invocation adapters — three doors, one engine
Tier 0 signatures · Tier 1 heuristics
executor.decide_and_execute
— the chokepoint
objective floor only
fails toward AUTH, never silent PASS
SecurityObject → decide(ctx) — every path, no exceptions
Policy core — imports no adapter
combine() — raise-only merge
of every guardrail fragment
learned, raise-only
forwarded to the real MCP servers / tool
auth/ — confirm · TOTP · elevation
elicitation → GUI → dashboard → TTY
approved → re-decide (TOCTOU) → forward
denied / timeout → not forwarded
not forwarded — policy error returned to the agent
Records — never decides
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.
- action_type: ActionType
- target_class: str
- risk: Risk
- fingerprints: HMAC
- immutable · redacted
- fingerprints: HMAC
- segments: ContentSegment[]
- apparent_intent
- never holds raw text
- role · mode
- metadata: elevation,
- abnormality …
- carried, not mutated
- verdict: Verdict
- reason_codes: ReasonCode[]
- risk: Risk
- one fragment per guardrail
fragments merge via combine() — raise-only, property-tested
- verdict: Verdict
- reason_codes: ReasonCode[]
- explanation: str — one line, human
- risk: Risk
- shadow · observe-only
- PASS < AUTH < BLOCK
- combine(a, b) — never returns
- lower than either input
- the load-bearing invariant
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.
dash/ · turngate/ policy core engine · roles · policy · storage · auth · subjective · egress
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 group | Interface | Notes |
|---|---|---|
| doberman.rules | engine/objective.py | objective rules; built-ins in engine/rules/ |
| doberman.detectors | engine/subjective.py | anomaly detectors; built-in token-channel detector |
| doberman.auth_providers | auth/provider.py | challenge back-ends |
| doberman.audit_sinks | storage/sinks.py | decision-log fan-out |
| doberman.policy_sources | policy/sources.py | external policy suppliers |
| doberman.drift_observers | policy/drift.py | watch the append-only policy ledger |
| doberman.algebra_adapters | subjective/adapters.py | refine-only domain adapters |
| doberman.cost_observers | storage/cost.py | cost-meter fan-out; never touches a verdict |
| doberman.adjudicators | engine/adjudicator.py | shadow-only: structurally no verdict authority |
| doberman.egress_brokers | egress/broker.py | dormant 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_only
combine()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.