Menu

#401 feat(dash): change the strictness mode from the dashboard

closed
nobody
None
2026-08-24
2026-08-17
Anonymous
No

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.

Related

Tickets: #436
Tickets: #437
Tickets: #448

Discussion

  • Anonymous

    Anonymous - 2026-08-19

    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_change weaken-gate as doberman mode instead 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 _ModeChangePrompter and the 16-test auth matrix are what a control-plane surface needs.

    Two things before it lands:

    1. main shifted under you while this sat, so there's a small conflict now: an import collision between save_message_tone and your save_mode in cli/main.py, plus a two-bullet overlap in the CHANGELOG. A rebase onto current main clears both.
    2. Because this is a control-plane weaken path, it gets a final security sign-off on our side before merge. I already went through it and found nothing wrong, so that step is policy for the risk class, not a worry about your code.

    Rebase when you get a moment and I'll take it from there. Really nice work on this one.

     
  • Anonymous

    Anonymous - 2026-08-19

    Originally posted by: QY-25123

    Rebased onto current main and resolved the CHANGELOG conflict — CI is green across the board. Ready for you to take the merge whenever.

     
  • Anonymous

    Anonymous - 2026-08-24

    Ticket changed by: fu351

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-08-24

    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_change living in policy/drift.py with 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: #398
    Tickets: #414
    Tickets: #415


Log in to post a comment.