Metadata-Version: 2.4
Name: doberman-core
Version: 0.17.1
Summary: Adaptive authorization layer for coding agents (open core)
Project-URL: Homepage, https://github.com/fu351/Doberman-Core
Project-URL: Repository, https://github.com/fu351/Doberman-Core
Project-URL: Issues, https://github.com/fu351/Doberman-Core/issues
Author: fu351
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agent-security,ai-agents,authorization,guardrails,llm,mcp,security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Requires-Dist: aiosqlite
Requires-Dist: mcp<2,>=1.27
Requires-Dist: numpy
Requires-Dist: pydantic>=2
Requires-Dist: pyotp
Requires-Dist: pyyaml
Requires-Dist: river>=0.21
Requires-Dist: scipy
Requires-Dist: typer
Provides-Extra: dash
Requires-Dist: starlette; extra == 'dash'
Requires-Dist: uvicorn; extra == 'dash'
Provides-Extra: dev
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: hypothesis; extra == 'dev'
Requires-Dist: import-linter; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest-xdist; extra == 'dev'
Requires-Dist: ruff<0.16,>=0.15; extra == 'dev'
Requires-Dist: starlette; extra == 'dev'
Requires-Dist: textual; extra == 'dev'
Requires-Dist: uvicorn; extra == 'dev'
Provides-Extra: explain
Requires-Dist: anthropic; extra == 'explain'
Provides-Extra: tui
Requires-Dist: textual; extra == 'tui'
Description-Content-Type: text/markdown

<div align="center">

<img src="logo.png" alt="Doberman logo" width="200">

# Doberman

**Adaptive Authorization & Runtime Guardrails for AI Coding Agents**

