Originally created by: QY-25123
Slice
- Repo: doberman-core
- Feature / Slice: dashboard D6 — change the strictness mode from the dashboard
- Plan reference: n/a (user-requested convenience feature, no plan slice)
What this PR does
Lets light/balanced/strict/paranoid be changed from the dashboard instead of only the
terminal: a change control next to the mode badge opens a small form backed by
GET/POST /api/mode (src/doberman/dash/app.py).
Both directions go through one shared gate, extracted from cli/main.py's
_apply_mode_change into doberman.policy.drift.apply_mode_change(name, repo_root, reason, *,
prompter=None, establish_ok=False) — the exact function doberman mode/doberman setup now
call too, so the CLI and the dashboard can never drift out of sync on this gate:
- Raising strictness stays frictionless (auto-approved, no prompt).
- Lowering it requires the same possession factor as the CLI (a 2FA code if enrolled, otherwise
the local password); with neither enrolled it fails closed.
- Every attempt — approved or denied — is written to the same append-only policy-change ledger
(doberman policy-history).
POST /api/mode accepts {"mode": <name>, "code"?: <str>}. Exactly like the existing
POST /api/resolve/{id}, the dash server never verifies the code itself — a new
non-interactive _ModeChangePrompter (implementing the existing Prompter protocol) carries
code through opaquely to the real gate in doberman.policy.drift, which performs the actual
verification via the existing auth modules. test_dash_app_still_never_imports_totp in the new
test file guards this the same way the D3 approve/deny tests already do for /api/resolve.
Docs (docs/SETUP.md) and CHANGELOG.md updated to describe the new control.
Tests added (run in CI)
tests/unit/test_dash_mode.py (16 tests): auth matrix (401s), GET /api/mode contract,
raising is frictionless and never denied, a no-op mode change skips the gate/ledger, lowering
is denied with no factor enrolled / no code supplied / a wrong TOTP code / a wrong local
credential, lowering succeeds and is ledgered correctly with a valid TOTP code and with a
valid local credential, unknown mode name → 400, missing mode field → 400, the
dash-never-imports-totp structural guarantee, and the credential never appearing in the HTTP
response body.
- Existing
tests/unit/test_cli_lowering_gate.py, test_mode_ledger.py,
test_drift_preferences_gate.py, and the full tests/unit/test_dash_*.py suite pass
unchanged against the refactor (the CLI's gate behavior is byte-for-byte preserved — it's now
a one-line wrapper around the shared function).
Public-release safety (doberman-core only)
- [x] Contains nothing from the "not allowed" list: no enterprise/hosted code, no proprietary detection, no customer data, no secrets, no commercial-license code
- [x] Core still builds/tests/runs with NO enterprise package installed
Security checklist
- [x] Fails closed on error / uncertainty
- [x] No secret, full file, or unredacted prompt logged or committed
- [x] Any guardrail/learning change is raise-only (no silent loosening) — lowering the mode is
gated behind the same possession factor as the CLI; raising is unrestricted, exactly as before
- [x] Every BLOCK/AUTH carries reason codes + a human explanation — a denied dashboard change
returns a 403 with an error message and is recorded in the policy-change ledger with its
denial method
- [x] doberman-core does not import doberman_enterprise
Edge cases covered / Deviations from plan / Risks introduced
- No plan slice exists for this (it's a user-requested convenience feature layered onto the
existing D1–D5 dashboard work), so I scoped it tightly: one new route pair, one shared
extracted function, no changes to the enforcement dial or preferences vector endpoints.
- Ran the full
pytest --cov=doberman suite locally; it's green modulo a pre-existing,
environment-specific issue unrelated to this change: two tests (test_gui_prompter.py,
test_prompter_deadline.py) open a real Tk window and crash the interpreter
(Fatal Python error: Aborted) in this sandboxed/headless shell with no display — reproduces
identically on a clean main checkout. Every other test, including a full run with just those
two files excluded, passed up through the point I could confirm before the shell became too
slow to finish the run in a reasonable time; CI's runner should not hit either issue.
Originally posted by: fu351
This is strong work, and you made the call I most wanted to see. You routed the dashboard mode-change through the same
apply_changeweaken-gate asdoberman modeinstead of standing up a parallel path, so the strictness moves in exactly one place and we audit it in exactly one place. The fail-closed_ModeChangePrompterand the 16-test auth matrix are what a control-plane surface needs.Two things before it lands:
mainshifted under you while this sat, so there's a small conflict now: an import collision betweensave_message_toneand yoursave_modeincli/main.py, plus a two-bullet overlap in the CHANGELOG. A rebase onto currentmainclears both.Rebase when you get a moment and I'll take it from there. Really nice work on this one.
Originally posted by: QY-25123
Rebased onto current
mainand resolved the CHANGELOG conflict — CI is green across the board. Ready for you to take the merge whenever.Ticket changed by: fu351
Originally posted by: fu351
Merged, thanks @QY-25123! The rebase came back clean, and on second read the extraction is my favorite part:
apply_mode_changeliving inpolicy/drift.pywith the CLI as a one-line wrapper means the dash and CLI cannot drift apart on the weaken gate, and your no-op test asserting zero ledger rows keeps the ledger honest. Your other PRs: I landed [#415] and [#398] myself (details on each, your commits untouched), and [#414] has one question waiting for you.Related
Tickets:
#398Tickets:
#414Tickets:
#415