Skip to content

release: v1.11.0 → main#736

Merged
VijitSingh97 merged 18 commits into
mainfrom
develop
Jul 21, 2026
Merged

release: v1.11.0 → main#736
VijitSingh97 merged 18 commits into
mainfrom
develop

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Promote v1.11.0 to main (released-only branch). Real merge (not squash) — main stays an ancestor of develop.

Tag v1.11.0 is 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.

VijitSingh97 and others added 18 commits July 21, 2026 08:58
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
@VijitSingh97
VijitSingh97 merged commit 24a2260 into main Jul 21, 2026
32 checks passed
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