Skip to content

Pin & verify the XMRig build (v6.26.0) - #18

Merged
VijitSingh97 merged 3 commits into
mainfrom
fix/pin-xmrig-build
Jun 7, 2026
Merged

VijitSingh97 merged 3 commits into
mainfrom
fix/pin-xmrig-build

Conversation

@VijitSingh97

Copy link
Copy Markdown
Contributor

What & why

compile_xmrig previously ran git clone --quiet https://github.com/xmrig/xmrig.git, building whatever happened to be at the HEAD of master — unpinned and unverified. That makes builds non-reproducible and exposes the worker to supply-chain risk: a compromised or force-pushed upstream branch would be compiled and run with no detection.

This PR hardens the build:

  • Pinned release tag. Two overridable globals are added near the top of the script:
    XMRIG_VERSION="${XMRIG_VERSION:-v6.26.0}"
    XMRIG_COMMIT="${XMRIG_COMMIT:-b2ca72480c58d197e18c885d9fc1a0c8d517e60a}"
    The clone now fetches exactly that tag, shallowly:
    git clone --quiet --branch "$XMRIG_VERSION" --depth 1 https://github.com/xmrig/xmrig.git
  • Asserted commit. After cloning, the checked-out commit is verified against the pinned SHA and the build aborts (via the script's error helper) on any mismatch, so a moved/retagged release cannot silently change what gets compiled:
    actual="$(git -C xmrig rev-parse HEAD)"
    [ "$actual" = "$XMRIG_COMMIT" ] || error "XMRig commit mismatch: expected $XMRIG_COMMIT, got $actual"

The rest of compile_xmrig (the donate.h sed patch, cmake, make) is unchanged. The pins are environment-overridable for users who need a different release.

Possible further step

XMRig publishes GPG-signed release artifacts. A natural follow-up would be to fetch the signed release tarball/binary and verify it against the project's public key, giving cryptographic provenance on top of the tag + commit assertion in this PR.

Validation

  • bash -n rigforge.sh parses cleanly.
  • shellcheck rigforge.sh introduces no new warnings (the two pre-existing info-level findings, SC2086 and SC2016, are unchanged).

Related issue

Closes #2

🤖 Generated with Claude Code

VijitSingh97 and others added 3 commits June 3, 2026 19:13
Pin the XMRig source build to a specific release tag and assert the
checked-out commit, replacing the previous unpinned/unverified clone of
master HEAD. This makes the worker build reproducible and hardens it
against supply-chain tampering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#18 adds `git -C xmrig rev-parse HEAD` verification against the pinned
XMRIG_COMMIT. Teach the git stub to answer rev-parse, wire a known commit
through both E2E paths, and add coverage:
- unit: compile_xmrig builds + logs "Verified" when the commit matches, and
  aborts with "commit mismatch" when STUB_GIT_HEAD forces a tampered HEAD
- E2E (native + Docker): assert the pinned commit is verified during a run

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Merged main (resolved a globals conflict — kept both the pin globals and #26's overridable path globals) and added coverage for the commit verification: a unit test where compile_xmrig builds + logs "Verified" when the commit matches and aborts with "commit mismatch" on a tampered HEAD, plus the E2E now asserts the pinned commit is verified during a run. (Also taught the E2E git stub to answer rev-parse.) CI green, including the Docker e2e (19/19).

@VijitSingh97
VijitSingh97 merged commit 433191b into main Jun 7, 2026
3 checks passed
@VijitSingh97
VijitSingh97 deleted the fix/pin-xmrig-build branch June 7, 2026 07:00
VijitSingh97 added a commit that referenced this pull request Jun 7, 2026
* fix: prune old XMRig build archives on re-run (#4)

prepare_workspace archived xmrig -> xmrig-<timestamp> on every run and never
cleaned up, so archives accumulated forever (a disk leak). Keep the most recent
KEEP_ARCHIVES (default 3) and remove older ones.

This is the disk-leak half of #4. The `upgrade` command + fast no-op re-run
(skip recompile when the pinned XMRIG_COMMIT is already built) is the remaining
piece — now unblocked by the pin landed in #18.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: make archive pruning set -e/pipefail safe (no archives = no abort)

The first run has no xmrig-* archives, so the prune's `ls` failed on the empty
glob and aborted prepare_workspace under `set -Eeuo pipefail`. Capture the
listing with `|| true` and only prune when non-empty. Added a regression test
that runs prepare_workspace WITHOUT `set +e` so this can't slip through again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: idempotent re-runs + upgrade command (#4)

compile_xmrig now records the built commit and is skipped when the pinned
XMRIG_COMMIT is already built, so a re-run is a fast no-op (no needless
recompile, and the service is not restarted). prepare_workspace only archives
when actually rebuilding. Adds a minimal command surface: `setup` (default),
`upgrade` (rebuild+restart only on a version bump), and `help` — the broader
surface (status/logs/doctor) remains #11.

Tests: xmrig_already_built detection, compile_xmrig skip/rebuild, and black-box
upgrade no-op / help / unknown-command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
VijitSingh97 added a commit that referenced this pull request Jul 1, 2026
Adds ~58 assertions for previously-untested branches and degenerate
inputs, all at the run.sh layer (stub-driven, hardware-independent,
bash 3.2-safe) per tests/README.md — no new files, SHELL_FILES unchanged.

Behaviours now covered:
- check_prerequisites: the whole jq-bootstrap (apt/dnf/pacman/none/brew/
  no-brew/already-present) — was entirely untested
- install_dependencies: the dnf and pacman branches + the no-manager
  warn-not-abort path + the apt-cache-present linux-tools branch
- install_service reboot-gate (#audit A2): enable-but-NOT-start when a
  reboot is pending, restart on rebuild, start on steady state, plus
  CPUPOWER_PATH substitution in ExecStartPre
- compile_xmrig drops the clone (rm -rf) on a commit mismatch (#18)
- config-gen round-trips: bracketed IPv6 url, single-pool tls, empty
  ACCESS_TOKEN -> JSON null
- restore of a corrupt / config-less archive: fails AND leaves the
  existing config.json untouched (no silent clobber)
- uninstall without --yes aborts cleanly on 'n' and reverts nothing
- mac_start guard (no built worker -> "run setup first"); mac_stop
  launchd-owned delegation
- numeric degenerate inputs: _median/_stddev/_mean on empty/single
  sample; _watts_from_energy backwards counter w/ no wrap-max; the
  no-power efficiency fallbacks in _accept_better and _autotune_score
- compute_build_jobs sub-2GB floor; the 1G fstab line on a fresh write;
  the rest of the xmrig.service hardening block

Verified: make lint clean; make test 664/664; make coverage 91.34%
(floor 80); suite passes 687/687 on a real Ubuntu 24.04 / bash 5.2 rig.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
VijitSingh97 added a commit that referenced this pull request Jul 2, 2026
* ci: supply-chain hardening (gitleaks, Dependabot, zizmor) + develop/main branch model (#120)

* ci: supply-chain & secrets hardening — gitleaks, Dependabot, zizmor (#117)

Adds the three cross-cutting gates from #117 on top of RigForge's existing
SHA-pinned actions and commit-verified XMRig build. Scope is only the gaps,
not a redo of that work.

- gitleaks: new Security workflow scans the full git history for committed
  secrets (pool creds, tokens, the #113 stratum access-password) on every push
  and PR, with a matching pre-commit hook. Binary is version+checksum pinned,
  mirroring the shellcheck/shfmt installs.
- Dependabot: .github/dependabot.yml for the github-actions ecosystem only
  (RigForge has no pip/npm/docker deps) — keeps the action pins current and
  surfaces advisories.
- zizmor: audits the workflows for template injection, over-broad GITHUB_TOKEN,
  and credential persistence. Hardened ci.yml/release.yml to a read-only default
  token and persist-credentials: false on checkout so the audit is clean.

The broader pre-commit hook set (shellcheck/shfmt/yamllint/markdownlint +
freebies) is left to #118, which extends .pre-commit-config.yaml.

Validated locally: gitleaks clean over full history, zizmor reports no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(security): enable zizmor online audits + weekly schedule

Run zizmor with online audits on (its default) so the known-vulnerable-actions
audit cross-references the actions we pin against the GitHub Advisory Database —
a CVE disclosed against a pinned action now fails the gate, not just structural
issues. GH_TOKEN is the built-in read-only token (advisory data is public; it's
only for API access).

Add a weekly schedule so a freshly-published advisory trips against main even
with no open PRs (the online audit is time-varying by design). gitleaks is
skipped on the scheduled tick — history doesn't change between pushes.

Complements Dependabot: zizmor blocks the merge, Dependabot opens the bump.
Validated: zizmor online reports no findings against the current pins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: adopt the develop/main branch model (mirrors Pithead)

Pithead uses develop as the default integration branch and main as the release
branch; mirror that here (develop is now the repo default).

- ci.yml + security.yml: run on push to [main, develop] (PRs already run on any
  base via the unfiltered pull_request trigger).
- coverage: diff-cover now compares against the PR's actual base branch
  (github.base_ref, falling back to the pushed branch) instead of a hardcoded
  origin/main — so patch coverage stays correct once develop diverges from main.
- CONTRIBUTING: document the two-branch model; PRs target develop.
- RELEASING: build the release commit on develop, fast-forward into main, tag
  from main. develop is merged to main at each release (1.1 onward).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contributing): state inbound contributions are MIT-licensed (closes #119) (#121)

* ci: DX glue + config/docs lint (.editorconfig, pre-commit, yamllint, markdownlint, lychee) (#118) (#122)

Rounds out the non-shell tooling around RigForge's existing shellcheck/shfmt +
kcov core, finishing the tooling epic (#116).

- .editorconfig: encodes the whitespace house style (shfmt -i 4, LF, final
  newline) so editors match CI.
- pre-commit: .pre-commit-config.yaml now orchestrates `make lint` (shellcheck +
  shfmt via the Makefile's SHELL_FILES — single source of truth, no duplicated
  list), the existing gitleaks hook, and freebie hygiene hooks (detect-private-key,
  check-added-large-files, end-of-file-fixer, trailing-whitespace).
- yamllint + markdownlint: new CI jobs in ci.yml (and `make lint-yaml` /
  `make lint-md`), each with a tuned config (.yamllint, .markdownlint-cli2.yaml)
  matching the repo's house style. Pinned via pipx / npx like diff-cover/zizmor.
- lychee: new scheduled workflow (links.yml) + `make lint-links`, kept off the PR
  path since external links are flaky-by-nature. Pinned + checksum-verified binary.
- docs: small fixes to satisfy the structural markdown rules (blank lines around
  headings/lists/fences via --fix; code-fence languages; fenced donation block;
  README hero subtitle as <h3> to keep the heading hierarchy clean).
- CONTRIBUTING: document the hooks + the config/docs lint targets.

Validated locally: yamllint --strict, markdownlint, zizmor (online), lychee
(198 links OK), and `pre-commit run --all-files` all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* release: v1.1.0

Roll up the Unreleased changelog under 1.1.0 and bump VERSION. v1.1 is the
tooling/code-quality + repo-hardening release: supply-chain & secrets gates
(#117), DX glue + config/docs lint (#118), the develop/main branch model, and
the MIT-licensing contributing note (#119). Stratum auth/TLS (#113/#115) were
deferred to v1.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: copy-edit for a more natural, less templated voice

Moderate style pass across the README, the docs/ guides, and the meta docs
(CONTRIBUTING, SECURITY, RELEASING, issue/PR templates): trim em-dash density,
drop gratuitous bold and italics-for-emphasis, soften marketing cadence and the
recurring "X, not Y" construction, and cut filler/virtue words.

Prose-only. Verified no code blocks, command tokens, flags, env vars, numbers,
link targets, or table rows changed; emoji section headers kept; markdownlint
clean across all 18 files. CHANGELOG left in the standard Keep-a-Changelog style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: defer release — keep v1.1 tooling/docs under [Unreleased]

The v1.1 work is dev tooling (gitleaks/zizmor/Dependabot, pre-commit, the lint
configs, the develop/main branch model) plus a docs copy-edit. None of the
tooling is in the release bundle (release.yml ships rigforge.sh/util/systemd/
config templates/README/docs/images/LICENSE/VERSION — not tests/, .github/, or
the dotfiles), and rigforge.sh is unchanged, so there's nothing user-facing to
release. Bumping to 1.1.0 (MINOR) would signal new functionality that isn't there.

Revert the premature VERSION bump (back to 1.0.1) and the changelog rollup; the
entries stay under [Unreleased] and will ship with the next release that actually
changes something for users. main stays at the last release (1.0.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: worker HTTP API open (read-only) by default (#125)

ACCESS_TOKEN no longer defaults to the rig name. Left unset, the rig's
:8080 xmrig API is served `restricted` (read-only) with NO token — which
matches Pithead's new default no-auth stats probe, so a stock rig needs
zero token coordination. Setting ACCESS_TOKEN re-enables Bearer auth (now
validated only when non-empty); match it dashboard-side with
workers.api_auth token|name.

Pairs with pithead#171/#172 (the dashboard's single config-driven worker
API probe).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(deps): bump actions/checkout in the github-actions group (#124)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@df4cb1c...9c091bb)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vijit Singh <vijit.n.singh@gmail.com>

* docs: rewrite for voice consistency (#126)

* docs: rewrite CONTRIBUTING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite RELEASING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite SECURITY.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/benchmarks.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/configuration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/faq.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/getting-started.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/hardware.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/how-it-works.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/operations.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/pithead-integration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite tests/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: ponytail cleanup — drop dead duplicates + stdlib trims (#128)

Behaviour-preserving cleanups from a ponytail (over-engineering) audit. The audit's
headline finding is that rigforge is already lean; these are the few real cuts.

rigforge.sh:
- macOS tuning block re-set YIELD/PRIORITY/NUMA/HTTP_RESTRICTED to the exact values
  the shared defaults block already sets — dead no-ops (plus an orphaned priority
  comment duplicating the default's). Keep only the values that actually differ.
- link_cli: fold the `ok` flag into the `if` it guards.

util/proposed-grub.sh:
- L3-cache + socket parsing: `lscpu | grep ... | awk` -> single `awk`; strip via
  bash builtin (`${L3_RAW//[!0-9]/}`) instead of `echo | sed`.
- 1GB-pages read: `$(<file)` instead of `cat ... || echo 0` (the file is guarded
  by -f already, and the default is set to 0 above).

No behaviour change; 614-test suite green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rename config files for clarity (minimal + reference) (#127)

config.json.template -> config.minimal.json   (the copy-me starter: just the pool URL)
config.advanced.example.json -> config.reference.json   (the full reference: every key
  with its default, enforced complete by the #23 test)

Matches the same rename in Pithead for cross-repo consistency. Content unchanged — the
the starter is still just the pool URL. Updated all references (rigforge.sh, RELEASING,
docs/configuration, .gitignore, the release workflow + macOS e2e bundle, and the config
tests). CHANGELOG keeps the old names (historical).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* test: cover the edge cases the suite was missing (#129)

Adds ~58 assertions for previously-untested branches and degenerate
inputs, all at the run.sh layer (stub-driven, hardware-independent,
bash 3.2-safe) per tests/README.md — no new files, SHELL_FILES unchanged.

Behaviours now covered:
- check_prerequisites: the whole jq-bootstrap (apt/dnf/pacman/none/brew/
  no-brew/already-present) — was entirely untested
- install_dependencies: the dnf and pacman branches + the no-manager
  warn-not-abort path + the apt-cache-present linux-tools branch
- install_service reboot-gate (#audit A2): enable-but-NOT-start when a
  reboot is pending, restart on rebuild, start on steady state, plus
  CPUPOWER_PATH substitution in ExecStartPre
- compile_xmrig drops the clone (rm -rf) on a commit mismatch (#18)
- config-gen round-trips: bracketed IPv6 url, single-pool tls, empty
  ACCESS_TOKEN -> JSON null
- restore of a corrupt / config-less archive: fails AND leaves the
  existing config.json untouched (no silent clobber)
- uninstall without --yes aborts cleanly on 'n' and reverts nothing
- mac_start guard (no built worker -> "run setup first"); mac_stop
  launchd-owned delegation
- numeric degenerate inputs: _median/_stddev/_mean on empty/single
  sample; _watts_from_energy backwards counter w/ no wrap-max; the
  no-power efficiency fallbacks in _accept_better and _autotune_score
- compute_build_jobs sub-2GB floor; the 1G fstab line on a fresh write;
  the rest of the xmrig.service hardening block

Verified: make lint clean; make test 664/664; make coverage 91.34%
(floor 80); suite passes 687/687 on a real Ubuntu 24.04 / bash 5.2 rig.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e-real): don't send an API Bearer when ACCESS_TOKEN is unset

The worker HTTP API now defaults to open (read-only) with no token, so
XMRig 401s the stale "Bearer $(hostname)" the verify warmup still sent.
Under set -e + pipefail that surfaced as curl -f exit 22 aborting verify
silently before any output. Send the header only when ACCESS_TOKEN is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tune): don't send an API Bearer when ACCESS_TOKEN is unset

_read_api_hashrate always sent "Bearer ${ACCESS_TOKEN:-}", so with the
new open (no-token) API default it sent an empty Bearer, XMRig 401'd it,
and curl -f (exit 22) aborted the caller under set -e — breaking every
live-hashrate read: autotune (incl. the monthly timer), tune --live,
tune --confirm, and the upgrade re-tune. Send the header only when
ACCESS_TOKEN is set. The suites stub this via API_CMD, so only the
real-hardware e2e gate exercised the curl path that regressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* release: v1.1.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(release): promote develop to main via a reviewable PR, not a direct push

The develop->main promotion goes through a PR (release gate + audit trail);
main is protected. Fast-forward/rebase-merge so the tag lands on develop's
release commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tune): cover _read_api_hashrate's auth-header logic; harden for bash 3.2

Adds a unit test driving the real _read_api_hashrate with a stubbed curl —
asserting no Authorization header when ACCESS_TOKEN is unset and a Bearer
when set. This is the branch the rest of the suite stubs via API_CMD, so it
was uncovered (diff-cover patch-coverage gate flagged the new lines) and is
exactly where the open-API regression hid. Also swaps the empty-array curl
arg for an if/else, since "${arr[@]}" on an empty array trips set -u on
bash 3.2 (the macOS CI job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
VijitSingh97 added a commit that referenced this pull request Jul 10, 2026
* ci: supply-chain hardening (gitleaks, Dependabot, zizmor) + develop/main branch model (#120)

* ci: supply-chain & secrets hardening — gitleaks, Dependabot, zizmor (#117)

Adds the three cross-cutting gates from #117 on top of RigForge's existing
SHA-pinned actions and commit-verified XMRig build. Scope is only the gaps,
not a redo of that work.

- gitleaks: new Security workflow scans the full git history for committed
  secrets (pool creds, tokens, the #113 stratum access-password) on every push
  and PR, with a matching pre-commit hook. Binary is version+checksum pinned,
  mirroring the shellcheck/shfmt installs.
- Dependabot: .github/dependabot.yml for the github-actions ecosystem only
  (RigForge has no pip/npm/docker deps) — keeps the action pins current and
  surfaces advisories.
- zizmor: audits the workflows for template injection, over-broad GITHUB_TOKEN,
  and credential persistence. Hardened ci.yml/release.yml to a read-only default
  token and persist-credentials: false on checkout so the audit is clean.

The broader pre-commit hook set (shellcheck/shfmt/yamllint/markdownlint +
freebies) is left to #118, which extends .pre-commit-config.yaml.

Validated locally: gitleaks clean over full history, zizmor reports no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(security): enable zizmor online audits + weekly schedule

Run zizmor with online audits on (its default) so the known-vulnerable-actions
audit cross-references the actions we pin against the GitHub Advisory Database —
a CVE disclosed against a pinned action now fails the gate, not just structural
issues. GH_TOKEN is the built-in read-only token (advisory data is public; it's
only for API access).

Add a weekly schedule so a freshly-published advisory trips against main even
with no open PRs (the online audit is time-varying by design). gitleaks is
skipped on the scheduled tick — history doesn't change between pushes.

Complements Dependabot: zizmor blocks the merge, Dependabot opens the bump.
Validated: zizmor online reports no findings against the current pins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: adopt the develop/main branch model (mirrors Pithead)

Pithead uses develop as the default integration branch and main as the release
branch; mirror that here (develop is now the repo default).

- ci.yml + security.yml: run on push to [main, develop] (PRs already run on any
  base via the unfiltered pull_request trigger).
- coverage: diff-cover now compares against the PR's actual base branch
  (github.base_ref, falling back to the pushed branch) instead of a hardcoded
  origin/main — so patch coverage stays correct once develop diverges from main.
- CONTRIBUTING: document the two-branch model; PRs target develop.
- RELEASING: build the release commit on develop, fast-forward into main, tag
  from main. develop is merged to main at each release (1.1 onward).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contributing): state inbound contributions are MIT-licensed (closes #119) (#121)

* ci: DX glue + config/docs lint (.editorconfig, pre-commit, yamllint, markdownlint, lychee) (#118) (#122)

Rounds out the non-shell tooling around RigForge's existing shellcheck/shfmt +
kcov core, finishing the tooling epic (#116).

- .editorconfig: encodes the whitespace house style (shfmt -i 4, LF, final
  newline) so editors match CI.
- pre-commit: .pre-commit-config.yaml now orchestrates `make lint` (shellcheck +
  shfmt via the Makefile's SHELL_FILES — single source of truth, no duplicated
  list), the existing gitleaks hook, and freebie hygiene hooks (detect-private-key,
  check-added-large-files, end-of-file-fixer, trailing-whitespace).
- yamllint + markdownlint: new CI jobs in ci.yml (and `make lint-yaml` /
  `make lint-md`), each with a tuned config (.yamllint, .markdownlint-cli2.yaml)
  matching the repo's house style. Pinned via pipx / npx like diff-cover/zizmor.
- lychee: new scheduled workflow (links.yml) + `make lint-links`, kept off the PR
  path since external links are flaky-by-nature. Pinned + checksum-verified binary.
- docs: small fixes to satisfy the structural markdown rules (blank lines around
  headings/lists/fences via --fix; code-fence languages; fenced donation block;
  README hero subtitle as <h3> to keep the heading hierarchy clean).
- CONTRIBUTING: document the hooks + the config/docs lint targets.

Validated locally: yamllint --strict, markdownlint, zizmor (online), lychee
(198 links OK), and `pre-commit run --all-files` all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* release: v1.1.0

Roll up the Unreleased changelog under 1.1.0 and bump VERSION. v1.1 is the
tooling/code-quality + repo-hardening release: supply-chain & secrets gates
(#117), DX glue + config/docs lint (#118), the develop/main branch model, and
the MIT-licensing contributing note (#119). Stratum auth/TLS (#113/#115) were
deferred to v1.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: copy-edit for a more natural, less templated voice

Moderate style pass across the README, the docs/ guides, and the meta docs
(CONTRIBUTING, SECURITY, RELEASING, issue/PR templates): trim em-dash density,
drop gratuitous bold and italics-for-emphasis, soften marketing cadence and the
recurring "X, not Y" construction, and cut filler/virtue words.

Prose-only. Verified no code blocks, command tokens, flags, env vars, numbers,
link targets, or table rows changed; emoji section headers kept; markdownlint
clean across all 18 files. CHANGELOG left in the standard Keep-a-Changelog style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: defer release — keep v1.1 tooling/docs under [Unreleased]

The v1.1 work is dev tooling (gitleaks/zizmor/Dependabot, pre-commit, the lint
configs, the develop/main branch model) plus a docs copy-edit. None of the
tooling is in the release bundle (release.yml ships rigforge.sh/util/systemd/
config templates/README/docs/images/LICENSE/VERSION — not tests/, .github/, or
the dotfiles), and rigforge.sh is unchanged, so there's nothing user-facing to
release. Bumping to 1.1.0 (MINOR) would signal new functionality that isn't there.

Revert the premature VERSION bump (back to 1.0.1) and the changelog rollup; the
entries stay under [Unreleased] and will ship with the next release that actually
changes something for users. main stays at the last release (1.0.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: worker HTTP API open (read-only) by default (#125)

ACCESS_TOKEN no longer defaults to the rig name. Left unset, the rig's
:8080 xmrig API is served `restricted` (read-only) with NO token — which
matches Pithead's new default no-auth stats probe, so a stock rig needs
zero token coordination. Setting ACCESS_TOKEN re-enables Bearer auth (now
validated only when non-empty); match it dashboard-side with
workers.api_auth token|name.

Pairs with pithead#171/#172 (the dashboard's single config-driven worker
API probe).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(deps): bump actions/checkout in the github-actions group (#124)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@df4cb1c...9c091bb)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vijit Singh <vijit.n.singh@gmail.com>

* docs: rewrite for voice consistency (#126)

* docs: rewrite CONTRIBUTING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite RELEASING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite SECURITY.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/benchmarks.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/configuration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/faq.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/getting-started.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/hardware.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/how-it-works.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/operations.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/pithead-integration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite tests/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: ponytail cleanup — drop dead duplicates + stdlib trims (#128)

Behaviour-preserving cleanups from a ponytail (over-engineering) audit. The audit's
headline finding is that rigforge is already lean; these are the few real cuts.

rigforge.sh:
- macOS tuning block re-set YIELD/PRIORITY/NUMA/HTTP_RESTRICTED to the exact values
  the shared defaults block already sets — dead no-ops (plus an orphaned priority
  comment duplicating the default's). Keep only the values that actually differ.
- link_cli: fold the `ok` flag into the `if` it guards.

util/proposed-grub.sh:
- L3-cache + socket parsing: `lscpu | grep ... | awk` -> single `awk`; strip via
  bash builtin (`${L3_RAW//[!0-9]/}`) instead of `echo | sed`.
- 1GB-pages read: `$(<file)` instead of `cat ... || echo 0` (the file is guarded
  by -f already, and the default is set to 0 above).

No behaviour change; 614-test suite green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rename config files for clarity (minimal + reference) (#127)

config.json.template -> config.minimal.json   (the copy-me starter: just the pool URL)
config.advanced.example.json -> config.reference.json   (the full reference: every key
  with its default, enforced complete by the #23 test)

Matches the same rename in Pithead for cross-repo consistency. Content unchanged — the
the starter is still just the pool URL. Updated all references (rigforge.sh, RELEASING,
docs/configuration, .gitignore, the release workflow + macOS e2e bundle, and the config
tests). CHANGELOG keeps the old names (historical).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* test: cover the edge cases the suite was missing (#129)

Adds ~58 assertions for previously-untested branches and degenerate
inputs, all at the run.sh layer (stub-driven, hardware-independent,
bash 3.2-safe) per tests/README.md — no new files, SHELL_FILES unchanged.

Behaviours now covered:
- check_prerequisites: the whole jq-bootstrap (apt/dnf/pacman/none/brew/
  no-brew/already-present) — was entirely untested
- install_dependencies: the dnf and pacman branches + the no-manager
  warn-not-abort path + the apt-cache-present linux-tools branch
- install_service reboot-gate (#audit A2): enable-but-NOT-start when a
  reboot is pending, restart on rebuild, start on steady state, plus
  CPUPOWER_PATH substitution in ExecStartPre
- compile_xmrig drops the clone (rm -rf) on a commit mismatch (#18)
- config-gen round-trips: bracketed IPv6 url, single-pool tls, empty
  ACCESS_TOKEN -> JSON null
- restore of a corrupt / config-less archive: fails AND leaves the
  existing config.json untouched (no silent clobber)
- uninstall without --yes aborts cleanly on 'n' and reverts nothing
- mac_start guard (no built worker -> "run setup first"); mac_stop
  launchd-owned delegation
- numeric degenerate inputs: _median/_stddev/_mean on empty/single
  sample; _watts_from_energy backwards counter w/ no wrap-max; the
  no-power efficiency fallbacks in _accept_better and _autotune_score
- compute_build_jobs sub-2GB floor; the 1G fstab line on a fresh write;
  the rest of the xmrig.service hardening block

Verified: make lint clean; make test 664/664; make coverage 91.34%
(floor 80); suite passes 687/687 on a real Ubuntu 24.04 / bash 5.2 rig.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e-real): don't send an API Bearer when ACCESS_TOKEN is unset

The worker HTTP API now defaults to open (read-only) with no token, so
XMRig 401s the stale "Bearer $(hostname)" the verify warmup still sent.
Under set -e + pipefail that surfaced as curl -f exit 22 aborting verify
silently before any output. Send the header only when ACCESS_TOKEN is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tune): don't send an API Bearer when ACCESS_TOKEN is unset

_read_api_hashrate always sent "Bearer ${ACCESS_TOKEN:-}", so with the
new open (no-token) API default it sent an empty Bearer, XMRig 401'd it,
and curl -f (exit 22) aborted the caller under set -e — breaking every
live-hashrate read: autotune (incl. the monthly timer), tune --live,
tune --confirm, and the upgrade re-tune. Send the header only when
ACCESS_TOKEN is set. The suites stub this via API_CMD, so only the
real-hardware e2e gate exercised the curl path that regressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* release: v1.1.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(release): promote develop to main via a reviewable PR, not a direct push

The develop->main promotion goes through a PR (release gate + audit trail);
main is protected. Fast-forward/rebase-merge so the tag lands on develop's
release commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tune): cover _read_api_hashrate's auth-header logic; harden for bash 3.2

Adds a unit test driving the real _read_api_hashrate with a stubbed curl —
asserting no Authorization header when ACCESS_TOKEN is unset and a Bearer
when set. This is the branch the rest of the suite stubs via API_CMD, so it
was uncovered (diff-cover patch-coverage gate flagged the new lines) and is
exactly where the open-API regression hid. Also swaps the empty-array curl
arg for an if/else, since "${arr[@]}" on an empty array trips set -u on
bash 3.2 (the macOS CI job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* v1.2 audit fixes: config permissions, SERVICE_NAME, GRUB escaping, robustness sweep, doc corrections (#150)

* fix(audit): close the v1.2 audit's code findings

- setup: chmod 600 config.json at interactive bootstrap, before the operator
  is told to paste a wallet/token into it (#131)
- install_service: honor the documented SERVICE_NAME override for the unit it
  writes/enables/starts, so uninstall manages the same unit setup installed (#133)
- grub: escape \ & | in the sed replacement when rewriting
  GRUB_CMDLINE_LINUX_DEFAULT, so a legal pre-existing kernel param can't
  corrupt /etc/default/grub; new _sed_escape_replacement helper (#134)
- robustness sweep (#135): warn/error to stderr; arm the tune cleanup trap at
  mktemp time (live-mode aborts leaked TUNE_TMP); doctor asserts the live
  config keeps http.restricted=true; HOME_DIR deny-list for bare system dirs
  ahead of sudo rm -rf; local actual in compile_xmrig; awk -v instead of
  interpolated program text (7 sites); quote $i; bounded poll instead of a
  fixed sleep in the macOS process test; fail-fast apt guard in the container
  e2e; derive Makefile SHELL_FILES from git ls-files

Each change carries a regression test; suite 687 passed / 0 failed, make lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: drop stale token-gated API claims; license + donation clarity

- SECURITY.md, README.md, docs/operations.md: the :8080 API has been open
  read-only by default since v1.1.0 — say so instead of overstating the
  posture as token-gated (#132)
- README: note XMRig's GPLv3 (compiled from upstream source on the user's
  machine, no binaries redistributed) and whose wallet the Donate address is;
  add .github/FUNDING.yml pointing the Sponsor button at the Donate section (#136)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(setup): prompt for the stack's stratum password at first run (#151)

The Pithead side shipped opt-in stratum auth (p2pool.stratum_password,
pithead#207): when set, the proxy rejects any rig whose pool `pass` doesn't
match. RigForge already flows pools[].pass through to the xmrig config — the
missing piece was zero-friction entry: the interactive first run now asks for
the password (Enter skips it), pre-validated with parse_config's exact pass
rule so a bad value fails before anything lands on disk.

An empty answer writes no `pass` key, keeping the no-auth minimal config
byte-identical to before. Docs gain the rotation runbook (regenerate on the
stack, re-paste + `apply` on each rig; stale rigs log `Permission denied` by
design).

Automated fetch of the secret stays deliberately unbuilt until pithead#208
picks a retrieval channel.

Closes #113

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(pools): tls-fingerprint — pin the stratum server cert by SHA-256 (#153)

Verified against the pinned XMRig v6.26.0: with tls:true and no fingerprint,
xmrig performs NO server certificate verification for stratum (Tls.cpp
verifyFingerprint returns true when unset) — so the pin is not a self-signed
nicety, it is the only server authentication stratum-TLS has.

- pools[].tls-fingerprint passes through parse/generate verbatim, emitted
  only when set (pre-#115 configs produce byte-identical POOLS_JSON)
- validated as 64 hex chars, either case; the rejection message embeds the
  openssl one-liner that produces the right form
- a pin without tls:true is a hard error — a silently-ignored pin would
  leave the operator believing they're protected
- docs: trust model stated plainly (TLS without a pin = encrypted but
  MITM-able), fingerprint one-liner, rotation runbook, troubleshooting row

Setup-time "stack offers TLS" detection stays unbuilt until pithead#261
picks its terminator/port model; the config surface won't change.

Closes #115

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(api): sister API — read-only stats superset on its own port (#99) (#154)

Opt-in ("api": "enabled", default off): a second HTTP endpoint (default
:8081) serving XMRig's /1/summary + /2/summary passed through verbatim plus
a namespaced `rigforge` object — applied tune knobs and last-run summary,
RAPL watts + hashrate-per-watt, the doctor probes (HugePages, MSR, governor,
RAM channels/speeds, memory-profile/SMT, throttling) as JSON, and pinned
version provenance — plus bare /health and /tune endpoints.

Serving is systemd socket activation: the .socket unit (Accept=yes) owns the
listener and spawns one short-lived `rigforge.sh api-serve` per request over
stdin/stdout. No resident daemon, no new dependencies (systemd + curl + jq,
all already required). The per-connection unit is sandboxed read-only
(ProtectSystem=strict, NoNewPrivileges, PrivateTmp, ...) — the handler is
read-only by construction (GET only, fixed routes, nothing from the request
is executed or logged) and the sandbox enforces it at the filesystem level.

Auth reuses ACCESS_TOKEN unchanged (open by default, exact Bearer when set —
never echoed anywhere). XMRig down still answers 200 with an
"xmrig_api": "unreachable" marker: health data matters most then. RAPL is
the only power source in the handler — TUNE_POWER_CMD stays env-only, since
config-derived text must never reach eval in a network-facing process.

Wired into setup, `apply` (the config-change path), and uninstall, mirroring
the autotune-timer toggle. Linux-only.

Closes #99

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): worker↔stack contract gate + sister-API hashrate-impact guard (#155)

tests/e2e-pithead.sh (#114): release-gated, manual, root — drives THIS rig's
real provisioned worker against a live Pithead stack named by PITHEAD_URL and
asserts the contract documented in pithead-integration.md:

- connect: stratum job + accepted share against the stack; the user-label
  discovery contract
- worker-api: open read-only default, 0.0.0.0 bind, restricted refusal, and
  the custom-token 401/200 pairing (worker half of pithead#171)
- api-impact: the #99 perf guard — baseline live hashrate vs 4 clients
  hammering :8081 back-to-back; fails if the loss exceeds
  E2E_API_IMPACT_TOLERANCE_PCT (default 3%)
- stratum-auth (E2E_STRATUM_PASS): right pass mines, wrong pass rejected
  with no jobs delivered, re-pasting recovers (#113's rotation runbook,
  proven mechanically)
- dashboard (E2E_DASH_URL): workers-alive shows the rig; a stopped rig
  drops off within the liveness window
- dev-fee: donate-level follows config, independent of the stack

The operator's config.json is snapshotted and restored (+ apply) on exit via
trap, whatever happens — this runs on production-adjacent rigs.

Belt for the braces: rigforge-api@.service gains Nice=19 +
IOSchedulingClass=idle so handlers can never compete with RandomX for CPU;
the unit test asserts the directive, the e2e phase proves the effect.

Closes #114

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* v1.5 completeness: api rebind + bearer-parsing fixes, integration coverage, standardized perf gate, changelog (#156)

* fix(api)+test+docs: v1.5 completeness — rebind bug, integration coverage, perf standard, changelog

Two real bugs from the completeness audit and the first live gate run:
- install_api: an api_port/api_bind edit + apply re-rendered the socket unit
  but never rebound — daemon-reload alone doesn't rebind a changed
  ListenStream, so the old port kept listening until reboot. Restart added;
  tested.
- api_serve: an Authorization header with no space after the colon (legal
  HTTP) 401'd a valid token. Whitespace-tolerant parsing; tested.

Integration coverage (all validated in the container e2e against a real /etc):
- in-container.sh deploys with "api": "enabled" — asserts the socket +
  handler units land fully rendered, and uninstall removes both.
- run.sh: apply reconciles the sister API on/off (autotune-precedent shape);
  port-change rebind; query-string + HEAD handler edges; corrupt
  tune-overrides answers 200 with nulls; second-pool tls-fingerprint index
  alignment; the Nice=19 perf-guard directive.

e2e gate hardening from the first miner-0 run:
- worker-api phase normalizes ACCESS_TOKEN before asserting the open default
  (operator rigs legitimately run tokened).
- dev-fee asserts the EFFECTIVE contract: max(config DONATION, the
  kMinimumDonateLevel compiled into donate.h) — XMRig clamps and autosaves,
  so lowering DONATION after a build needs a rebuild. Documented in
  configuration.md.
- stratum-auth guards a missing worker log on standalone runs.

Standardized performance testing:
- e2e-real gains a `perf` phase: offline 1M bench vs a COMMITTED per-host
  baseline (tests/perf-baselines/, E2E_PERF_RECORD=1 to record,
  E2E_PERF_TOLERANCE_PCT default 5). Runs inside `all`; a regression fails
  the release gate. tests/README.md documents the three standardized
  measurements (absolute, under-API-load, tune improvement).

Docs: CHANGELOG [Unreleased] now covers everything since 1.1.0; SECURITY.md
documents the :8081 posture; pithead-integration.md gains the sister-API
contract section (the pithead#235 feed); README table mentions :8081.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(net): network-level leak validation + spirit-of-XMRig wire contract

New `network` phase in the e2e-pithead gate — the boundary, enforced on the
wire of a real rig:
- outbound: xmrig's only established TCP peers are the configured pool
- listeners: the miner owns :8080 and nothing else; :8081 exists exactly
  while api is enabled and closes on disable
- superset on the wire: sister /2/summary minus `rigforge` carries EXACTLY
  the key set xmrig's own API serves — nothing renamed, dropped, or invented
- leak sweep: with a token and a stratum pass configured, no byte of any
  response on either port (authed, unauthed, errors, headers) contains
  either secret

Unit-level wire-contract locks in run.sh: exact /health and /tune key sets
(the pithead#235 contract), application/json + exactly three headers with no
server banner to fingerprint, and authed-200 responses from every endpoint
asserted free of ACCESS_TOKEN and pools[].pass.

Docs: pithead-integration.md states the XMRig-convention alignment and the
enforced network boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: nudge (empty)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore(dev): repo-health tooling + miner-0 perf baseline; fix baselines README (#157)

Fixes develop CI: tests/perf-baselines/README.md was written with shell-style
comment lines that markdownlint reads as malformed headings — now real
markdown. The class of failure is closed, not just the instance:

- pre-commit gains markdown + yaml hooks (via the same make targets CI runs,
  so file lists and version pins stay single-sourced) — `make lint` alone
  never sees .md/.yml, which is exactly how this slipped through
- actionlint joins CI (pinned + checksum-verified): zizmor audits workflow
  security, actionlint now checks workflow correctness; `make lint-actions`
  locally, wired into lint-all
- `make dev-setup`: one command for the local toolchain (brew/apt) + git
  hooks, printing the CI-pinned versions to match
- `make ci`: everything CI runs that can run locally, adding the container
  e2e when Docker is up

Plus the first committed perf baseline: miner-0 (7800X3D) at 10789.5 H/s
offline 1M bench, recorded 2026-07-10 — arms the e2e-real perf regression
gate from the next run onward.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* release: v1.2.0 (#158)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
VijitSingh97 added a commit that referenced this pull request Jul 10, 2026
* ci: supply-chain hardening (gitleaks, Dependabot, zizmor) + develop/main branch model (#120)

* ci: supply-chain & secrets hardening — gitleaks, Dependabot, zizmor (#117)

Adds the three cross-cutting gates from #117 on top of RigForge's existing
SHA-pinned actions and commit-verified XMRig build. Scope is only the gaps,
not a redo of that work.

- gitleaks: new Security workflow scans the full git history for committed
  secrets (pool creds, tokens, the #113 stratum access-password) on every push
  and PR, with a matching pre-commit hook. Binary is version+checksum pinned,
  mirroring the shellcheck/shfmt installs.
- Dependabot: .github/dependabot.yml for the github-actions ecosystem only
  (RigForge has no pip/npm/docker deps) — keeps the action pins current and
  surfaces advisories.
- zizmor: audits the workflows for template injection, over-broad GITHUB_TOKEN,
  and credential persistence. Hardened ci.yml/release.yml to a read-only default
  token and persist-credentials: false on checkout so the audit is clean.

The broader pre-commit hook set (shellcheck/shfmt/yamllint/markdownlint +
freebies) is left to #118, which extends .pre-commit-config.yaml.

Validated locally: gitleaks clean over full history, zizmor reports no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(security): enable zizmor online audits + weekly schedule

Run zizmor with online audits on (its default) so the known-vulnerable-actions
audit cross-references the actions we pin against the GitHub Advisory Database —
a CVE disclosed against a pinned action now fails the gate, not just structural
issues. GH_TOKEN is the built-in read-only token (advisory data is public; it's
only for API access).

Add a weekly schedule so a freshly-published advisory trips against main even
with no open PRs (the online audit is time-varying by design). gitleaks is
skipped on the scheduled tick — history doesn't change between pushes.

Complements Dependabot: zizmor blocks the merge, Dependabot opens the bump.
Validated: zizmor online reports no findings against the current pins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: adopt the develop/main branch model (mirrors Pithead)

Pithead uses develop as the default integration branch and main as the release
branch; mirror that here (develop is now the repo default).

- ci.yml + security.yml: run on push to [main, develop] (PRs already run on any
  base via the unfiltered pull_request trigger).
- coverage: diff-cover now compares against the PR's actual base branch
  (github.base_ref, falling back to the pushed branch) instead of a hardcoded
  origin/main — so patch coverage stays correct once develop diverges from main.
- CONTRIBUTING: document the two-branch model; PRs target develop.
- RELEASING: build the release commit on develop, fast-forward into main, tag
  from main. develop is merged to main at each release (1.1 onward).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contributing): state inbound contributions are MIT-licensed (closes #119) (#121)

* ci: DX glue + config/docs lint (.editorconfig, pre-commit, yamllint, markdownlint, lychee) (#118) (#122)

Rounds out the non-shell tooling around RigForge's existing shellcheck/shfmt +
kcov core, finishing the tooling epic (#116).

- .editorconfig: encodes the whitespace house style (shfmt -i 4, LF, final
  newline) so editors match CI.
- pre-commit: .pre-commit-config.yaml now orchestrates `make lint` (shellcheck +
  shfmt via the Makefile's SHELL_FILES — single source of truth, no duplicated
  list), the existing gitleaks hook, and freebie hygiene hooks (detect-private-key,
  check-added-large-files, end-of-file-fixer, trailing-whitespace).
- yamllint + markdownlint: new CI jobs in ci.yml (and `make lint-yaml` /
  `make lint-md`), each with a tuned config (.yamllint, .markdownlint-cli2.yaml)
  matching the repo's house style. Pinned via pipx / npx like diff-cover/zizmor.
- lychee: new scheduled workflow (links.yml) + `make lint-links`, kept off the PR
  path since external links are flaky-by-nature. Pinned + checksum-verified binary.
- docs: small fixes to satisfy the structural markdown rules (blank lines around
  headings/lists/fences via --fix; code-fence languages; fenced donation block;
  README hero subtitle as <h3> to keep the heading hierarchy clean).
- CONTRIBUTING: document the hooks + the config/docs lint targets.

Validated locally: yamllint --strict, markdownlint, zizmor (online), lychee
(198 links OK), and `pre-commit run --all-files` all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* release: v1.1.0

Roll up the Unreleased changelog under 1.1.0 and bump VERSION. v1.1 is the
tooling/code-quality + repo-hardening release: supply-chain & secrets gates
(#117), DX glue + config/docs lint (#118), the develop/main branch model, and
the MIT-licensing contributing note (#119). Stratum auth/TLS (#113/#115) were
deferred to v1.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: copy-edit for a more natural, less templated voice

Moderate style pass across the README, the docs/ guides, and the meta docs
(CONTRIBUTING, SECURITY, RELEASING, issue/PR templates): trim em-dash density,
drop gratuitous bold and italics-for-emphasis, soften marketing cadence and the
recurring "X, not Y" construction, and cut filler/virtue words.

Prose-only. Verified no code blocks, command tokens, flags, env vars, numbers,
link targets, or table rows changed; emoji section headers kept; markdownlint
clean across all 18 files. CHANGELOG left in the standard Keep-a-Changelog style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: defer release — keep v1.1 tooling/docs under [Unreleased]

The v1.1 work is dev tooling (gitleaks/zizmor/Dependabot, pre-commit, the lint
configs, the develop/main branch model) plus a docs copy-edit. None of the
tooling is in the release bundle (release.yml ships rigforge.sh/util/systemd/
config templates/README/docs/images/LICENSE/VERSION — not tests/, .github/, or
the dotfiles), and rigforge.sh is unchanged, so there's nothing user-facing to
release. Bumping to 1.1.0 (MINOR) would signal new functionality that isn't there.

Revert the premature VERSION bump (back to 1.0.1) and the changelog rollup; the
entries stay under [Unreleased] and will ship with the next release that actually
changes something for users. main stays at the last release (1.0.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: worker HTTP API open (read-only) by default (#125)

ACCESS_TOKEN no longer defaults to the rig name. Left unset, the rig's
:8080 xmrig API is served `restricted` (read-only) with NO token — which
matches Pithead's new default no-auth stats probe, so a stock rig needs
zero token coordination. Setting ACCESS_TOKEN re-enables Bearer auth (now
validated only when non-empty); match it dashboard-side with
workers.api_auth token|name.

Pairs with pithead#171/#172 (the dashboard's single config-driven worker
API probe).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(deps): bump actions/checkout in the github-actions group (#124)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@df4cb1c...9c091bb)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vijit Singh <vijit.n.singh@gmail.com>

* docs: rewrite for voice consistency (#126)

* docs: rewrite CONTRIBUTING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite RELEASING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite SECURITY.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/benchmarks.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/configuration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/faq.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/getting-started.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/hardware.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/how-it-works.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/operations.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/pithead-integration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite tests/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: ponytail cleanup — drop dead duplicates + stdlib trims (#128)

Behaviour-preserving cleanups from a ponytail (over-engineering) audit. The audit's
headline finding is that rigforge is already lean; these are the few real cuts.

rigforge.sh:
- macOS tuning block re-set YIELD/PRIORITY/NUMA/HTTP_RESTRICTED to the exact values
  the shared defaults block already sets — dead no-ops (plus an orphaned priority
  comment duplicating the default's). Keep only the values that actually differ.
- link_cli: fold the `ok` flag into the `if` it guards.

util/proposed-grub.sh:
- L3-cache + socket parsing: `lscpu | grep ... | awk` -> single `awk`; strip via
  bash builtin (`${L3_RAW//[!0-9]/}`) instead of `echo | sed`.
- 1GB-pages read: `$(<file)` instead of `cat ... || echo 0` (the file is guarded
  by -f already, and the default is set to 0 above).

No behaviour change; 614-test suite green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rename config files for clarity (minimal + reference) (#127)

config.json.template -> config.minimal.json   (the copy-me starter: just the pool URL)
config.advanced.example.json -> config.reference.json   (the full reference: every key
  with its default, enforced complete by the #23 test)

Matches the same rename in Pithead for cross-repo consistency. Content unchanged — the
the starter is still just the pool URL. Updated all references (rigforge.sh, RELEASING,
docs/configuration, .gitignore, the release workflow + macOS e2e bundle, and the config
tests). CHANGELOG keeps the old names (historical).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* test: cover the edge cases the suite was missing (#129)

Adds ~58 assertions for previously-untested branches and degenerate
inputs, all at the run.sh layer (stub-driven, hardware-independent,
bash 3.2-safe) per tests/README.md — no new files, SHELL_FILES unchanged.

Behaviours now covered:
- check_prerequisites: the whole jq-bootstrap (apt/dnf/pacman/none/brew/
  no-brew/already-present) — was entirely untested
- install_dependencies: the dnf and pacman branches + the no-manager
  warn-not-abort path + the apt-cache-present linux-tools branch
- install_service reboot-gate (#audit A2): enable-but-NOT-start when a
  reboot is pending, restart on rebuild, start on steady state, plus
  CPUPOWER_PATH substitution in ExecStartPre
- compile_xmrig drops the clone (rm -rf) on a commit mismatch (#18)
- config-gen round-trips: bracketed IPv6 url, single-pool tls, empty
  ACCESS_TOKEN -> JSON null
- restore of a corrupt / config-less archive: fails AND leaves the
  existing config.json untouched (no silent clobber)
- uninstall without --yes aborts cleanly on 'n' and reverts nothing
- mac_start guard (no built worker -> "run setup first"); mac_stop
  launchd-owned delegation
- numeric degenerate inputs: _median/_stddev/_mean on empty/single
  sample; _watts_from_energy backwards counter w/ no wrap-max; the
  no-power efficiency fallbacks in _accept_better and _autotune_score
- compute_build_jobs sub-2GB floor; the 1G fstab line on a fresh write;
  the rest of the xmrig.service hardening block

Verified: make lint clean; make test 664/664; make coverage 91.34%
(floor 80); suite passes 687/687 on a real Ubuntu 24.04 / bash 5.2 rig.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e-real): don't send an API Bearer when ACCESS_TOKEN is unset

The worker HTTP API now defaults to open (read-only) with no token, so
XMRig 401s the stale "Bearer $(hostname)" the verify warmup still sent.
Under set -e + pipefail that surfaced as curl -f exit 22 aborting verify
silently before any output. Send the header only when ACCESS_TOKEN is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tune): don't send an API Bearer when ACCESS_TOKEN is unset

_read_api_hashrate always sent "Bearer ${ACCESS_TOKEN:-}", so with the
new open (no-token) API default it sent an empty Bearer, XMRig 401'd it,
and curl -f (exit 22) aborted the caller under set -e — breaking every
live-hashrate read: autotune (incl. the monthly timer), tune --live,
tune --confirm, and the upgrade re-tune. Send the header only when
ACCESS_TOKEN is set. The suites stub this via API_CMD, so only the
real-hardware e2e gate exercised the curl path that regressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* release: v1.1.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(release): promote develop to main via a reviewable PR, not a direct push

The develop->main promotion goes through a PR (release gate + audit trail);
main is protected. Fast-forward/rebase-merge so the tag lands on develop's
release commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tune): cover _read_api_hashrate's auth-header logic; harden for bash 3.2

Adds a unit test driving the real _read_api_hashrate with a stubbed curl —
asserting no Authorization header when ACCESS_TOKEN is unset and a Bearer
when set. This is the branch the rest of the suite stubs via API_CMD, so it
was uncovered (diff-cover patch-coverage gate flagged the new lines) and is
exactly where the open-API regression hid. Also swaps the empty-array curl
arg for an if/else, since "${arr[@]}" on an empty array trips set -u on
bash 3.2 (the macOS CI job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* v1.2 audit fixes: config permissions, SERVICE_NAME, GRUB escaping, robustness sweep, doc corrections (#150)

* fix(audit): close the v1.2 audit's code findings

- setup: chmod 600 config.json at interactive bootstrap, before the operator
  is told to paste a wallet/token into it (#131)
- install_service: honor the documented SERVICE_NAME override for the unit it
  writes/enables/starts, so uninstall manages the same unit setup installed (#133)
- grub: escape \ & | in the sed replacement when rewriting
  GRUB_CMDLINE_LINUX_DEFAULT, so a legal pre-existing kernel param can't
  corrupt /etc/default/grub; new _sed_escape_replacement helper (#134)
- robustness sweep (#135): warn/error to stderr; arm the tune cleanup trap at
  mktemp time (live-mode aborts leaked TUNE_TMP); doctor asserts the live
  config keeps http.restricted=true; HOME_DIR deny-list for bare system dirs
  ahead of sudo rm -rf; local actual in compile_xmrig; awk -v instead of
  interpolated program text (7 sites); quote $i; bounded poll instead of a
  fixed sleep in the macOS process test; fail-fast apt guard in the container
  e2e; derive Makefile SHELL_FILES from git ls-files

Each change carries a regression test; suite 687 passed / 0 failed, make lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: drop stale token-gated API claims; license + donation clarity

- SECURITY.md, README.md, docs/operations.md: the :8080 API has been open
  read-only by default since v1.1.0 — say so instead of overstating the
  posture as token-gated (#132)
- README: note XMRig's GPLv3 (compiled from upstream source on the user's
  machine, no binaries redistributed) and whose wallet the Donate address is;
  add .github/FUNDING.yml pointing the Sponsor button at the Donate section (#136)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(setup): prompt for the stack's stratum password at first run (#151)

The Pithead side shipped opt-in stratum auth (p2pool.stratum_password,
pithead#207): when set, the proxy rejects any rig whose pool `pass` doesn't
match. RigForge already flows pools[].pass through to the xmrig config — the
missing piece was zero-friction entry: the interactive first run now asks for
the password (Enter skips it), pre-validated with parse_config's exact pass
rule so a bad value fails before anything lands on disk.

An empty answer writes no `pass` key, keeping the no-auth minimal config
byte-identical to before. Docs gain the rotation runbook (regenerate on the
stack, re-paste + `apply` on each rig; stale rigs log `Permission denied` by
design).

Automated fetch of the secret stays deliberately unbuilt until pithead#208
picks a retrieval channel.

Closes #113

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(pools): tls-fingerprint — pin the stratum server cert by SHA-256 (#153)

Verified against the pinned XMRig v6.26.0: with tls:true and no fingerprint,
xmrig performs NO server certificate verification for stratum (Tls.cpp
verifyFingerprint returns true when unset) — so the pin is not a self-signed
nicety, it is the only server authentication stratum-TLS has.

- pools[].tls-fingerprint passes through parse/generate verbatim, emitted
  only when set (pre-#115 configs produce byte-identical POOLS_JSON)
- validated as 64 hex chars, either case; the rejection message embeds the
  openssl one-liner that produces the right form
- a pin without tls:true is a hard error — a silently-ignored pin would
  leave the operator believing they're protected
- docs: trust model stated plainly (TLS without a pin = encrypted but
  MITM-able), fingerprint one-liner, rotation runbook, troubleshooting row

Setup-time "stack offers TLS" detection stays unbuilt until pithead#261
picks its terminator/port model; the config surface won't change.

Closes #115

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(api): sister API — read-only stats superset on its own port (#99) (#154)

Opt-in ("api": "enabled", default off): a second HTTP endpoint (default
:8081) serving XMRig's /1/summary + /2/summary passed through verbatim plus
a namespaced `rigforge` object — applied tune knobs and last-run summary,
RAPL watts + hashrate-per-watt, the doctor probes (HugePages, MSR, governor,
RAM channels/speeds, memory-profile/SMT, throttling) as JSON, and pinned
version provenance — plus bare /health and /tune endpoints.

Serving is systemd socket activation: the .socket unit (Accept=yes) owns the
listener and spawns one short-lived `rigforge.sh api-serve` per request over
stdin/stdout. No resident daemon, no new dependencies (systemd + curl + jq,
all already required). The per-connection unit is sandboxed read-only
(ProtectSystem=strict, NoNewPrivileges, PrivateTmp, ...) — the handler is
read-only by construction (GET only, fixed routes, nothing from the request
is executed or logged) and the sandbox enforces it at the filesystem level.

Auth reuses ACCESS_TOKEN unchanged (open by default, exact Bearer when set —
never echoed anywhere). XMRig down still answers 200 with an
"xmrig_api": "unreachable" marker: health data matters most then. RAPL is
the only power source in the handler — TUNE_POWER_CMD stays env-only, since
config-derived text must never reach eval in a network-facing process.

Wired into setup, `apply` (the config-change path), and uninstall, mirroring
the autotune-timer toggle. Linux-only.

Closes #99

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): worker↔stack contract gate + sister-API hashrate-impact guard (#155)

tests/e2e-pithead.sh (#114): release-gated, manual, root — drives THIS rig's
real provisioned worker against a live Pithead stack named by PITHEAD_URL and
asserts the contract documented in pithead-integration.md:

- connect: stratum job + accepted share against the stack; the user-label
  discovery contract
- worker-api: open read-only default, 0.0.0.0 bind, restricted refusal, and
  the custom-token 401/200 pairing (worker half of pithead#171)
- api-impact: the #99 perf guard — baseline live hashrate vs 4 clients
  hammering :8081 back-to-back; fails if the loss exceeds
  E2E_API_IMPACT_TOLERANCE_PCT (default 3%)
- stratum-auth (E2E_STRATUM_PASS): right pass mines, wrong pass rejected
  with no jobs delivered, re-pasting recovers (#113's rotation runbook,
  proven mechanically)
- dashboard (E2E_DASH_URL): workers-alive shows the rig; a stopped rig
  drops off within the liveness window
- dev-fee: donate-level follows config, independent of the stack

The operator's config.json is snapshotted and restored (+ apply) on exit via
trap, whatever happens — this runs on production-adjacent rigs.

Belt for the braces: rigforge-api@.service gains Nice=19 +
IOSchedulingClass=idle so handlers can never compete with RandomX for CPU;
the unit test asserts the directive, the e2e phase proves the effect.

Closes #114

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* v1.5 completeness: api rebind + bearer-parsing fixes, integration coverage, standardized perf gate, changelog (#156)

* fix(api)+test+docs: v1.5 completeness — rebind bug, integration coverage, perf standard, changelog

Two real bugs from the completeness audit and the first live gate run:
- install_api: an api_port/api_bind edit + apply re-rendered the socket unit
  but never rebound — daemon-reload alone doesn't rebind a changed
  ListenStream, so the old port kept listening until reboot. Restart added;
  tested.
- api_serve: an Authorization header with no space after the colon (legal
  HTTP) 401'd a valid token. Whitespace-tolerant parsing; tested.

Integration coverage (all validated in the container e2e against a real /etc):
- in-container.sh deploys with "api": "enabled" — asserts the socket +
  handler units land fully rendered, and uninstall removes both.
- run.sh: apply reconciles the sister API on/off (autotune-precedent shape);
  port-change rebind; query-string + HEAD handler edges; corrupt
  tune-overrides answers 200 with nulls; second-pool tls-fingerprint index
  alignment; the Nice=19 perf-guard directive.

e2e gate hardening from the first miner-0 run:
- worker-api phase normalizes ACCESS_TOKEN before asserting the open default
  (operator rigs legitimately run tokened).
- dev-fee asserts the EFFECTIVE contract: max(config DONATION, the
  kMinimumDonateLevel compiled into donate.h) — XMRig clamps and autosaves,
  so lowering DONATION after a build needs a rebuild. Documented in
  configuration.md.
- stratum-auth guards a missing worker log on standalone runs.

Standardized performance testing:
- e2e-real gains a `perf` phase: offline 1M bench vs a COMMITTED per-host
  baseline (tests/perf-baselines/, E2E_PERF_RECORD=1 to record,
  E2E_PERF_TOLERANCE_PCT default 5). Runs inside `all`; a regression fails
  the release gate. tests/README.md documents the three standardized
  measurements (absolute, under-API-load, tune improvement).

Docs: CHANGELOG [Unreleased] now covers everything since 1.1.0; SECURITY.md
documents the :8081 posture; pithead-integration.md gains the sister-API
contract section (the pithead#235 feed); README table mentions :8081.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(net): network-level leak validation + spirit-of-XMRig wire contract

New `network` phase in the e2e-pithead gate — the boundary, enforced on the
wire of a real rig:
- outbound: xmrig's only established TCP peers are the configured pool
- listeners: the miner owns :8080 and nothing else; :8081 exists exactly
  while api is enabled and closes on disable
- superset on the wire: sister /2/summary minus `rigforge` carries EXACTLY
  the key set xmrig's own API serves — nothing renamed, dropped, or invented
- leak sweep: with a token and a stratum pass configured, no byte of any
  response on either port (authed, unauthed, errors, headers) contains
  either secret

Unit-level wire-contract locks in run.sh: exact /health and /tune key sets
(the pithead#235 contract), application/json + exactly three headers with no
server banner to fingerprint, and authed-200 responses from every endpoint
asserted free of ACCESS_TOKEN and pools[].pass.

Docs: pithead-integration.md states the XMRig-convention alignment and the
enforced network boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: nudge (empty)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore(dev): repo-health tooling + miner-0 perf baseline; fix baselines README (#157)

Fixes develop CI: tests/perf-baselines/README.md was written with shell-style
comment lines that markdownlint reads as malformed headings — now real
markdown. The class of failure is closed, not just the instance:

- pre-commit gains markdown + yaml hooks (via the same make targets CI runs,
  so file lists and version pins stay single-sourced) — `make lint` alone
  never sees .md/.yml, which is exactly how this slipped through
- actionlint joins CI (pinned + checksum-verified): zizmor audits workflow
  security, actionlint now checks workflow correctness; `make lint-actions`
  locally, wired into lint-all
- `make dev-setup`: one command for the local toolchain (brew/apt) + git
  hooks, printing the CI-pinned versions to match
- `make ci`: everything CI runs that can run locally, adding the container
  e2e when Docker is up

Plus the first committed perf baseline: miner-0 (7800X3D) at 10789.5 H/s
offline 1M bench, recorded 2026-07-10 — arms the e2e-real perf regression
gate from the next run onward.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* release: v1.2.0 (#158)

* test(perf): commit the EPYC fleet baselines (miner-1..7) (#160)

Recorded on each rig right after the v1.2.0 deploy — seven 96-core EPYC
7642s at 36.7-36.9 kH/s offline 1M bench. Arms the e2e-real perf regression
gate for the whole fleet; miner-0 (7800X3D) was committed with the gate
itself.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(api): relax handler scheduling so it isn't starved under full mining load (#161)

The sister-API handler ran at Nice=19 + IOSchedulingClass=idle — SCHED_IDLE-
like "run only when a core is free." On a rig where XMRig pins every core
(96 threads on 96 cores) that is never, so a /health request took ~51s on a
loaded EPYC. Found confirming the production fleet through the API after the
v1.2.0 deploy; the api-impact e2e phase missed it because it ran on miner-0
(16 threads, spare capacity) and only asserted hashrate wasn't shaved, never
that the API stayed responsive.

Fix: Nice=10 (drop the idle IO class). Still cedes CPU to the miner — one
short-lived handler is a rounding error against N compute threads — but the
scheduler gives it a slice. Validated on loaded miner-3 (96-core EPYC):
/health 51s -> 8.2s, hashrate unchanged (36863 -> 36872 H/s, within noise).

The api-impact phase now bounds response latency under load too
(E2E_API_LATENCY_S, default 15s), so the responsiveness half of the perf
contract can't regress silently. Run it on a saturated many-core rig.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* release: v1.2.1 (#162)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
VijitSingh97 added a commit that referenced this pull request Jul 10, 2026
* ci: supply-chain hardening (gitleaks, Dependabot, zizmor) + develop/main branch model (#120)

* ci: supply-chain & secrets hardening — gitleaks, Dependabot, zizmor (#117)

Adds the three cross-cutting gates from #117 on top of RigForge's existing
SHA-pinned actions and commit-verified XMRig build. Scope is only the gaps,
not a redo of that work.

- gitleaks: new Security workflow scans the full git history for committed
  secrets (pool creds, tokens, the #113 stratum access-password) on every push
  and PR, with a matching pre-commit hook. Binary is version+checksum pinned,
  mirroring the shellcheck/shfmt installs.
- Dependabot: .github/dependabot.yml for the github-actions ecosystem only
  (RigForge has no pip/npm/docker deps) — keeps the action pins current and
  surfaces advisories.
- zizmor: audits the workflows for template injection, over-broad GITHUB_TOKEN,
  and credential persistence. Hardened ci.yml/release.yml to a read-only default
  token and persist-credentials: false on checkout so the audit is clean.

The broader pre-commit hook set (shellcheck/shfmt/yamllint/markdownlint +
freebies) is left to #118, which extends .pre-commit-config.yaml.

Validated locally: gitleaks clean over full history, zizmor reports no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(security): enable zizmor online audits + weekly schedule

Run zizmor with online audits on (its default) so the known-vulnerable-actions
audit cross-references the actions we pin against the GitHub Advisory Database —
a CVE disclosed against a pinned action now fails the gate, not just structural
issues. GH_TOKEN is the built-in read-only token (advisory data is public; it's
only for API access).

Add a weekly schedule so a freshly-published advisory trips against main even
with no open PRs (the online audit is time-varying by design). gitleaks is
skipped on the scheduled tick — history doesn't change between pushes.

Complements Dependabot: zizmor blocks the merge, Dependabot opens the bump.
Validated: zizmor online reports no findings against the current pins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: adopt the develop/main branch model (mirrors Pithead)

Pithead uses develop as the default integration branch and main as the release
branch; mirror that here (develop is now the repo default).

- ci.yml + security.yml: run on push to [main, develop] (PRs already run on any
  base via the unfiltered pull_request trigger).
- coverage: diff-cover now compares against the PR's actual base branch
  (github.base_ref, falling back to the pushed branch) instead of a hardcoded
  origin/main — so patch coverage stays correct once develop diverges from main.
- CONTRIBUTING: document the two-branch model; PRs target develop.
- RELEASING: build the release commit on develop, fast-forward into main, tag
  from main. develop is merged to main at each release (1.1 onward).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contributing): state inbound contributions are MIT-licensed (closes #119) (#121)

* ci: DX glue + config/docs lint (.editorconfig, pre-commit, yamllint, markdownlint, lychee) (#118) (#122)

Rounds out the non-shell tooling around RigForge's existing shellcheck/shfmt +
kcov core, finishing the tooling epic (#116).

- .editorconfig: encodes the whitespace house style (shfmt -i 4, LF, final
  newline) so editors match CI.
- pre-commit: .pre-commit-config.yaml now orchestrates `make lint` (shellcheck +
  shfmt via the Makefile's SHELL_FILES — single source of truth, no duplicated
  list), the existing gitleaks hook, and freebie hygiene hooks (detect-private-key,
  check-added-large-files, end-of-file-fixer, trailing-whitespace).
- yamllint + markdownlint: new CI jobs in ci.yml (and `make lint-yaml` /
  `make lint-md`), each with a tuned config (.yamllint, .markdownlint-cli2.yaml)
  matching the repo's house style. Pinned via pipx / npx like diff-cover/zizmor.
- lychee: new scheduled workflow (links.yml) + `make lint-links`, kept off the PR
  path since external links are flaky-by-nature. Pinned + checksum-verified binary.
- docs: small fixes to satisfy the structural markdown rules (blank lines around
  headings/lists/fences via --fix; code-fence languages; fenced donation block;
  README hero subtitle as <h3> to keep the heading hierarchy clean).
- CONTRIBUTING: document the hooks + the config/docs lint targets.

Validated locally: yamllint --strict, markdownlint, zizmor (online), lychee
(198 links OK), and `pre-commit run --all-files` all green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* release: v1.1.0

Roll up the Unreleased changelog under 1.1.0 and bump VERSION. v1.1 is the
tooling/code-quality + repo-hardening release: supply-chain & secrets gates
(#117), DX glue + config/docs lint (#118), the develop/main branch model, and
the MIT-licensing contributing note (#119). Stratum auth/TLS (#113/#115) were
deferred to v1.6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: copy-edit for a more natural, less templated voice

Moderate style pass across the README, the docs/ guides, and the meta docs
(CONTRIBUTING, SECURITY, RELEASING, issue/PR templates): trim em-dash density,
drop gratuitous bold and italics-for-emphasis, soften marketing cadence and the
recurring "X, not Y" construction, and cut filler/virtue words.

Prose-only. Verified no code blocks, command tokens, flags, env vars, numbers,
link targets, or table rows changed; emoji section headers kept; markdownlint
clean across all 18 files. CHANGELOG left in the standard Keep-a-Changelog style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: defer release — keep v1.1 tooling/docs under [Unreleased]

The v1.1 work is dev tooling (gitleaks/zizmor/Dependabot, pre-commit, the lint
configs, the develop/main branch model) plus a docs copy-edit. None of the
tooling is in the release bundle (release.yml ships rigforge.sh/util/systemd/
config templates/README/docs/images/LICENSE/VERSION — not tests/, .github/, or
the dotfiles), and rigforge.sh is unchanged, so there's nothing user-facing to
release. Bumping to 1.1.0 (MINOR) would signal new functionality that isn't there.

Revert the premature VERSION bump (back to 1.0.1) and the changelog rollup; the
entries stay under [Unreleased] and will ship with the next release that actually
changes something for users. main stays at the last release (1.0.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: worker HTTP API open (read-only) by default (#125)

ACCESS_TOKEN no longer defaults to the rig name. Left unset, the rig's
:8080 xmrig API is served `restricted` (read-only) with NO token — which
matches Pithead's new default no-auth stats probe, so a stock rig needs
zero token coordination. Setting ACCESS_TOKEN re-enables Bearer auth (now
validated only when non-empty); match it dashboard-side with
workers.api_auth token|name.

Pairs with pithead#171/#172 (the dashboard's single config-driven worker
API probe).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(deps): bump actions/checkout in the github-actions group (#124)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@df4cb1c...9c091bb)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vijit Singh <vijit.n.singh@gmail.com>

* docs: rewrite for voice consistency (#126)

* docs: rewrite CONTRIBUTING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite RELEASING.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite SECURITY.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/benchmarks.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/configuration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/faq.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/getting-started.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/hardware.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/how-it-works.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/operations.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite docs/pithead-integration.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: rewrite tests/README.md for voice consistency

Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs.
No technical facts, commands, flags, addresses, ports, or numbers changed
(code fences and inline code verified byte-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: ponytail cleanup — drop dead duplicates + stdlib trims (#128)

Behaviour-preserving cleanups from a ponytail (over-engineering) audit. The audit's
headline finding is that rigforge is already lean; these are the few real cuts.

rigforge.sh:
- macOS tuning block re-set YIELD/PRIORITY/NUMA/HTTP_RESTRICTED to the exact values
  the shared defaults block already sets — dead no-ops (plus an orphaned priority
  comment duplicating the default's). Keep only the values that actually differ.
- link_cli: fold the `ok` flag into the `if` it guards.

util/proposed-grub.sh:
- L3-cache + socket parsing: `lscpu | grep ... | awk` -> single `awk`; strip via
  bash builtin (`${L3_RAW//[!0-9]/}`) instead of `echo | sed`.
- 1GB-pages read: `$(<file)` instead of `cat ... || echo 0` (the file is guarded
  by -f already, and the default is set to 0 above).

No behaviour change; 614-test suite green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: rename config files for clarity (minimal + reference) (#127)

config.json.template -> config.minimal.json   (the copy-me starter: just the pool URL)
config.advanced.example.json -> config.reference.json   (the full reference: every key
  with its default, enforced complete by the #23 test)

Matches the same rename in Pithead for cross-repo consistency. Content unchanged — the
the starter is still just the pool URL. Updated all references (rigforge.sh, RELEASING,
docs/configuration, .gitignore, the release workflow + macOS e2e bundle, and the config
tests). CHANGELOG keeps the old names (historical).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* test: cover the edge cases the suite was missing (#129)

Adds ~58 assertions for previously-untested branches and degenerate
inputs, all at the run.sh layer (stub-driven, hardware-independent,
bash 3.2-safe) per tests/README.md — no new files, SHELL_FILES unchanged.

Behaviours now covered:
- check_prerequisites: the whole jq-bootstrap (apt/dnf/pacman/none/brew/
  no-brew/already-present) — was entirely untested
- install_dependencies: the dnf and pacman branches + the no-manager
  warn-not-abort path + the apt-cache-present linux-tools branch
- install_service reboot-gate (#audit A2): enable-but-NOT-start when a
  reboot is pending, restart on rebuild, start on steady state, plus
  CPUPOWER_PATH substitution in ExecStartPre
- compile_xmrig drops the clone (rm -rf) on a commit mismatch (#18)
- config-gen round-trips: bracketed IPv6 url, single-pool tls, empty
  ACCESS_TOKEN -> JSON null
- restore of a corrupt / config-less archive: fails AND leaves the
  existing config.json untouched (no silent clobber)
- uninstall without --yes aborts cleanly on 'n' and reverts nothing
- mac_start guard (no built worker -> "run setup first"); mac_stop
  launchd-owned delegation
- numeric degenerate inputs: _median/_stddev/_mean on empty/single
  sample; _watts_from_energy backwards counter w/ no wrap-max; the
  no-power efficiency fallbacks in _accept_better and _autotune_score
- compute_build_jobs sub-2GB floor; the 1G fstab line on a fresh write;
  the rest of the xmrig.service hardening block

Verified: make lint clean; make test 664/664; make coverage 91.34%
(floor 80); suite passes 687/687 on a real Ubuntu 24.04 / bash 5.2 rig.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e-real): don't send an API Bearer when ACCESS_TOKEN is unset

The worker HTTP API now defaults to open (read-only) with no token, so
XMRig 401s the stale "Bearer $(hostname)" the verify warmup still sent.
Under set -e + pipefail that surfaced as curl -f exit 22 aborting verify
silently before any output. Send the header only when ACCESS_TOKEN is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tune): don't send an API Bearer when ACCESS_TOKEN is unset

_read_api_hashrate always sent "Bearer ${ACCESS_TOKEN:-}", so with the
new open (no-token) API default it sent an empty Bearer, XMRig 401'd it,
and curl -f (exit 22) aborted the caller under set -e — breaking every
live-hashrate read: autotune (incl. the monthly timer), tune --live,
tune --confirm, and the upgrade re-tune. Send the header only when
ACCESS_TOKEN is set. The suites stub this via API_CMD, so only the
real-hardware e2e gate exercised the curl path that regressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* release: v1.1.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(release): promote develop to main via a reviewable PR, not a direct push

The develop->main promotion goes through a PR (release gate + audit trail);
main is protected. Fast-forward/rebase-merge so the tag lands on develop's
release commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tune): cover _read_api_hashrate's auth-header logic; harden for bash 3.2

Adds a unit test driving the real _read_api_hashrate with a stubbed curl —
asserting no Authorization header when ACCESS_TOKEN is unset and a Bearer
when set. This is the branch the rest of the suite stubs via API_CMD, so it
was uncovered (diff-cover patch-coverage gate flagged the new lines) and is
exactly where the open-API regression hid. Also swaps the empty-array curl
arg for an if/else, since "${arr[@]}" on an empty array trips set -u on
bash 3.2 (the macOS CI job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* v1.2 audit fixes: config permissions, SERVICE_NAME, GRUB escaping, robustness sweep, doc corrections (#150)

* fix(audit): close the v1.2 audit's code findings

- setup: chmod 600 config.json at interactive bootstrap, before the operator
  is told to paste a wallet/token into it (#131)
- install_service: honor the documented SERVICE_NAME override for the unit it
  writes/enables/starts, so uninstall manages the same unit setup installed (#133)
- grub: escape \ & | in the sed replacement when rewriting
  GRUB_CMDLINE_LINUX_DEFAULT, so a legal pre-existing kernel param can't
  corrupt /etc/default/grub; new _sed_escape_replacement helper (#134)
- robustness sweep (#135): warn/error to stderr; arm the tune cleanup trap at
  mktemp time (live-mode aborts leaked TUNE_TMP); doctor asserts the live
  config keeps http.restricted=true; HOME_DIR deny-list for bare system dirs
  ahead of sudo rm -rf; local actual in compile_xmrig; awk -v instead of
  interpolated program text (7 sites); quote $i; bounded poll instead of a
  fixed sleep in the macOS process test; fail-fast apt guard in the container
  e2e; derive Makefile SHELL_FILES from git ls-files

Each change carries a regression test; suite 687 passed / 0 failed, make lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: drop stale token-gated API claims; license + donation clarity

- SECURITY.md, README.md, docs/operations.md: the :8080 API has been open
  read-only by default since v1.1.0 — say so instead of overstating the
  posture as token-gated (#132)
- README: note XMRig's GPLv3 (compiled from upstream source on the user's
  machine, no binaries redistributed) and whose wallet the Donate address is;
  add .github/FUNDING.yml pointing the Sponsor button at the Donate section (#136)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(setup): prompt for the stack's stratum password at first run (#151)

The Pithead side shipped opt-in stratum auth (p2pool.stratum_password,
pithead#207): when set, the proxy rejects any rig whose pool `pass` doesn't
match. RigForge already flows pools[].pass through to the xmrig config — the
missing piece was zero-friction entry: the interactive first run now asks for
the password (Enter skips it), pre-validated with parse_config's exact pass
rule so a bad value fails before anything lands on disk.

An empty answer writes no `pass` key, keeping the no-auth minimal config
byte-identical to before. Docs gain the rotation runbook (regenerate on the
stack, re-paste + `apply` on each rig; stale rigs log `Permission denied` by
design).

Automated fetch of the secret stays deliberately unbuilt until pithead#208
picks a retrieval channel.

Closes #113

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(pools): tls-fingerprint — pin the stratum server cert by SHA-256 (#153)

Verified against the pinned XMRig v6.26.0: with tls:true and no fingerprint,
xmrig performs NO server certificate verification for stratum (Tls.cpp
verifyFingerprint returns true when unset) — so the pin is not a self-signed
nicety, it is the only server authentication stratum-TLS has.

- pools[].tls-fingerprint passes through parse/generate verbatim, emitted
  only when set (pre-#115 configs produce byte-identical POOLS_JSON)
- validated as 64 hex chars, either case; the rejection message embeds the
  openssl one-liner that produces the right form
- a pin without tls:true is a hard error — a silently-ignored pin would
  leave the operator believing they're protected
- docs: trust model stated plainly (TLS without a pin = encrypted but
  MITM-able), fingerprint one-liner, rotation runbook, troubleshooting row

Setup-time "stack offers TLS" detection stays unbuilt until pithead#261
picks its terminator/port model; the config surface won't change.

Closes #115

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(api): sister API — read-only stats superset on its own port (#99) (#154)

Opt-in ("api": "enabled", default off): a second HTTP endpoint (default
:8081) serving XMRig's /1/summary + /2/summary passed through verbatim plus
a namespaced `rigforge` object — applied tune knobs and last-run summary,
RAPL watts + hashrate-per-watt, the doctor probes (HugePages, MSR, governor,
RAM channels/speeds, memory-profile/SMT, throttling) as JSON, and pinned
version provenance — plus bare /health and /tune endpoints.

Serving is systemd socket activation: the .socket unit (Accept=yes) owns the
listener and spawns one short-lived `rigforge.sh api-serve` per request over
stdin/stdout. No resident daemon, no new dependencies (systemd + curl + jq,
all already required). The per-connection unit is sandboxed read-only
(ProtectSystem=strict, NoNewPrivileges, PrivateTmp, ...) — the handler is
read-only by construction (GET only, fixed routes, nothing from the request
is executed or logged) and the sandbox enforces it at the filesystem level.

Auth reuses ACCESS_TOKEN unchanged (open by default, exact Bearer when set —
never echoed anywhere). XMRig down still answers 200 with an
"xmrig_api": "unreachable" marker: health data matters most then. RAPL is
the only power source in the handler — TUNE_POWER_CMD stays env-only, since
config-derived text must never reach eval in a network-facing process.

Wired into setup, `apply` (the config-change path), and uninstall, mirroring
the autotune-timer toggle. Linux-only.

Closes #99

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): worker↔stack contract gate + sister-API hashrate-impact guard (#155)

tests/e2e-pithead.sh (#114): release-gated, manual, root — drives THIS rig's
real provisioned worker against a live Pithead stack named by PITHEAD_URL and
asserts the contract documented in pithead-integration.md:

- connect: stratum job + accepted share against the stack; the user-label
  discovery contract
- worker-api: open read-only default, 0.0.0.0 bind, restricted refusal, and
  the custom-token 401/200 pairing (worker half of pithead#171)
- api-impact: the #99 perf guard — baseline live hashrate vs 4 clients
  hammering :8081 back-to-back; fails if the loss exceeds
  E2E_API_IMPACT_TOLERANCE_PCT (default 3%)
- stratum-auth (E2E_STRATUM_PASS): right pass mines, wrong pass rejected
  with no jobs delivered, re-pasting recovers (#113's rotation runbook,
  proven mechanically)
- dashboard (E2E_DASH_URL): workers-alive shows the rig; a stopped rig
  drops off within the liveness window
- dev-fee: donate-level follows config, independent of the stack

The operator's config.json is snapshotted and restored (+ apply) on exit via
trap, whatever happens — this runs on production-adjacent rigs.

Belt for the braces: rigforge-api@.service gains Nice=19 +
IOSchedulingClass=idle so handlers can never compete with RandomX for CPU;
the unit test asserts the directive, the e2e phase proves the effect.

Closes #114

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* v1.5 completeness: api rebind + bearer-parsing fixes, integration coverage, standardized perf gate, changelog (#156)

* fix(api)+test+docs: v1.5 completeness — rebind bug, integration coverage, perf standard, changelog

Two real bugs from the completeness audit and the first live gate run:
- install_api: an api_port/api_bind edit + apply re-rendered the socket unit
  but never rebound — daemon-reload alone doesn't rebind a changed
  ListenStream, so the old port kept listening until reboot. Restart added;
  tested.
- api_serve: an Authorization header with no space after the colon (legal
  HTTP) 401'd a valid token. Whitespace-tolerant parsing; tested.

Integration coverage (all validated in the container e2e against a real /etc):
- in-container.sh deploys with "api": "enabled" — asserts the socket +
  handler units land fully rendered, and uninstall removes both.
- run.sh: apply reconciles the sister API on/off (autotune-precedent shape);
  port-change rebind; query-string + HEAD handler edges; corrupt
  tune-overrides answers 200 with nulls; second-pool tls-fingerprint index
  alignment; the Nice=19 perf-guard directive.

e2e gate hardening from the first miner-0 run:
- worker-api phase normalizes ACCESS_TOKEN before asserting the open default
  (operator rigs legitimately run tokened).
- dev-fee asserts the EFFECTIVE contract: max(config DONATION, the
  kMinimumDonateLevel compiled into donate.h) — XMRig clamps and autosaves,
  so lowering DONATION after a build needs a rebuild. Documented in
  configuration.md.
- stratum-auth guards a missing worker log on standalone runs.

Standardized performance testing:
- e2e-real gains a `perf` phase: offline 1M bench vs a COMMITTED per-host
  baseline (tests/perf-baselines/, E2E_PERF_RECORD=1 to record,
  E2E_PERF_TOLERANCE_PCT default 5). Runs inside `all`; a regression fails
  the release gate. tests/README.md documents the three standardized
  measurements (absolute, under-API-load, tune improvement).

Docs: CHANGELOG [Unreleased] now covers everything since 1.1.0; SECURITY.md
documents the :8081 posture; pithead-integration.md gains the sister-API
contract section (the pithead#235 feed); README table mentions :8081.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(net): network-level leak validation + spirit-of-XMRig wire contract

New `network` phase in the e2e-pithead gate — the boundary, enforced on the
wire of a real rig:
- outbound: xmrig's only established TCP peers are the configured pool
- listeners: the miner owns :8080 and nothing else; :8081 exists exactly
  while api is enabled and closes on disable
- superset on the wire: sister /2/summary minus `rigforge` carries EXACTLY
  the key set xmrig's own API serves — nothing renamed, dropped, or invented
- leak sweep: with a token and a stratum pass configured, no byte of any
  response on either port (authed, unauthed, errors, headers) contains
  either secret

Unit-level wire-contract locks in run.sh: exact /health and /tune key sets
(the pithead#235 contract), application/json + exactly three headers with no
server banner to fingerprint, and authed-200 responses from every endpoint
asserted free of ACCESS_TOKEN and pools[].pass.

Docs: pithead-integration.md states the XMRig-convention alignment and the
enforced network boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: nudge (empty)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* chore(dev): repo-health tooling + miner-0 perf baseline; fix baselines README (#157)

Fixes develop CI: tests/perf-baselines/README.md was written with shell-style
comment lines that markdownlint reads as malformed headings — now real
markdown. The class of failure is closed, not just the instance:

- pre-commit gains markdown + yaml hooks (via the same make targets CI runs,
  so file lists and version pins stay single-sourced) — `make lint` alone
  never sees .md/.yml, which is exactly how this slipped through
- actionlint joins CI (pinned + checksum-verified): zizmor audits workflow
  security, actionlint now checks workflow correctness; `make lint-actions`
  locally, wired into lint-all
- `make dev-setup`: one command for the local toolchain (brew/apt) + git
  hooks, printing the CI-pinned versions to match
- `make ci`: everything CI runs that can run locally, adding the container
  e2e when Docker is up

Plus the first committed perf baseline: miner-0 (7800X3D) at 10789.5 H/s
offline 1M bench, recorded 2026-07-10 — arms the e2e-real perf regression
gate from the next run onward.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* release: v1.2.0 (#158)

* test(perf): commit the EPYC fleet baselines (miner-1..7) (#160)

Recorded on each rig right after the v1.2.0 deploy — seven 96-core EPYC
7642s at 36.7-36.9 kH/s offline 1M bench. Arms the e2e-real perf regression
gate for the whole fleet; miner-0 (7800X3D) was committed with the gate
itself.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(api): relax handler scheduling so it isn't starved under full mining load (#161)

The sister-API handler ran at Nice=19 + IOSchedulingClass=idle — SCHED_IDLE-
like "run only when a core is free." On a rig where XMRig pins every core
(96 threads on 96 cores) that is never, so a /health request took ~51s on a
loaded EPYC. Found confirming the production fleet through the API after the
v1.2.0 deploy; the api-impact e2e phase missed it because it ran on miner-0
(16 threads, spare capacity) and only asserted hashrate wasn't shaved, never
that the API stayed responsive.

Fix: Nice=10 (drop the idle IO class). Still cedes CPU to the miner — one
short-lived handler is a rounding error against N compute threads — but the
scheduler gives it a slice. Validated on loaded miner-3 (96-core EPYC):
/health 51s -> 8.2s, hashrate unchanged (36863 -> 36872 H/s, within noise).

The api-impact phase now bounds response latency under load too
(E2E_API_LATENCY_S, default 15s), so the responsiveness half of the perf
contract can't regress silently. Run it on a saturated many-core rig.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* release: v1.2.1 (#162)

* feat(bios): guided, resumable BIOS tuning — detect, guide, reboot, re-verify (#165)

New top-level `bios` verb (#80): the walk-through for the firmware settings
tune can't reach from the OS — the memory profile (XMP/EXPO/DOCP), SMT, and
the CPU power/boost posture (--efficiency swaps the boost item for Eco-Mode
+ Curve Optimizer, same target vocabulary and precedence as tune).

detect: reuses doctor's exact probe expressions (_mem_summary, SMT_CONTROL,
_cpu_eff_khz vs CPUFREQ_MAX with MIN_CLOCK_PCT) so bios and doctor can never
disagree about the same rig. guide: prints the firmware context, walks the
pending items one at a time with the exact menu path for the detected board
(ASUS/ASRock/Gigabyte/MSI + a generic fallback), and saves only the still-
pending items to $WORKER_ROOT/rigforge-bios.json (now in backup/restore).
re-verify: re-reads the same probes and flips an item to applied ONLY when
its OS-visible fingerprint says so — the boost item needs the miner running
to measure, and with it stopped it stays pending with an honest note rather
than guessing. Converged runs delete the state and hand off to `tune --live`
(the hardware envelope changed). RigForge never writes BIOS itself.

Auto-elevates like tune (dmidecode needs root); Linux-only hard error
elsewhere; unknown flags use the #149 error template. 27 new assertions
drive every path off the #78 firmware fixtures. No new config keys.

Closes #80

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(bios): close the #165 patch-coverage gap (21 lines) (#166)

The bios PR merged with the coverage gate red — a chained command merged
without gating on the check result. This closes the gap properly:

- Real untested branches now tested: --perf flag, the ASRock/Gigabyte/MSI
  menu paths, missing SMT sysfs (no judgment emitted), the sudo auto-
  elevation hop, unknown ids in a stale state file (skipped, not fatal),
  the non-root memory re-verify keep, and the dispatch entry.
- Two kcov-blind constructs restructured into attributable form (behavior
  identical, proven by the unchanged suite): the state writer's brace-group
  pipeline became three plain guarded assignments, and the verify loop's
  process substitution became an assignment + herestring.

805 passed / 0 failed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* release: v1.3.0 (#167)

* fix(api): serialize handlers — polling can no longer shave hashrate (#168)

The v1.3.0 gate caught the Nice=10 tradeoff on the 16-thread rig: four
concurrent handlers under worst-case polling shaved 3.1% hashrate (10754.9
-> 10421.7 H/s), past the 3% api-impact tolerance. Concurrency is the
CPU-theft multiplier — so bound it structurally instead of fighting
scheduler priorities: the socket admits ONE handler at a time
(MaxConnections=1). Extra connections queue in the accept backlog; the real
consumer is a single dashboard poller, so queueing costs it nothing, and
it's a free DoS bound.

Also lands the api-impact latency assertion for real: the #161 hunk was
lost to a branch-cut race (checkout -B after edits) and I misreported it
as shipped — the phase now bounds /health latency under full mining load
(E2E_API_LATENCY_S, default 15s) alongside the hashrate check.

Changelog: amended the not-yet-tagged [1.3.0] section.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): api-impact + network phases work standalone against a tokened rig (#169)

Running api-impact alone on miner-0 exposed a phase-isolation bug: its
:8081 probes (come-up poll, hammer, latency, superset read) used bare curl,
which 401s when the operator config keeps its own ACCESS_TOKEN. Full runs
masked it — the worker-api phase incidentally clears the token first.

New api8081() helper (token-aware, like api8080) used everywhere the phases
read the sister API on the operator's behalf. The worker-api phase's bare
curls are deliberate and untouched — they test the token contract itself.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(api): probe cache + CPUQuota ceiling — polling cannot shave hashrate, at all (#170)

Serializing handlers (#168) queued the same continuous work: the gate still
measured a 4.5% shave under back-to-back polling on the 16-thread rig. The
per-request cost IS the problem (#164): sourcing the script + ~15 jq spawns
+ the probe pass, every time.

Two-layer fix:
- The expensive probe blocks (health, tune) are cached with a 5s TTL in the
  unit's RuntimeDirectory (writable under ProtectSystem=strict). Repeat
  requests serve cached bytes; power stays fresh (hs-per-watt is coupled to
  the live hashrate). Also collapses the loaded 96-core latency (#164).
- CPUQuota=40% on the handler cgroup: the adversarial ceiling — at most 40%
  of ONE core (~2.5% of a 16-thread rig) no matter how hard anything polls.

Also repairs two more #161 branch-cut losses discovered here: the run.sh
unit assertions never landed, and the stale Nice=19 assert FALSE-PASSED by
matching the new template's comment text. The unit asserts are now
grep-anchored to directive lines (^Nice=10$, no IOSchedulingClass,
^CPUQuota, ^RuntimeDirectory) so comments can't satisfy them. Cache
behavior tested: store, poisoned-cache hit, TTL=0 bypass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(api): admission control — pace accepts to bound cache-pollution rate (#171)

Third gate iteration told the real story: with the probe cache the handler
answers in 1s, yet the shave WORSENED to 4.8% — faster handlers let the
unpaced hammer drive more requests/second, and the damage is L3-cache
pollution from per-request process churn (bash spawn + exec), not CPU time.
CPUQuota held its 40% ceiling and the shave still exceeded it; RandomX on a
7800X3D's V-cache is maximally sensitive to eviction, and pollution scales
with request RATE — immune to Nice and quotas.

The fix is admission control, not more scheduler knobs: with the serialized
socket (MaxConnections=1), the handler holds its slot one extra second
after the response is on the wire. Accept rate is bounded to <1/s no matter
how hard anything polls; a dashboard on a 5-15s interval never notices;
sleep costs no CPU and no cache. One line in the dispatch, covered by the
existing black-box test.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): honest api-impact hammer — no client-side churn on the rig (#172)

Three fix iterations (quota, cache, pacing) barely moved the measured shave
because the measurement was contaminated: the hammer ran four while-loops ON
THE MINING RIG, each iteration spawning jq + curl — client-side process
churn polluting the same L3/V-cache the assertion measures. A real dashboard
polls from the stack host; the rig sees only the server path.

The hammer now reads the token once and runs ONE long-lived curl per client
using URL-range sequencing ([1-10000]): zero client spawns between requests,
full pressure on the server. The server-side defenses landed along the way
(Nice=10, CPUQuota=40%, probe cache, MaxConnections=1, 1s pace) all stand —
each fixed a real issue the iterations exposed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(api): re-architect on the xmrig model — persistent server + refresh timer (#164) (#173)

Four gate iterations proved the diagnosis: no scheduler knob fixes a
per-request-process architecture. The v1.2.x design (systemd Accept=yes,
the inetd model) paid a full unit+cgroup+bash-parse+jq lifecycle per
request — 3-5% hashrate under worst-case polling on 16 threads, however we
tuned priorities, quotas, caches, or pacing.

Now it works the way xmrig's own API does (persistent listener, serves
pre-built state) combined with the node_exporter textfile pattern
(background producer writes files atomically):

- rigforge-api-refresh.timer: the probe pass every 15s, Nice=19 +
  IOSchedulingClass=idle, writing summary/health/tune bodies to
  /run/rigforge-api via tmp+rename. All cost lives off the request path,
  constant regardless of polling.
- util/api-server.py: ~80 lines of python3 stdlib (an apt-level guarantee
  on Ubuntu 24.04), single persistent sandboxed process, no banners,
  Bearer auth read at startup, FAIL-CLOSED on an unreadable config. A
  request is a tmpfs read: microseconds, no spawn, no cgroup churn.

Deleted: the bash HTTP handler, the TTL cache, the socket pair,
MaxConnections, CPUQuota, the accept pacing. api-serve -> api-refresh.
Upgrades remove the old socket units on sight. Wire contract unchanged
(routes, token rule, exact headers and key sets). Staleness <=15s, within
xmrig's own 10s hashrate-window resolution. python3 joins the apt deps,
the container-e2e prereqs, and the test stubs; the python wire suite skips
loudly in the apt-free kcov container (enforced by the other CI jobs).

Closes #164

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): let the miner re-establish stratum before the outbound check (#174)

The network phase follows api-impact, whose cleanup apply restarts the
miner — the ss inspection could run before the pool connection came back
(caught on the first all-green run of the new API architecture, where it
was the sole failure). Poll up to 45s for an established peer first.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.

Pin & verify the XMRig source build (reproducible, supply-chain hardening)

1 participant