release: v1.10.0 → main - #709
Merged
Merged
Conversation
Back-merge main into develop after v1.9.3
… bench hostnames (#685) docs/dev/releasing.md documents the two required matrix runs (readiness in release.sh stage 2, the borrowed-rig e2e.sh --mode targeted before the cut) and the post-deploy --check sweep with its expected parked-bench baseline. Private bench hostnames come out of the repo: the release-server allocation table names roles instead of boxes (each box's ~/README.md carries its specifics), and the stray box names in comments, one changelog entry, and the rig-lock busy message are generic. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… API fetches (#686) * security(#660): shared response-size cap for every external HTTP fetch All five external call sites — the GitHub release check (#224), the three XvB reads (stats, reward estimates, winners), and the CoinGecko price feed (#651) — read via a shared helper/http.py bounded_get that streams the body and cuts it at 1 MiB. Over-cap raises a RequestException subclass, so each client's existing fail-silent contract (None / keep last good) applies unchanged. Known residual from the #642/#646 security reviews; the local Telegram/healthchecks/monerod clients keep their own contracts and stay out of scope. Closes #660 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * security(#660): review fixes — scope the claim to the five capped clients, pin the at-cap boundary, widen the drift guard The changelog headline claimed "every external dashboard fetch" is size-capped, but the Tor heal probe, Telegram long-poll, and healthchecks ping keep their own contracts — the entry now names the actual scope: the update check, the price feed, and the XvB client's calls. A new tier-1 test pins the strictly-greater cap (a body of exactly max_bytes succeeds; a regression to >= fails it), and the wiring drift guard now rejects any direct requests.<verb>( in the client modules, not just requests.get(. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: ruff format on the at-cap boundary test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* security(#660): shared response-size cap for every external HTTP fetch All five external call sites — the GitHub release check (#224), the three XvB reads (stats, reward estimates, winners), and the CoinGecko price feed (#651) — read via a shared helper/http.py bounded_get that streams the body and cuts it at 1 MiB. Over-cap raises a RequestException subclass, so each client's existing fail-silent contract (None / keep last good) applies unchanged. Known residual from the #642/#646 security reviews; the local Telegram/healthchecks/monerod clients keep their own contracts and stay out of scope. Closes #660 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(#596): per-worker RigForge new-version badge One fleet-wide, hourly, Tor-routed fetch of the latest RigForge release (UpdateChecker.latest_release_cached, gated on the same dashboard.check_for_updates flag) cached as raw {tag, url}; each rig's badge is derived at the render seam from its live reported version (rigforge_update_for), so derived state never outlives its inputs (#664) and a rig on X can never badge X available. compute_update normalizes the rig's bare 1.11.2 against the tag's v1.11.2. Badges render in the Workers Alive table and Worker Inspect; rigs reporting no version show no badge (unknown, not up-to-date). Closes #596 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#596): review fixes — snapshot restore drops rigforge_release; docstring + catalog row - Snapshot restore popped `update` (#664) but not `rigforge_release`, so with check_for_updates flipped off a restored snapshot re-seeded stale per-worker badges until the first poll cycle. Pop it alongside `update`; the checker re-fetches on its own cadence. Extends the #664 restore test (fails without the pop). - update_checker docstring said the check is opt-in (default off) — the config default and docs/configuration.md say on by default. Docstring now matches. - testing-strategy scenario catalog (Section I) gains the #596 badge row: tier 1 covered today, live badge owed to the #597 gouda loaner session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…checks ping, Telegram getUpdates (#699) * security(#660): shared response-size cap for every external HTTP fetch All five external call sites — the GitHub release check (#224), the three XvB reads (stats, reward estimates, winners), and the CoinGecko price feed (#651) — read via a shared helper/http.py bounded_get that streams the body and cuts it at 1 MiB. Over-cap raises a RequestException subclass, so each client's existing fail-silent contract (None / keep last good) applies unchanged. Known residual from the #642/#646 security reviews; the local Telegram/healthchecks/monerod clients keep their own contracts and stay out of scope. Closes #660 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * security(#660): review fixes — scope the claim to the five capped clients, pin the at-cap boundary, widen the drift guard The changelog headline claimed "every external dashboard fetch" is size-capped, but the Tor heal probe, Telegram long-poll, and healthchecks ping keep their own contracts — the entry now names the actual scope: the update check, the price feed, and the XvB client's calls. A new tier-1 test pins the strictly-greater cap (a body of exactly max_bytes succeeds; a regression to >= fails it), and the wiring drift guard now rejects any direct requests.<verb>( in the client modules, not just requests.get(. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * security(#698): bound the remaining external GETs — Tor probe, Healthchecks ping, Telegram getUpdates The #660 cap now covers every external GET. The Tor egress probe and the Healthchecks ping are one-word conversions: both already treat any RequestException as failure, so an over-cap body follows the existing contract. The Telegram long-poll keeps its (connect, read) timeout tuple — bounded_get streams, but headers only arrive once the hold ends, so the read timeout still spans it — and gains a getUpdates limit of 10: the offset only advances after a batch is parsed, so an uncapped batch that tripped the size cap would be re-fetched forever and wedge the poll loop. Offset priming drains batch by batch to keep skipping the whole startup backlog. BoundedResponse grows the raise_for_status the long-poll already called, raising HTTPError to match requests' contract. Closes #698 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: ruff format on the at-cap boundary test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(#698): pin the long-poll (connect, read) timeout tuple Issue #698's first hold-while-converting invariant — the read timeout must outlast Telegram's long-poll hold — had no test: dropping the tuple would abort every legitimate poll mid-hold and nothing would fail. Assert it behaviourally (read > requested hold) in the existing getUpdates wiring test instead of pinning the exact numbers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs: add missing egress to the architecture diagram — CoinGecko price feed, worker-API LAN polling, XvB raffle calls Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: worker-API prose — config applies are the host runner's dial, not the dashboard's Review fix: #185 deliberately keeps the rig tokens out of the dashboard container — the dashboard spools an intent and the host-side control runner makes the HTTP call. Attribute the apply dial correctly, and drop the absolute 'never touches the internet' (the stats probe dials the rig's address directly; the code allows non-LAN miner IPs) in favor of what the code guarantees: direct LAN dials that never route over Tor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…692) * fix(#689): only remove control-runner units the acting checkout owns The pithead-control.{path,service} names are box-global, but a release bench holds several checkouts (live stack + e2e harness + bundle-smoke tmp dirs). Both removal paths — provision_control_runner's disabled branch and the e2e teardown reap — deleted whatever units were installed, stranding the live stack's dashboard control requests (the config editor sat at "Previewing…" until the next apply reinstalled the units; hit live on the bench 2026-07-19, twice in one afternoon). Key both removals on the service unit's ExecStart matching $PWD, with grep -F throughout — versioned dirs carry dots (pithead-v1.9.3), and a regex dot could claim a sibling's unit as our own. Foreign-owned units are left in place (the e2e reap counts that as success); a dangling path unit with no service file is still reaped. PITHEAD_UNIT_DIR env seam added for the tier-1 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#689): compare unit ownership by physical path; add the changelog entry Review fixes for the ownership check. The literal ExecStart=$PWD match missed that one checkout has two path spellings: production units carry the versioned dir, while an operator's disable apply runs through the `current` symlink — the apply saw its own unit as foreign and never removed it (fails safe, never converges). Both removal paths now extract the ExecStart path from the service unit and compare physical resolutions (deepest existing ancestor via cd/pwd -P, remainder kept verbatim; unparseable ExecStart stays foreign). The e2e teardown mirror in tests/integration/run.sh gets the same treatment, and the tier-1 block gains the versioned-spelling-via-symlink case — it fails on the pre-fix literal comparison, and the foreign-owner cases still hold. Also the missing Keep-a-Changelog entry under [Unreleased]. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(#695,#696): unedited Save & preview shows zero changes Two spurious rows appeared in the dashboard's Review changes modal on a bundle-deployed box with nothing edited: - SCRIPT_DIR now resolves with pwd -P (#695), so every $PWD-derived .env path (CLEARNET_STATE_DIR, CONTROL_DIR, CADDY_LOG_DIR, the Tari wallet secret file) renders the same physical spelling however pithead is invoked — the deploy symlink interactively and the systemd control runner on the real dir used to diff the same directory against itself, and alternating invocations churned .env back and forth. CLEARNET_STATE_DIR joins CONTROL_DIR/CADDY_LOG_DIR as a silent internal path in describe_change. - The #504 energy preview check merges the config.reference.json defaults into BOTH sides before comparing (#696): the editor round-trips the reference-merged form, so a config.json that never set dashboard.energy staged the materialized defaults and raised "Energy calculator settings updated" on every unedited save. The test sandbox is canonicalized with pwd -P so macOS's /var symlink doesn't leak into path assertions now that pithead resolves it. Closes #695. Closes #696. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#696): gate audit-key derivation merges energy defaults like the preview leg The verifier pass on #700 caught the commit-side sibling of the preview fix: control_approval_gate's synthetic DASHBOARD_ENERGY audit key used the old strict comparison, so any reference-merged dashboard commit on a config.json with a sparse dashboard.energy block audited a phantom energy change. Same merge-both-sides expression as the preview leg, and the unedited round-trip test now commits and asserts the audit line carries no DASHBOARD_ENERGY. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* security(#660): shared response-size cap for every external HTTP fetch All five external call sites — the GitHub release check (#224), the three XvB reads (stats, reward estimates, winners), and the CoinGecko price feed (#651) — read via a shared helper/http.py bounded_get that streams the body and cuts it at 1 MiB. Over-cap raises a RequestException subclass, so each client's existing fail-silent contract (None / keep last good) applies unchanged. Known residual from the #642/#646 security reviews; the local Telegram/healthchecks/monerod clients keep their own contracts and stay out of scope. Closes #660 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(#596): per-worker RigForge new-version badge One fleet-wide, hourly, Tor-routed fetch of the latest RigForge release (UpdateChecker.latest_release_cached, gated on the same dashboard.check_for_updates flag) cached as raw {tag, url}; each rig's badge is derived at the render seam from its live reported version (rigforge_update_for), so derived state never outlives its inputs (#664) and a rig on X can never badge X available. compute_update normalizes the rig's bare 1.11.2 against the tag's v1.11.2. Badges render in the Workers Alive table and Worker Inspect; rigs reporting no version show no badge (unknown, not up-to-date). Closes #596 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(#597): one-click remote worker upgrade Fuses the two existing control templates: worker-apply's rig resolution (address + bearer from the HOST config, never the intent; charset guards; per-drain budget — one upgrade per drain) and the stack upgrade's throttled host-side target re-derivation (RigForge releases API over Tor, 10-min tag cache + pre-dial stamp so a flooding container costs at most one egress per window). POST :8082/upgrade with the host-derived tag; poll :8082/status matching change_id to applied/rolled_back/failed with a 12-min cap (builds ~10 min); rig-side throttle refusals map to a calm 'throttled' retry-later status. Dashboard: POST /api/control/worker-upgrade spools {worker, version} only and 202s immediately; a rig already reporting the target version no-ops without spooling (never burns the rig's 6h window). Inspect gains an arm→confirm Upgrade button where the #596 badge shows. Requires rig >= v1.11.2 (documented; earlier ancestry guard refuses fresh-clone upgrades). Per-rig only — no upgrade-all. Closes #597 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(#596): review fixes — snapshot restore drops rigforge_release; docstring + catalog row - Snapshot restore popped `update` (#664) but not `rigforge_release`, so with check_for_updates flipped off a restored snapshot re-seeded stale per-worker badges until the first poll cycle. Pop it alongside `update`; the checker re-fetches on its own cadence. Extends the #664 restore test (fails without the pop). - update_checker docstring said the check is opt-in (default off) — the config default and docs/configuration.md say on by default. Docstring now matches. - testing-strategy scenario catalog (Section I) gains the #596 badge row: tier 1 covered today, live badge owed to the #597 gouda loaner session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(#597): pin the poll-cap timeout, GitHub tag derive, and rig-refusal paths Review fixes for #691. The sec-review's headline 90s poll cap gains a test seam (CONTROL_WU_POLL_CAP) and a stack case proving the timeout→accepted fallback; three new cases cover the GitHub tag derivation (fresh derive + cache write, unreachable-over-Tor refusal, no-usable-tag refusal) that every accept case had pre-cached away; one covers the rig non-202 refusal (where an old rig < v1.11.2 surfaces) incl. the 500-char cap on rig-supplied error text. testing-strategy Section I gains the #597 catalog row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(#597): pin the non-JSON 400 and submit-failure 500 endpoint branches The develop merge narrowed the patch-coverage diff to this PR's own lines, surfacing the two uncovered error branches the review had flagged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… the XvB stats fetch (#702) * fix(#701): egress panel no longer reports a phantom clearnet leak for the XvB stats fetch The #170 posture panel and topology view routed the dashboard's XvB stats connection through _xvb_route(xvb_enabled, xvb_tor), so xvb.tor: false displayed it as a clearnet leak. That fetch is unconditionally Tor-proxied (XvbClient defaults tor_proxy to TOR_SOCKS_PROXY, compose-pinned to the bridge SOCKS); xvb.tor gates only the xmrig-proxy donation dial (#166). The route is now TOR if xvb_enabled else INACTIVE, matching the code and docs/privacy.md. Tests that pinned the phantom leak now pin the corrected behaviour, plus an all-knobs sweep asserting the dashboard never derives a clearnet route. Closes #701 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(#701): scope the dashboard Tor sweep to the hard-wired clients The all-configs sweep asserted NO dashboard connection is ever clearnet. That invariant is broader than #701's claim and breaks the moment a dashboard egress with a legitimate clearnet mode lands (#380's alert-sink LAN carve-out in flight on another branch derives exactly that). Scope the sweep to the Tor-hard-wired clients by name — with a drift guard so a rename can't hollow it out — and pin the topology XvB stats edge as inactive when XvB is off, which only the posture side asserted before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(#380): egress posture missed the webhook/ntfy alert sinks The dashboard's egress panel and topology listed every other dashboard egress (XvB stats, update check, Healthchecks, Telegram, price feed) but not the #380 alert sinks. Add the conn and topology edge, derived from the live config: Tor when notifications.tor is on, clearnet (a real leak — the host-networked dashboard bypasses the #270 firewall) when it's off and a sink is public, inactive when unconfigured. The notifications.tor:false LAN carve-out routes as local, not a leak, when every configured endpoint is a private/loopback IP literal — the only proof available without a DNS lookup; hostname endpoints count as clearnet. Documented in docs/privacy.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review(#380 posture): changelog entry + pin the private-IP boundary cases The Unreleased section gets the Fixed entry the posture change warrants (user-visible egress-panel behaviour). The _sinks_all_private test picks up the boundary cases a privacy classifier must pin: IPv6 ULA, the real _notify_knobs shape (unset NTFY_URL beside a private webhook), localhost and userinfo hosts, CGNAT, and IPv4-mapped IPv6 to a public v4 address — the last locks in the CPython >= 3.11.10 mapped-address rules so a runtime downgrade can't silently classify a public POST as local. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ership + preview fixes (#703) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.10.0 to main (released-only branch). Tag v1.10.0 sits on develop HEAD (bba9f8b); nothing landed post-cut, so this is a clean straight develop→main real merge.
Contents: worker upgrade (badge #596 + one-click #597), bounded external fetches (#660/#698), control-runner unit ownership (#689), unedited-preview fixes (#695/#696), egress posture + phantom-leak fixes (#380/#701), architecture-diagram accuracy (#694).
Each constituent PR was per-PR reviewed (tests + docs), ponytail-gated, and CI-green before merge to develop. Lean already.
🤖 Generated with Claude Code