[![CI](https://github.com/fu351/Doberman-Core/actions/workflows/ci.yml/badge.svg)](https://github.com/fu351/Doberman-Core/actions/workflows/ci.yml)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
[![Status](https://img.shields.io/badge/status-alpha-orange.svg)](#roadmap)
[![Discord](https://img.shields.io/badge/Discord-join%20the%20pack-5865F2?logo=discord&logoColor=white)](https://discord.gg/Sfy5XGNqty)

Your AI coding agent can `rm -rf` your repo, leak your API keys, or be prompt-injected into exfiltrating data — **autonomously, with no undo.** Doberman is the guard dog on the execution path that stops the dangerous call **before it runs.**

</div>

<p align="center">
  <img src="docs/assets/dash-demo.gif" alt="The doberman demo attack reel against the live dashboard: a secret exfiltration, a destructive rm -rf, a protected-branch force push, a smuggled-token egress and a .env read are blocked in the live feed, then a human denies a high-risk SSH-trust-file write from the pending-approvals queue" width="820">
  <br>
  <em><code>doberman demo</code> against the live dashboard (<code>doberman dash</code>): five attacks blocked as they happen, then a human denies a high-risk approval.</em>
</p>

> If it isn't on the execution path, it's advisory, not protective.

Doberman sits *between the agent and its tools* — a transparent **MCP proxy** or **host hook** — and turns every action into an explicit, auditable decision. Every tool call gets exactly one verdict, decided *before* it executes:

| Verdict | What happens |
|---|---|
| **PASS** | Routine work — straight through, zero friction. |
| **AUTH** | Sensitive — paused for your one-tap approval. |
| **BLOCK** | Dangerous — stopped cold. It never runs. |

```
AI agent ──▶ Doberman ──▶ real tools (files, shell, MCP servers, APIs)
                 └─ normalize → risk engine → PASS / AUTH / BLOCK
```

Works with **Claude Code, Cursor, Codex, Copilot, and any MCP-compatible agent.** Open-source, local-first, and bound by two rules it will never break: it **fails closed** (uncertainty denies) and is **raise-only** (it can tighten automatically, but never silently loosens).

<div align="center">

### [Get protected in two commands](#quick-start)  ·  [Join the pack on Discord](https://discord.gg/Sfy5XGNqty)

</div>

---

## Contents

- [Why Doberman?](#why-doberman) — what it does, and the two guarantees
- [Quick Start](#quick-start) — install and protect an agent in two commands
- [Verify it end-to-end](#verify-it-end-to-end) — watch it front a real MCP server
- [Turn gate](#turn-gate) — the optional pre-inference chokepoint
- [Benchmark](#benchmark) — attack-block rate (ASR) vs. false-positive friction (FPR)
- [Write a custom Guardrail](#write-a-custom-guardrail-plugin) — register your own rule as a plugin
- [Tune to your risk tolerance](#tune-to-your-risk-tolerance) — strictness modes + the enforcement dial
- [Who is this for?](#who-is-this-for)
- [Roadmap](#roadmap)
- [Contributing](#contributing) · [License](#license)

---

## Why Doberman?

Prompt injection, tool poisoning, data exfiltration, and runaway agents are the defining security problems of agentic AI. Most "AI guardrails" inspect prompts and offer advice. Doberman is different: it is **on the tool-execution path**, so a blocked action *never runs*.

Those three verdicts aren't advice a model can talk its way past — each is enforced at the one place that counts: the instant *before* the call runs. Evade the model's own guardrails all you want; the action still has to clear Doberman.

**Two non-negotiable properties:**

- **Fail closed** — any error, uncertainty, or unhandled case denies the action. There is no path to a tool around the decision engine. *Including silence:* an approval prompt nobody answers is bounded by a hard deadline (2 minutes for the desktop dialog, 20 minutes as the whole-challenge backstop) and resolves to a **denial**, logged distinctly as `timeout` rather than `denied` — an indefinite block is not a denial, and unattended is exactly how agents usually run.
- **Raise-only learning** — guardrails and adaptive learning can auto-*tighten*, never silently loosen. Every permanent policy weakening requires explicit, possession-factor-gated, audited human approval (TOTP if enrolled, otherwise the local Doberman password).

---

## Quick Start

Doberman guards any **MCP-compatible** coding agent: pick your agent, run one command, and every
tool call is reviewed *before* it executes. The full walkthrough - every option, flag, the
dashboard, and health checks - lives in the **[Setup Guide](docs/SETUP.md)**.

```bash
pip install doberman-core
```

| Your agent | How Doberman plugs in | Get started |
|---|---|---|
| **Claude Code** | Hooks - gate every built-in *and* MCP tool call *(recommended)* | `doberman setup` -> [guide](docs/SETUP.md#claude-code-hooks) |
| **Claude Desktop / Cursor / Codex** | MCP proxy - wrap your tool server | `doberman serve -- <your-server>` -> [guide](docs/SETUP.md#mcp-proxy) |
| **OpenClaw** | Native plugin adapter | [guide](docs/SETUP.md#openclaw) - [adapter](adapters/openclaw/README.md) |
| **Any MCP-compatible agent** | MCP proxy | [guide](docs/SETUP.md#mcp-proxy) |

**Fastest path - Claude Code:**

```bash
doberman setup      # pick a strictness mode, tune guardrails, wire the hooks
```

Doberman now reviews every tool call your agent makes. Confirm it with `doberman doctor`, or watch
real verdicts with `doberman demo`. Everything else - MCP-proxy wiring, the dashboard, TUI, scan,
and 2FA - is in the **[Setup Guide](docs/SETUP.md)**.

Verdicts are colour-coded everywhere they're shown (`BLOCK` red, `AUTH` amber, `PASS` green) —
`review`, `status`, `log`, the TUI, and `demo`, which also wraps its explanations to your terminal
width. Colour is dropped automatically when output is piped or redirected, and honours
[`NO_COLOR`](https://no-color.org) when that variable is set to a non-empty value.

---

## Verify it end-to-end

Two ways to watch Doberman front a **real** MCP server — no in-process test doubles anywhere in the chain.

**Interactive demo — MCP Inspector + a real filesystem server:**

```bash
npx -y @modelcontextprotocol/inspector doberman serve -- npx -y @modelcontextprotocol/server-filesystem ~/my-project
```

Open the Inspector UI and call tools through Doberman: routine reads and writes PASS straight through to the real filesystem server; a destructive call comes back as a policy error and never executes.

**End-to-end test — in a dev checkout:**

```bash
pytest tests/integration/test_serve_end_to_end.py -q
```

This spawns `doberman serve` as a real subprocess fronting a real stdio tool server ([`tests/fixtures/stdio_tool_server.py`](tests/fixtures/stdio_tool_server.py)), connects to it with a real MCP client playing the agent, and asserts the deployable chain over actual stdio:

1. the downstream's tools are re-exposed through the proxy,
2. a PASS verdict reaches the tool (the downstream's call log records it), and
3. a BLOCK verdict (`rm -rf /`) never reaches it — the call log stays empty.

That last assertion is the **chokepoint property** the whole project hangs on.

> **Note on the test fixtures:** the rest of the integration suite deliberately uses an *in-process* fake downstream ([`tests/fixtures/fake_tool_server.py`](tests/fixtures/fake_tool_server.py)) that records every call it executes — recording is how the tests prove a blocked action reached *nothing*. It is a test fixture, not the runtime. `doberman serve` always spawns and talks to the real server you give it after `--`.

---

## Turn gate

A **second invocation point** for the same decision engine — consulted at a host **pre-inference hook** on the user's *turn* (prompt + attached/pasted/tool-fetched content), so a flagrant turn is judged **before a single inference token is spent**. The turn gate is an **efficiency / early-warning** layer with a deliberately narrow guarantee — *no Tier-0-signature turn reaches the model*. The **action gate above remains the safety guarantee**: an attacker who evades the turn gate still meets it.

- **One engine, two invocation points.** `decide_turn` reuses the raise-only `combine`, the `Decision` audit model, and the tiered-auth challenge — **zero new verdict authority**. It needs no change to the action path; `turngate/` is a new adapter (sibling to the proxy), and the engine stays pure (the tier guardrails are injected). Turn verdicts append to the same redacted `decisions` log, marked `action_type='turn'`.
- **Tier 0 — deterministic signatures (the only hard-stop, small by design).** `instruction_nullification`, `authority_override` (impersonation / mode-switch / "print your system prompt"), `secret_export`, and `encoded_payload`. The precision core is the **issue-vs-mention discriminator** + **origin rule**: a match in *untrusted* (pasted / tool-fetched) content blocks unconditionally (indirect injection); a *typed + issued* match blocks; a *typed + mentioned* (quoted / meta-discussion) match steps up to AUTH so a researcher discussing an attack is never hard-blocked; ambiguous → issued; code fences never exempt.
- **Tier 1 — heuristic recall (AUTH-only, structurally BLOCK-incapable).** Embedded agent-directed instructions inside *pasted* text (not mere imperative mood, so tutorials don't trip it), persona override, sub-threshold obfuscation, and urgency+secrecy framing — a false positive costs one tap, not a denied prompt.
- **Stylometric co-occurrence gate.** A per-entity **prompt-style baseline** (coarse scalar buckets only — length, word shape, punctuation/case/digit density; never any text) scores each turn's style as an empirical-CDF p-value, the same calibration idea the subjective baseline uses for actions. The gate steps up **only** when an *extreme* style outlier co-occurs with a *sensitive* apparent intent (credential / destructive / external-send) — **never on style alone** (people type differently tired/mobile/pasting; a device or language switch is drift, not an attack). Style-weird alone is **tagged, not gated**, and stays inert until the prompt baseline matures (the same maturity rule as the other baselines); Tier 0 is active from turn one. Known limitation: shared accounts blend typists, degrading the style signal toward noise — co-occurrence bounds the cost at one extra tap.
- **Tag-and-pass: turn signals feed the action stage (raise-only).** Every *released* turn publishes its stylometric p-value and heuristic flags as a per-entity `TurnContext`. The action stage consumes it two ways — the subjective surprise term accepts a **bounded, non-negative** contribution (a flagged turn makes its follow-on actions score harsher; a clean turn contributes exactly nothing), and actions tracing to **flagged pasted segments** inherit `provenance: untrusted_data`, which makes the lethal-trifecta floor reachable for them. Raise-only by construction; a blocked turn publishes nothing.
- **Repeat-after-block escape hatch.** A near-match resubmission of a just-blocked turn is treated as deliberate human intent and routed to a challenge with proof **scaled to the block** (a Tier 0 signature → `two_factor`; a replay loop cannot produce a TOTP), the **original reason restated**. Approval is single-use; the third attempt within the TTL locks out. The cache stores only a per-entity HMAC fingerprint, a reason, a count, and an expiry — **never the prompt**.
- **AUTH-first & graceful absence.** `BLOCK` is reserved for the obvious; everything merely suspicious asks the human. Any internal error **fails toward the human** (AUTH), never a silent pass. With no host pre-inference hook (pure MCP-proxy deployments) or `DOBERMAN_TURN_GATE=off`, the gate is simply absent and the action gate carries everything.

---

## Benchmark

A suite-agnostic harness scores Doberman as a **filter over labeled actions** and reports **ASR** (attack bypass rate) and **FPR** (benign over-block / friction). It runs the real decision engine over each labeled tool-call — Doberman is the filter, not the agent — so the gated path is deterministic and offline.

```bash
python -m tests.benchmarks.run --suite synthetic --profile both          # builtins vs plugins
python -m tests.benchmarks.run --suite synthetic --profile before_after  # without vs with Doberman
```

It reports two plugin profiles — `builtins_only` and `with_plugins` (built-ins plus any installed entry-point plugins) — and their uplift. The `before_after` profile adds a **no-guardrail baseline** (the unmediated tool path, where every attack executes) so you can read the engine's effect directly as `{before, after, delta}` — how many otherwise-executing attacks it stops vs. how much benign friction it adds. A deterministic synthetic suite gates in CI; map external task suites (**AgentDojo**, AgentDyn, AgentSentry, …) onto core's types with a small adapter — see [`tests/benchmarks/README.md`](tests/benchmarks/README.md).

> Reports hold counts, verdicts, and reason codes only — never payload text. ASR is reported alongside a stricter `asr_strict` (where only a hard `BLOCK` counts as mitigation): honest measurement, not a single headline number.

---

## Write a custom Guardrail (plugin)

Third-party rules register through the **`doberman.rules`** entry-point group. Core never imports your package by name — install it, and `discover_rules()` / the objective guardrail pick it up automatically.

A five-minute worked example lives at [`examples/plugin-guardrail/`](examples/plugin-guardrail/) (from a git checkout):

```bash
pip install -e ".[dev]"
pip install -e examples/plugin-guardrail
pytest examples/plugin-guardrail/tests -q
pip uninstall -y doberman-example-plugin-guardrail   # optional: restore core-only discovery
```

The tutorial rule steps up a write to `SECRETS_TODO.md` to AUTH, stays raise-only, and never puts the path or payload into its explanation. Copy the package when you need a real custom rule of your own.

> While the example is installed, core's "no plugins registered" checks will see it — that is expected. Uninstall before re-running the full core suite if you want a clean standalone environment.

---

## Tune to your risk tolerance

Set a mode in `.doberman/policies.yaml` or via `doberman mode <mode>`. Every mode change made this way — the CLI dial or the setup wizard — is recorded in the append-only policy-change ledger (view it with `doberman policy-history`). Lowering strictness (paranoid → strict → balanced → light) is a **weaken** and requires confirmation plus a possession factor: **TOTP if enrolled, otherwise the local Doberman password**. If neither exists, the lowering fails closed; confirmation alone never suffices. Raising stays frictionless and auto-applies:

| Mode | Best for | Bulk-delete threshold | Step-up for unknown destinations | Step-up for behavioral anomalies | Lethal-trifecta exfil |
|---|---|---|---|---|---|
| **Light** | Exploratory / trusted environments | 100 files | No | No | AUTH |
| **Balanced** *(default)* | Everyday coding agents | 25 files | No | Yes | AUTH |
| **Strict** | Production repos, shared codebases | 10 files | Yes | Yes | **BLOCK** |
| **Paranoid** | Highly autonomous or security-critical agents | 3 files | Yes | Yes | **BLOCK** |

> Hard blocks (secret exfiltration, destructive commands, role-boundary violations, smuggled-token-channel exfiltration) are **identical in every mode**. The mode dial only affects where step-up authentication is required for ambiguous or high-risk actions.
>
> Editing **CI/CD pipeline configuration** — GitHub Actions, GitLab CI (`.gitlab-ci.yml`), Jenkins (`Jenkinsfile`), CircleCI (`.circleci/`), or Azure Pipelines (`azure-pipelines.yml`) — steps up to **authentication in every mode**: that config builds, tests, signs, and deploys the repo, so an agent rewrite is always a human-in-the-loop moment.
>
> **Unknown network destinations** step up to authentication only in Strict/Paranoid. Light and Balanced treat a plain unknown host (e.g. fetching a docs site or an API) as allowed — that AUTH fired on almost every web fetch and was the top source of benign prompts. This relaxes the *destination-alone* signal only: a secret leaving to **any** host is still a hard block (secrets rule + raise-only combine, every mode), and the sharper destination smells (credentials embedded in the URL, raw IP addresses, unresolvable hosts) still step up in every mode. An **out-of-scope role target** likewise steps up in Balanced/Strict/Paranoid but is relaxed in Light; a role-**blocked** target is a hard block in every mode.
>
> One escalation is mode-gated: the **lethal trifecta** — sensitive data **and** untrusted-content provenance **and** an external destination — steps up to authentication in Light/Balanced, and is a hard **BLOCK** in Strict/Paranoid. Those high-security modes refuse this serious-exfil pattern outright rather than leaving it to a confirmation prompt that alert fatigue could rubber-stamp.
>
> Strict/Paranoid now hard-**BLOCK** a **LOCAL hard smuggled-token channel** (previously AUTH); this is raise-only, and Light/Balanced remain unchanged at AUTH.

### Enforce / monitor / off — the enforcement dial

Orthogonal to the strictness *mode* is an **enforcement dial** (`enforce` *(default)* / `monitor` / `off`) that decides whether Doberman **acts** on a verdict or just observes:

- **`enforce`** — the normal behavior: AUTH prompts, BLOCK denies.
- **`monitor`** — a deliberate **observe mode**. The *discretionary* layer (behavioral anomalies, soft step-ups) is evaluated and **recorded** — `doberman log` / `doberman tui` show what *would* have happened — but it never blocks or prompts. Use it to try Doberman on a repo without friction, or to tune before turning it on.
- **`off`** — the discretionary layer is not evaluated.

Set it with **`doberman enforcement <enforce|monitor|off>`** (no argument prints the current state). Turning the dial *down* is confirmed — plus the strongest **enrolled** possession factor (a TOTP code if 2FA is enrolled, otherwise the local Doberman password) — and recorded in the ledger (view it with `doberman policy-history`); with **neither** factor enrolled the change now **fails closed** (there is no confirm-only fallback — run `doberman password set` first, then retry). Turning it back *up* re-arms automatically, with no gate.

**In every state the objective floor stays live.** Secret exfiltration, multi-step/confirmed exfil, destructive commands, protected-path writes, role/policy blocks, and the lethal trifecta always block — `monitor`/`off` can only soften the *discretionary* verdicts, never a catastrophic action. Softening the dial is gated behind confirmation plus a possession factor (TOTP if enrolled, else the local password — fails closed if neither is enrolled), and the on-disk value is **ledger-verified** on every call, so a hand-edited `enforcement: off` in `policies.yaml` with no matching approved change is caught and clamped back to `enforce` (fail-closed).

### Subjective preference weights — `doberman prefs`

The adaptive layer's four SL5 "care" weights (`confidentiality`, `reversibility`, `interruption_tolerance`, `blast_radius`, each in `[0, 1]`) tune how readily *discretionary* behavioral signals step up — the objective hard-block floor never moves. Show the active vector with `doberman prefs`, set one weight with `doberman prefs <dimension> <value>`. The same permanent-lowering rule as `mode` applies: **lowering** a weight requires TOTP if enrolled, otherwise the local Doberman password; with neither factor it fails closed, and confirmation alone never suffices. **Raising** a weight is a strengthen and always applies immediately. Every attempt, approved or denied, is recorded in the append-only ledger.

---

## Who is this for?

- **Developers running AI coding agents** who want autonomous agents without `rm -rf` roulette.
- **Security engineers** evaluating AI agent security, MCP security, LLM tool-use sandboxing, and zero-trust architectures for agentic AI.
- **Platform teams** deploying agent fleets who need policy enforcement, audit logs, and human-in-the-loop approval for destructive actions.

---

## Roadmap <a name="roadmap"></a>

Planned and in-flight work now lives on GitHub — the **[Doberman Roadmap board](https://github.com/users/fu351/projects/5)** (current focus:
host-harness containment, cost observability, and the enterprise platform). For everything already shipped,
see the **[changelog](CHANGELOG.md)**.

### Known limitations

Doberman is **defense-in-depth, not airtight** — no single rule is a guarantee. The concrete, currently-known gaps:

- **Whole-script homoglyph confusables.** The deterministic check catches *intra-token* mixed-script confusables (e.g. `раypal`, which mixes Cyrillic and Latin). But a token rendered **entirely in one non-Latin script** that mimics a Latin word (e.g. an all-Cyrillic look-alike of `paypal`) is NFKC-stable and is **not** caught by the core deterministic check today. Closing it is planned via a perplexity/confusable detector.
- **Bare high-entropy hex.** To avoid flagging git SHAs, content/AST digests, and lockfile hashes as secrets — a noisy false positive that also poisoned the multi-step taint ledger — the generic high-entropy heuristic ignores tokens that are *entirely* hash-shaped hex (≥ 40 chars). A real secret that is bare hex with **no** surrounding credential name is therefore not stepped up by this heuristic alone; it is still caught when it carries a credential key-name (e.g. `API_KEY=…`), matches a known credential shape, or is later matched by the read-vs-send fingerprint.
- **Bare-token fixture/pattern-text suppression is WEAK-path only, and marker-gated on the residual.** A bare (non-assignment) token that is regex-pattern source text being quoted (e.g. `sk-ant-[A-Za-z0-9_-]{20,}`) or an obvious hand-written fixture is not stepped up by the high-entropy heuristic alone (#73). Because a fixture marker (`EXAMPLE`/`SAMPLE`/`FAKE`/`DUMMY`) and ordered `0-9`/`a-z` filler are attacker-controllable — and for a *shapeless* secret the high-entropy heuristic is the only signal — a marker on its own is **not** trusted: the token is suppressed only when, after stripping the markers and ascending runs, the residual is too short/low-entropy to be a secret. A real key padded with `EXAMPLE` keeps a high-entropy residual and still fires, and a variable merely *named* with a marker never suppresses its value (the check runs on the RHS after the `=` split). The suppression also never touches the STRONG credential-shape path, which can still drive `secret_exfiltration`. Regex-pattern source (`[]{}\`) is suppressed unconditionally — the tokenizer charset can't produce those characters in a real token. A full live-shaped example key quoted in prose with no marker is still indistinguishable from a real one and steps up.
- **Static egress classification, not a runtime egress broker.** Doberman now reads the external destination out of **shell / package / git commands** too — not just `network_request` calls. The direct-egress verb set spans HTTP/copy tools (`curl`/`wget`/`scp`/`sftp`/`rsync`) **and** raw socket/shell channels (`nc`/`ncat`/`netcat`/`ssh`/`telnet`/`ftp`/`tftp`/`socat`) — so a secret piped to `curl <host>` (or `nc host port`) is a hard **BLOCK**, and *any* such command egress (even to a trusted-looking host, or one it cannot resolve to a single route — e.g. a bare `nc host port` or `ssh -R` tunnel with no URL) steps up to **authentication**. This is **raise-only**: it never mints a new silent allow, and ambiguity fails *toward* the human. But it is a *static* parse of the command string — it can flag "this looks like egress" yet cannot prove the host it classified is the socket the process actually opens. A redirect file, `--resolve`/`--connect-to`, an `HTTP(S)_PROXY`/`ALL_PROXY` override, DNS rebinding, a URL built at runtime, `git push` to an already-configured origin, a package lifecycle script, a trusted tenant abused as a channel, or egress from a spawned child process can all still route around the static classifier. **Non-verb channels also remain uncovered:** DNS-label exfil (`dig`/`host`/`nslookup` TXT lookups), bash's built-in `/dev/tcp`, and `openssl s_client` present no recognizable egress verb, so static classification does not see them. Real containment needs a runtime egress broker (planned — the `EgressBroker` seam and its entry-point group, `doberman.egress_brokers`, now exist and are consulted on every egress-classified action; a registered broker's *retrospective* ground-truth signal — what an entity's connections actually showed a moment ago — can now **raise** a decision toward `AUTH` when it diverges from the static classification, but a broker verdict still cannot lower one or grant a `PASS` on its own). A concrete core reference broker's building blocks now exist too — a default-deny allowlist, a two-sided enforcement probe (a direct connection must fail *and* a broker-routed one must succeed), and now a real listener: a minimal, stdlib-only `asyncio` HTTP `CONNECT` forward proxy (`doberman.egress.proxy.ForwardProxy`) that enforces the allowlist at the socket — a denied destination's upstream connection is never opened. It is **`CONNECT`-only (no SOCKS)** and has **no transparent/SNI-sniffing mode** (it can only mediate traffic explicitly routed to it), and it still ships unregistered as a `doberman.egress_brokers` entry point in core (opt-in wiring only). PASS-authority now exists (RB.4): a registered broker can let `ExternalDestinationRule` contribute `PASS` instead of its usual AUTH, but only when the broker is `PROVEN` to enforce egress **and** its verdict both allowlists **and** will itself enforce this exact destination at the socket — a bare allowlist claim from an unproven or non-enforcing broker still stays AUTH, and RB.3's route-divergence check always wins over a broker PASS. Paranoid mode (RB.5) can now escalate a non-allowlisted destination all the way to a hard BLOCK, but only under the mirror-image condition — a `PROVEN`, `will_enforce`-attesting broker — so the escalation is never a bare mode toggle pretending to be real enforcement; with no broker registered, Paranoid is unchanged from every other mode. A registered broker's retrospective connection history now also feeds a bounded, in-memory per-entity velocity check (RB.6): burst/volume/fan-out over the same recent window can **raise** a `PASS` to `AUTH` (winning even over a broker-backed `PASS`) or append a reason code onto an already-`AUTH`/`BLOCK` result, never lower one, and it is silent with no broker or no `connection_events()`.
- **Artifact digest verification (RB.7) is post-fetch and opt-in — it does not, and cannot, verify content before the fetch decision.** A `PASS` on a `network_request` action is granted *before* the fetch happens, and the RB.2b `ForwardProxy` broker is an HTTP `CONNECT` proxy that relays TLS **opaquely** — it never sees plaintext response bytes, so it cannot inspect or verify a payload pre-decision (that would require TLS MITM interception, deliberately out of scope for this feature). What Doberman does instead: at the same point the existing output secret-scan runs (after the downstream tool call returns), it compares the fetched RESULT text's sha256 digest against any pin an operator configured in `.doberman/artifact_pins.yaml`. A mismatch withholds the content from the agent; a match passes it through. **Any artifact without a configured pin is not verified at all** — this is a narrow, explicit-allowlist integrity check, not a general supply-chain guarantee, and with no pins file present behavior is completely unchanged.
- **Egress behind a flag-taking transparent wrapper steps up to AUTH — and static classification is still bypassable.** When an egress command is invoked through a wrapper that takes its own flags (e.g. `sudo -u www-data curl …`, `nice -n 10 curl …`, `ionice -c 2 wget …`), the wrapper's option shifts argv so the option is misread as the command. Doberman detects the hidden command and steps it up to **authentication** when the shlex-normalized command tokens name a known egress tool — including a quote-split (`cu''rl`) or path-qualified (`/usr/bin/curl`) verb. This is **raise-only** (it never mints a new silent allow), but two honest limits remain: (1) static parsing cannot recover the *wrapped* command's host, so a wrapped secret exfiltration resolves to **AUTH, not the hard BLOCK** its un-wrapped form gets (the secret-exfil floor needs the host it cannot see); and (2) a verb obscured beyond token normalization — a nested shell (`sh -c '…'`), command substitution (`$(…)`), or a name assembled at runtime — can still evade the static classifier. Conversely, an egress name that appears only as an *argument to a flag-taking wrapper* (e.g. `sudo -u www-data grep curl x`, where argv-shifting makes the wrapper misread `curl` as the command) over-steps to AUTH — a deliberate fail-closed cost; a bare `grep curl x` with no wrapper parses cleanly and is not flagged. Robust containment here needs the runtime egress broker (planned).
- **The adaptive layer runs on the MCP proxy path, not the host-hook path.** The Claude Code and OpenClaw hooks run the deterministic objective guardrail only — the per-entity behavioral baseline, surprise scoring, and drift detection (`doberman.subjective`) are not consulted there. This is deliberate: a `PreToolUse` hook runs before *every* tool call, and importing `numpy`/`scipy`/`river` at module scope costs ~2s per call. The hook path gives you the deterministic floor (path confinement, destructive-command detection, secret patterns, egress classification, role boundaries, the enforcement dial); adaptive escalation currently needs the proxy. Wiring the adaptive layer onto the hook path via a warm process is planned.

---

## Contributing

Start with [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, CI checks, project
invariants, and the PR workflow.

**Come say hi.** Questions, ideas, a rule pack to share, or an attack you caught in the wild?
[**Join the pack on Discord →**](https://discord.gg/Sfy5XGNqty) — it's where the roadmap gets shaped.

**Found a vulnerability or a way around a guardrail?** Please report it privately — see
[SECURITY.md](SECURITY.md). Don't open a public issue or Discord post for a security report.

---

## License

Apache-2.0. The core is genuinely standalone — no proprietary dependency, ever (CI-enforced).

---
