Skip to content

feat(cli): add doberman enforcement <enforce|monitor|off> command (UX-A)#97

Open
fu351 wants to merge 1 commit into
mainfrom
feat/policy/enforcement-cli-setter
Open

feat(cli): add doberman enforcement <enforce|monitor|off> command (UX-A)#97
fu351 wants to merge 1 commit into
mainfrom
feat/policy/enforcement-cli-setter

Conversation

@fu351

@fu351 fu351 commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Slice

What this PR does

Adds the setter for the enforcement dial that #88 wired the consumption of. Before this there was no in-product way to turn Doberman's enforcement down — the single "turn it down" knob a frustrated user reaches for simply didn't exist as a command.

It routes through the existing gated chokepoint apply_enforcement_change() (policy/drift.py) — semantics unchanged:

  • Soften (enforcemonitor/off) is confirmed — plus a TOTP code when 2FA is enrolled, and confirm-only otherwise so the safety valve is never out of reach — and recorded in the append-only policy-change ledger.
  • Re-arm (→ enforce) is a strengthen and applies automatically, ungated.
  • Persists exactly the fields the ledger recorded, so the read-side effective_enforcement clamp confirms a soften; a hand-edited enforcement: off with no approved ledger row is still clamped back to enforce (fail closed).
  • No argument → prints the current on-disk state. Mirrors the existing mode / prefs command shape.

Also fixes two import bugs in the pre-existing in-progress work: load_enforcement was used but not imported (would NameError at runtime); resolve_enforcement_sync was imported into main.py but unused (ruff F401).

Tests added (run in CI)

  • tests/unit/test_cli_enforcement.py (8 tests): show defaults to enforce; confirm-gated soften round-trips and becomes effective (resolve_enforcement_sync); monitor round-trips; a denied soften leaves state unchanged (fail closed); an unknown state is rejected before it reaches the gate or the policy file; no-op is a friendly no-op; re-arm is never gated (a prompter that raises on any prompt proves it); and a hand-edited off with no ledger row is clamped back to enforce (the tamper contrast that is the whole reason the command exists). Unenrolled 2FA exercises the confirm-only safety-valve path.

Local: ruff check ✓, ruff format --check ✓, lint-imports ✓ (2/2 contracts kept), the 8 enforcement tests ✓ (full suite running for coverage).

Security checklist

  • Fails closed on error / uncertainty — a denied/failed gate leaves enforce; an unknown argument exits 2 before the gate.
  • No secret, full file, or unredacted prompt logged or committed.
  • Any guardrail/learning change is raise-only — soften is human-gated + ledgered; re-arm auto-applies; a hand-edited soften is clamped back up.
  • Every BLOCK/AUTH carries reason codes + a human explanation — N/A (this is a CLI setter, not a decision-path rule); the gate it calls is unchanged.
  • doberman-core does not import doberman_enterprise.

Public-release safety (doberman-core only)

  • Contains nothing from the "not allowed" list.
  • Core still builds/tests/runs with NO enterprise package installed.

Edge cases covered / Deviations from plan / Risks introduced

…UX-A)

Wire the gated setter apply_enforcement_change() to a CLI verb so a user
finally has an in-product way to turn Doberman's enforcement dial down. Before
this, #88 wired only the decision-path *consumption* of the dial — no setter
existed, so a frustrated user had no in-band knob to reach for.

Turning the dial DOWN is a weaken: it is confirmed — plus a TOTP code when 2FA
is enrolled, and confirm-only otherwise so the safety valve is never out of
reach — and recorded in the append-only policy-change ledger. Re-arming
(-> enforce) is a strengthen and applies automatically, ungated. The command
persists the exact fields the ledger just recorded, so the read-side
effective_enforcement clamp confirms a soften; a hand-edited `enforcement: off`
with no approved ledger row is still clamped back to enforce (fail closed).

Mirrors the existing `mode`/`prefs` command shape. With no argument, prints the
current on-disk state. Adds tests/unit/test_cli_enforcement.py covering show,
the confirm-gated soften round trip, denial-leaves-state-unchanged (fail
closed), unknown-state rejection, re-arm-is-never-gated, and the hand-edit
tamper clamp. Unenrolled 2FA takes the confirm-only path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant