feat(#719): confirm-gate operationally-disruptive config fields (keep the perimeter host-only)#727
Merged
Merged
Conversation
… the perimeter host-only) The dashboard config editor now commits a small set of disruptive-but-recoverable settings behind a type-to-confirm instead of hard-refusing them, while the security perimeter stays host-only in every direction. In scope (now confirm-gated, previously host-only DEST): - monero/tari/p2pool/dashboard .data_dir moves (re-sync) - p2pool.stratum_port (rigs repoint) - monero/tari clearnet_initial_sync ENABLE (host IP exposed during IBD) - monero.prune ENABLE (off->on); DISABLE stays host-only DEST describe_change is the source of truth: it emits a new CONFIRM flag per in-scope DIRECTION. The approval gate lets CONFIRM keys past the default-deny (a second allowlist, CONTROL_DASHBOARD_CONFIRM_KEYS), still refuses every DEST perimeter row, and requires a typed "APPLY" (carried on the commit intent) before a CONFIRM row proceeds. A dashboard-confirmed change is audited as a distinct `commit-confirmed` action. Perimeter guards left intact: wallets/view keys, dashboard auth, onion.*, network.tor_egress_firewall, dashboard.control.enabled, stratum password, workers.list[]/dashboard.workers[], node RPC creds, the closed-schema guard. UI: _confirm_keys surfaces the confirm-gated paths; markEditable renders them editable-with-confirm (not greyed); PreviewModal arms the type-APPLY box for CONFIRM rows. Type-to-confirm is UX friction, not a security control. Tests: gate unit (in-scope -> confirm-required not refused; wrong token refused; APPLY applies + audits commit-confirmed; perimeter incl. tor_egress_firewall/view_key /prune-disable refused even WITH the token), frontend markEditable + PreviewModal confirm flow, _confirm_keys served + drift guard. Docs + CHANGELOG updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two #719 perimeter cases in the control-gate suite (tor-egress-firewall disable, monero.view_key set) committed WITHOUT a confirm token via gate_try, so they only proved token-LESS refusal — which held before #719. The mandate was to prove the perimeter stays refused EVEN WITH a valid confirm="APPLY" present. gate_try now takes an optional token arg; both perimeter cases pass "APPLY". The refusal fires at the security-sensitive/DEST gate before the confirm check consumes the token, so they stay rejected — proving the typed confirmation is UX friction, not a perimeter unlock. All other gate_try calls stay token-less (unchanged). make test-stack: 1560 passed, 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ol_service Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #719.
What this does
Lets operators edit a small set of operationally-disruptive config from the dashboard behind a type-to-confirm, instead of the current hard-refuse. The security perimeter stays host-only. Ships as v1.11 (feature + control-channel security-model change).
In scope — now confirm-gated (previously host-only DEST)
These render editable-with-confirm and commit only behind a typed
APPLY:MONERO_DATA_DIR,TARI_DATA_DIR,P2POOL_DATA_DIR,DASHBOARD_DATA_DIR(re-sync)STRATUM_PORT(p2pool.stratum_port; rigs must repoint)MONERO_CLEARNET_SYNC,TARI_CLEARNET_SYNC(host IP exposed during IBD, auto-reverts to Tor)MONERO_PRUNEoff→on onlydescribe_changeis the source of truth: it emits a newCONFIRMflag per in-scope direction. The heavier direction still emitsDESTand stays refused (prune DISABLE → full re-sync;TOR_DATA_DIRmove).Perimeter — MUST stay refused (guards I did NOT change)
The typed confirmation is UX friction, not a security control (a compromised dashboard can fill the box), so the boundary is unchanged. These stay host-only, verified by tests:
monero.view_key), node RPC credsonion.*network.tor_egress_firewalldashboard.control.enabled(the control channel itself)workers.list[]/dashboard.workers[]descriptor guard (unchanged)The gate still
DEST-refuses every perimeter row, and the default-deny closed allowlist is only widened by a second allowlist (CONTROL_DASHBOARD_CONFIRM_KEYS) that carries exactly the in-scope keys — which still must clear the perimeter and the typed-confirmation check.How the confirmation flows
configview.mjscommit →POST /api/control/commitnow carriesconfirm(the typed word) for disruptive changes →server.py→control_service.submit(confirm=…)→ request spool.pitheadcontrol_process_requestallows theconfirmkey and passes it tocontrol_commit→control_approval_gate "$staged" "$confirm". ACONFIRMrow proceeds only ifconfirm == "APPLY".commit-confirmedaction (vs an ordinarycommit).Tests
Stack gate (
tests/stack/run.sh):confirm-gated candidate previews destructive:true+ carries aCONFIRMrowconfirm-gated commit without a token is refused/refusal asks for the typed APPLYconfirm-gated commit with the wrong token is refusedconfirm-gated commit with APPLY applies+confirmed commit audits as commit-confirmed with the key nameperimeter RPC-LAN change is refused despite the APPLY token,prune DISABLE is refused despite the APPLY token, plustor-egress-firewall disable,monero view-key set, wallet/auth/onion/xvb/healthchecks/workers all refuseddescribe_changeunit: prune enable=CONFIRM / disable=DEST; stratum port=CONFIRM; monero/dashboard data_dir=CONFIRM,TOR_DATA_DIR=DEST; clearnet enable=CONFIRM / disable=INFOPython (
test_control_service.py):_confirm_keysserved + perimeter excluded + disjoint from editable;test_confirm_keys_have_no_intra_repo_drift;commit carries the typed confirmation.Frontend:
markEditableconfirm flag (+ editable-wins);PreviewModalwarns and gates Confirm behind the typedAPPLY; form field renders confirm-gated tooltip.Docs + CHANGELOG (
[Unreleased], Added/Changed) updated in house voice.Local:
make test-stack(1560 pass),make test-dashboard(1467 pass, control_service 100%),make test-frontend(227 pass),make lint-sh/py/js/md/docs-voiceclean, patch coverage 100%. (lint-protoneeds docker; no proto touched.)This changes the control-channel approval semantics (#33/#537 hardening).