# Objective guardrail

This page describes the deterministic half of a decision: the fixed rule roster that runs on every action before anything adaptive gets a say.

The objective guardrail runs first, and it runs the same way every time. No learning, no per-deployment tuning, no state carried between calls beyond the current policy. Each rule looks at the normalized `SecurityObject` plus the evaluation context (repo root, active role, strength mode) and returns its own `GuardrailResult`. None of them can lower another's verdict; only the engine's raise-only combine, described in [The decision path](https://docs.trydoberman.dev/concepts/decision-path/), decides the final answer.

The built-in roster:

- `commands.py`: `DestructiveCommandRule` catches recursive force-deletes, history rewrites, piping a download into a shell, and similar irreversible shell or git patterns. It also flags bare environment dumps (`env`, `printenv`, `export -p`) and protects Doberman's own control-plane files from the agent it supervises.
- `paths.py`: `ProtectedPathRule` blocks or steps up access to a path after resolving it through the shared canonicalize helper and confining it to the repo root, so a `..` or a symlink can't walk it outside that boundary.
- `destinations.py`: `ExternalDestinationRule` looks at where an action is headed. An unrecognized host requires authentication; a shell, git, or package-install command whose runtime destination can't be proven statically requires it too; and a proven egress broker can turn the same rule into a hard block or a pass-through, depending on what it actually observed at the socket.
- `data_classes.py`: `PiiDataClassRule` recognizes checksum-valid personal or financial data leaving the workspace: a payment card number by issuer prefix and Luhn check, an IBAN by mod-97, and a dashed US Social Security number. It fires only when the action also has an external destination.
- `trifecta_floor.py`: `TrifectaFloorRule` steps up any action whose classification shows private data, untrusted provenance, and an external channel all at once. It's the one condition allowed to turn a subjective signal into a hard block.
- `role_boundary.py`: `RoleBoundaryRule` matches the action against the active agent role's declared boundaries, per repo.
- `policy_source.py`: `PolicySourceRule` checks the instruction's originating source (a GitHub issue, a webpage, an email) against a policy-defined block or sensitivity list.

Destination gating is worth stating twice: presence alone never escalates anything. A card number in a file you're writing locally isn't exfiltration; the same number in a payload headed to an external destination is. The rules that touch sensitive data classes only fire once both conditions hold.

Every rule shares `normalization.py`'s token and segment helpers, so a command parses the same way whether `commands.py`, `destinations.py`, or the trifecta floor is looking at it. And before any rule compares a path, it goes through the one canonicalize helper described in [Verdicts, risk and reason codes](https://docs.trydoberman.dev/concepts/verdicts/).

None of this is airtight, and it isn't meant to be. Deterministic pattern matching catches the shapes it was written to catch; the [subjective layer](https://docs.trydoberman.dev/concepts/subjective-layer/) exists because static rules can't cover what a rule author didn't anticipate. Treat the objective guardrail as one layer of defense in depth, not a guarantee.

```mermaid
flowchart TB
  SO["SecurityObject + EvalContext"]
  subgraph RULES["Built-in rule roster"]
    CMD["commands.py<br/>destructive cmds · control-plane<br/>self-protection · env-dump AUTH"]
    PTH["paths.py<br/>ProtectedPathRule: canonicalize,<br/>confine to repo root"]
    DST["destinations.py<br/>ExternalDestinationRule ·<br/>broker grants · secret co-occurrence"]
    PII["data_classes.py<br/>PiiDataClassRule: Luhn · IBAN mod-97 ·<br/>dashed SSN, destination-gated"]
    TRI2["trifecta_floor.py<br/>private data + injection<br/>+ egress = step-up"]
    RB["role_boundary.py<br/>per-repo role matcher"]
    PS["policy_source.py<br/>policy-as-source checks"]
  end
  SO --> RULES
  RULES --> GR["GuardrailResult × N<br/>reason_codes + explanation"]
  GR --> CMB["engine.combine()"]
  N["normalization.py<br/>shared token/segment helpers"] --- CMD
  classDef amber fill:#b45309,color:#fff,stroke:#b45309
  class PII,TRI2 amber
```
Highlighted rules are destination-gated: a card number in a local file write is not exfiltration on its own.
