release: v1.11.0 → main#736
Merged
Merged
Conversation
back-merge: main → develop after v1.10.2
…-1) (#707) * feat(#196): expose blocks/disk_growth/xvb_history on /api/state (Tier-1) The #196 telemetry backbone (PR #600) added persisted SQLite tables and capture hooks for five series but shipped without surfacing any of them to the client. This adds the missing /api/state exposure for the three Tier-1 series — blocks, disk_growth, xvb_history — range-filtered like share_stats and bucket-averaged past the existing 700-point chart cap for the two higher-cadence series. network_history and worker_history (Tier-2) are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(#196): fold disk_growth/xvb_history formatters into one gauge-series helper ponytail-review: build_disk_growth and build_xvb_history duplicated the same filter -> downsample -> {x, ...cols} shape, differing only in which columns they carry. Shared _gauge_series helper, net -13 lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…711) * feat(#530): audit out-of-band config changes, group the trail by time Detects two kinds of config drift the #33 control-channel audit never saw: config.json changing without a matching dashboard commit (host-edit), and a rig applying a control-API change the dashboard never sent (rig-edit). Both append key-names-only rows to a new durable audit_events table, which also mirrors control.log so the Security panel's new hour/day/month grouping can drill deeper than the log's own trimmed tail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#530): drop unearned "reuse wherever" framing from a single-use CSS class ponytail-review pass: .card-header-row has exactly one caller today (the audit trail's grouping select) — the comment claimed forward-looking genericity it hasn't earned yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#530): bound rig-edit audit rows, re-mask host config, pin correlation window Review follow-up on PR #711: - HIGH: rig-edit re-fired every ~30s poll into a permanent, never-pruned table — a rig keeps reporting its last terminal change_id, and the random row id defeated INSERT OR IGNORE. Fix: deterministic id `rig-edit-<worker>-<change_id>` (bounds the table to distinct real edits, survives restart) plus an in-memory (worker, change_id) guard that skips the redundant per-poll DB work. A rogue unauthenticated rig can no longer flood the trail with one bogus change_id. Tested across two consecutive polls and across a simulated restart. - MEDIUM: _read_host_config now re-applies the SECRET_PATHS mask (mirroring control_service.read_config's defense-in-depth pass), so a host-side masking regression can't leave a raw secret value resident in _last_host_config. The mount is already the pre-masked copy (#440) — the raw config.json is never mounted — so this is belt-and-suspenders, not a live leak; docstring corrected to state that accurately. - LOW: pinned the ≤1s "explained by a fresh commit" correlation window with a boundary test and named the ceiling in a ponytail comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#530): sanitize all audit-row fields, add accessible name to grouping select Security review follow-up on PR #711: - _record_audit_event only ran actor/keys through audit_service._clean, despite the docstring's claim every field gets the same whitelist treatment. action/ status/source were code-controlled closed-enum values already, so this closes the gap defense-in-depth (no behavior change on any real input). - The hour/day/month <select> in AuditCard had no accessible name (no aria-label, no associated <label for>) — a regression against this repo's own precedent (xvb-tier-select, chart-controls role=group aria-label, etc). Added aria-label="Group audit trail by", pinned with a test assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#530): correlate host-edit detection BY KEY, not just by time The out-of-band host-config watcher (`_watch_host_config`) suppressed a detected config.json diff whenever ANY control-channel commit/applied entry landed in the correlation window, regardless of which keys that commit touched (`explained = any(...)`). A legit dashboard commit of key A concurrent with a host-side hand-edit of key B silently swallowed B — the exact out-of-band change this feature exists to catch. Now each fresh commit only explains the keys it actually changed: its audit `keys` (env-var names) are bridged to config.json paths via the new `control_service.env_key_config_paths` (reusing EDITABLE_ENV_KEY_PATHS, plus the synthetic DASHBOARD_ENERGY -> `dashboard.energy` prefix the commit gate folds in). Any changed key no recent commit covers is still recorded. The 1s time window stays as an outer bound. Tier-1 tests: a commit of key A + host-edit of key B records B (not A), and an energy-subkey commit is explained by prefix. Both fail on the old time-only logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…health failures (#708) * feat(#490): opt-in fail-closed miner hold on unrecoverable dashboard health failures Adds dashboard.fail_closed (default false). Off, an unrecoverable failure only alerts (existing #131/#337 paths) and mining continues, since the dashboard is an observability layer independent of the xmrig-proxy -> p2pool -> monerod datapath. On, it reuses the #35 sync gate's own hold (stop/start SYNC_GATE_CONTAINERS) rather than a new path, but — unlike that one-way latch — re-checks every cycle and releases once the condition clears. Scoped narrowly to genuine, non-transient failures: the DB auto-heal (#489) rebuild itself failing (StateManager.is_db_unrecoverable, distinct from the existing db_healthy flag, which also covers an ordinary transient write error that must never gate), or the dashboard container crash-looping (ContainerHealthMonitor.is_bad("dashboard")). XvB/payout state-integrity loss already safes to plain p2pool via algo_service's existing fail_count>=3 guard and isn't duplicated here. Registered in config.reference.json and both drift guards (pithead's CONTROL_DASHBOARD_EDITABLE_KEYS, control_service.py's EDITABLE_ENV_KEY_PATHS) so it round-trips the #33 control-approval path the same as dashboard.tari_required. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: share the SYNC_GATE_CONTAINERS stop/start loop between the sync gate and the new fail-closed gate ponytail-review finding: _apply_sync_gate and _apply_fail_closed_gate duplicated the same stop-every-container / start-every-container-tracking-ok loops. Extract _stop_gate_containers(quiet) / _start_gate_containers() and use them from both, so the two holds over the same container set can't drift out of step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#490): gate fail-closed on a debounce-CONFIRMED bad verdict, not a first-sighting seed Coordinator finding 1: is_bad("dashboard") read the raw level, which ContainerHealthMonitor.update() seeds to "bad" on a first-sighting unhealthy snapshot WITHOUT clearing the 120s continuous-unhealthy debounce a known container must pass. That seed is intentionally silent for alerting, but the fail-closed gate read it as a live truth, so a dashboard reported unhealthy at first sighting could hold the fleet on an unconfirmed, possibly-transient state — contradicting the "only confirmed, non-transient failures gate" intent. Fix: rename is_bad -> is_confirmed_bad and require c["alerted"] (True only when the debounce produced an alert edge — crash loop or unhealthy past the threshold), so a silently-seeded baseline never gates. One-line predicate change; the alerted flag already existed. Finding 2: added classification-layer tests pinning transient-never-gates — seed not confirmed, single/early unhealthy not confirmed before 120s, confirmed only after the debounce (test_container_health); the disabled-sinks alert path still feeds the tracker and confirms through the 120s debounce (test_alert_service); and the real trigger expression (is_db_unrecoverable OR is_confirmed_bad) built from real objects does not gate on a seed (test_data_service). The is_db_unrecoverable-only-after-failed-rebuild and transient-write-error-does-not vs. gate cases were already covered in test_storage_service. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#490): add CHANGELOG entry + fix testing-strategy method name - CHANGELOG: open the [Unreleased] section (consumed into [1.10.0] at the last release-prep) with the dashboard.fail_closed Added entry, matching the per-feature-PR convention #596/#597 followed. - testing-strategy.md: the shipped accessor is is_confirmed_bad, not is_bad; also mark tier-3 deferred (· 3 ▶) to match the sibling docker-control stop/start rows and the PR's own deferred-to-gouda note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
) * feat(#249): warm the backup stack's XvB donation state on failover A two-host failover pair (same wallet, workers listing both hosts in pools[]) cold-started the backup's XvB donation controller when the fleet failed over to it: the closed-loop split restarted from the feedforward estimate and re-ramped for hours, over/under-shooting the credited tier. Persist the controller's commanded donation fraction so a plain restart resumes the warmed split, and add a one-way, backup-pulls-from-primary standby channel: the backup points xvb.standby.source at the primary dashboard's new read-only /api/xvb-standby endpoint, periodically pulls the primary's controller state, holds it as standby (inspectable in /api/state), and adopts it the first time it actually donates at failover. Seed precedence: this host's own persisted commanded fraction (a restart) beats the primary's standby (a first failover), which beats the cold feedforward estimate. Inert unless configured; an idle backup has no workers so it never donates while the primary is authoritative. An .onion source rides Tor, a LAN source dials direct — no new clearnet egress. The source URL is a capability secret (it can carry the primary's dashboard basic-auth), so pithead renders it into the owner-only .env and masks it in the control-channel config, mirroring the Healthchecks ping URL. Tier-1 unit coverage for persistence, warm-resume seed precedence, cold-start fallback, the standby puller, and the endpoint. Live 2-host failover validation is tier-4 and deferred. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#249): route XvB standby pull through Tor unless source is a private IP Three security fixes on the #249 warm-standby puller: - CRITICAL clearnet leak: _proxies() only sent .onion sources over Tor; every other host (public IP or hostname) fell through to a direct dial, leaking the backup's real IP and the backup↔primary relationship (#160 threat class). Now mirrors egress.py's carve-out: onion or any non-provably-private source rides Tor; only a private/loopback/link-local IP literal dials direct. Classification shared via egress._xvb_standby_route so the puller and the Security panel can't drift. - HIGH secret in logs: the startup line printed the full source URL (which can embed basic-auth userinfo). Now logs enabled-state only, like healthchecks.py never logging the ping_url. - MEDIUM blind panel: added the xvb_standby route to compute_egress_posture and compute_topology (tor / local / inactive, never clearnet) so the panel tells the truth about where the pull goes. Corrects the now-false "no new clearnet egress either way" claim in the docstrings and CHANGELOG. Tests cover onion/private/public/hostname routing, log secrecy, and the egress classification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: re-trigger workflows --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(#593): opt-in local miner — hand off stratum URL + secret for a co-located RigForge worker A box that runs the stack 24/7 can mine with its spare CPU by co-locating a RigForge worker on the stack host, pointed at the stack's own loopback stratum. Pithead's side is purely intent + hand-off: - `local_miner.enabled` config flag (default off), registered in config.reference.json (closed-schema superset invariant). - A `pithead setup` prompt ("Also mine on this machine with its spare CPU?", default N) that writes the flag. - announce_local_miner(): on setup/apply, surface the two values a RigForge install would otherwise prompt for — the pool URL (loopback 127.0.0.1:PORT, or the bound p2pool.stratum_bind address for the LAN-bind edge case) and the stratum secret already in .env — so the co-located rig self-registers through the proxy like any other worker. Pithead adds NO HugePage/GRUB/MSR/governor code: RigForge owns all host-level tuning and the miner service. Docs (docs/workers.md) state that ownership. Tier-1 shell coverage: announce_local_miner opt-out/opt-in, loopback vs LAN bind, no-password case; wizard prompt-count pin; wizard opt-in write. A real co-located mining run is tier-4 (deferred). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#593): map local_miner into the Mining logical group (config-group completeness) The new local_miner.enabled config leaf fell into the catch-all Other group, failing the buildSections completeness test that every reference leaf resolves to a real group. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… the perimeter host-only (#727) * feat(#719): confirm-gate operationally-disruptive config fields (keep 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> * ci: re-trigger workflows * test(#719): perimeter refusal tests now carry a valid APPLY token 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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#713 message change The #719 prune-disable refusal test asserted the stale '#338' string that #713 (v1.10.2) removed from the destructive refusal; updated it to the new 'Edit config.json on the host' message. CHANGELOG unions the v1.11 [Unreleased] over the [1.10.2]/[1.10.1] sections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llapse gap-fix) Two test assertions (prune-disable, tor-egress) and two docs (SECURITY.md, dashboard.md) still carried the stale '#338 out-of-band approval' reference that #713 removed. Also updates SECURITY.md's control-channel model to reflect #719 (operational fields now confirm-gated; perimeter never dashboard-committable), not the old default-deny-everything. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ard in the docker group across 1 directory (#729) Bumps the docker group with 1 update in the /build/dashboard directory: python. Updates `python` from `baf8980` to `db3ff2e` --- updated-dependencies: - dependency-name: python dependency-version: 3.11-slim dependency-type: direct:production dependency-group: docker ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](actions/checkout@9c091bb...3d3c42e) Updates `actions/setup-python` from 6.3.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@ece7cb0...5fda3b9) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/setup-python dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#731) Bumps the python group in /build/dashboard with 3 updates: [aiohttp](https://github.com/aio-libs/aiohttp), [hypothesis](https://github.com/HypothesisWorks/hypothesis) and [ruff](https://github.com/astral-sh/ruff). Updates `aiohttp` from 3.14.1 to 3.14.2 - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](aio-libs/aiohttp@v3.14.1...v3.14.2) Updates `hypothesis` from 6.156.6 to 6.158.0 - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](HypothesisWorks/hypothesis@v6.156.6...v6.158.0) Updates `ruff` from 0.15.21 to 0.15.22 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](astral-sh/ruff@0.15.21...0.15.22) --- updated-dependencies: - dependency-name: aiohttp dependency-version: 3.14.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python - dependency-name: hypothesis dependency-version: 6.158.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python - dependency-name: ruff dependency-version: 0.15.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…d flood (#733) The out-of-band audit trail (#530) writes a permanent `rig-edit-<worker>-<change_id>` row into `audit_events` whenever a worker reports a terminal change_id this dashboard never issued. That feed is unauthenticated LAN input: #530's deterministic id collapses REPEATS of one change_id to a single row, but a rogue device can report a fresh random change_id every ~30s poll — each a new permanent row in a table that has no pruning. A slow-burn, attacker-driven disk-fill DoS from any device on the miner LAN. Cap NEW rig-edit rows per worker per rolling hour (`_RIG_EDIT_CAP_PER_HOUR`, in-memory fixed window like `_flagged_rig_changes`). Beyond the cap the row is dropped — but never silently: on the transition a single `rate-limited` marker row (deterministic id keyed to the worker's window start, so it's idempotent across a restart) is recorded and a warning logged, so the flood stays visible in the Security panel. A genuine occasional rig change still records normally; only a flood is capped. The #530 same-change_id dedup and the non-attacker-controllable host-edit / mirrored control.log paths are untouched. Worker-name rotation sidesteps a per-worker cap — that's the broader unauthenticated-feed vector (#235), out of scope here and flagged in-code; the cap is keyed on the worker per the issue so one rogue rig can't crowd genuine rig-edit history out. Docs: SECURITY.md / operations.md / dashboard.md qualify #530's "permanent, no pruning" note with the cap; CHANGELOG under [Unreleased] › Security. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… data root (#734) * feat(#728): allowlist dashboard-confirmed data-dir moves to the stack data root #719 made the four *_DATA_DIR moves confirm-gated, so a dashboard operator who types APPLY can now relocate a service's data dir. The destination was still checked only by assert_safe_dir — a BLOCKLIST that refuses the catastrophic roots but passes any other absolute path. At host-shell trust that is proportionate; at dashboard trust it lets a confirmed move target another user's home or another service's volume and have pithead mkdir/chown -R it and bind-mount it into a recreated container — a destination trust-escalation. control_approval_gate runs ONLY for dashboard commits (the host `apply` path never calls it), so it is exactly where the control-only rule belongs. For a control-channel move, narrow the destination from a blocklist to an ALLOWLIST: permit only a path under the stack's own data root ($PWD/data) or a parent the stack already keeps data in (each live *_DATA_DIR's parent — covers a #455 co-located data root). Anything else is refused even with the APPLY token and stays host-CLI only. The host `./pithead apply` path keeps the wider blocklist. Tier-1 stack tests: a control-confirmed move under the data root applies with APPLY; a move to an arbitrary non-blocklisted-but-non-allowed path is refused even with APPLY; the same path from the host shell still applies. Docs: SECURITY.md, docs/dashboard.md, CHANGELOG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#728): match resolve_default's exact sentinel set, not DYNAMIC_* wildcard Security review flagged the skip-condition `DYNAMIC_*` as broader than the values that actually resolve to an in-root stack default. resolve_default defaults exactly "" | auto | DYNAMIC_DATA | DYNAMIC_HOST | DYNAMIC_ID and passes anything else (e.g. DYNAMIC_FOO) through literally. Mirror that exact set so the allowlist skip can never swallow a non-sentinel, and pin the assert_safe_dir- runs-first ordering that keeps `..`/relative dests from reaching this block. Not exploitable before this change (a DYNAMIC_-prefixed string can't be absolute, and assert_safe_dir refuses it in the dry-run re-derivation first) — tightens a latent defense-in-depth mismatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v1.11: collapse fleet-resilience & hardening features into develop
Bump VERSION + pyproject + uv.lock to 1.11.0 in lockstep; roll CHANGELOG [Unreleased] → [1.11.0] - 2026-07-21. v1.11 theme: telemetry persistence (#196), fail-closed miner hold (#490), out-of-band config/rig audit (#530) + flood cap (#724), opt-in local miner (#593), XvB standby warm-up (#249), confirm-gated operational config editing (#719) + data-dir move allowlist (#728). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
release: prep v1.11.0 — fleet resilience & hardening
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.
Promote v1.11.0 to
main(released-only branch). Real merge (not squash) —mainstays an ancestor ofdevelop.Tag
v1.11.0is on develop HEAD (6850580); nothing landed after the cut, so this is the straight develop→main shape. Back-merge main→develop follows.Contents: the v1.11 fleet-resilience & hardening theme (#196/#490/#530/#724/#593/#249/#719/#728) + dep bumps (#729/#730/#731), collapsed via #732, prep #735.