Skip to content

release: v1.10.0 → main - #709

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

release: v1.10.0 → main#709
VijitSingh97 merged 12 commits into
mainfrom
develop

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

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

VijitSingh97 and others added 12 commits July 19, 2026 15:55
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>
@VijitSingh97
VijitSingh97 merged commit c6e238d 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