Skip to content

release: v1.4.0#473

Merged
VijitSingh97 merged 83 commits into
mainfrom
release/v1.4.0
Jul 11, 2026
Merged

release: v1.4.0#473
VijitSingh97 merged 83 commits into
mainfrom
release/v1.4.0

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Cuts v1.4.0. Bumps VERSION 1.3.1 → 1.4.0 and dates the CHANGELOG ([Unreleased][1.4.0] - 2026-07-11).

Contains the full v1.4 feature set already merged + security-reviewed on develop (roadmap #390). The #423 XvB-overshoot gate is resolved (validated on live dwell-mechanics + the fixed sim — see #423).

Release mechanics after merge: real merge (not squash) → make release on gouda (digest-pinned, cosign opt-in/skipped) → back-merge main→develop → deploy gouda then prod via pithead upgrade.

🤖 Generated with Claude Code

VijitSingh97 and others added 30 commits July 10, 2026 12:17
Keeps main an ancestor of develop after the patch cut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation docs (#430)

Two automations collided on miner-0 (2026-07-10) because nothing on a shared
box said it was claimed. This is the pithead half of the fix; rigforge#183 is
the canonical mechanism and every decision here follows it.

- lib.sh: rig_lock() copied verbatim from rigforge#183 (env-overridable
  RIG_LOCK_FILE/RIG_LOCK_HOLDER per its note 6) — the same lock path on every
  box IS the protocol. Plus rig_lock_remote(): hold the lock on a remote box
  via a long-lived ssh whose stdin pipe (local FD 8) dies with this process,
  so the kernel drops the remote lock on any exit, kill -9 included.
- run.sh: take the lock on the target before preflight — in-process for
  --local, over ssh for --host. Read-only modes (--check/--readiness) take it
  shared; everything else exclusive. Busy exits 75 naming the holder;
  RIG_LOCK_WAIT=1 queues.
- e2e.sh: take the loaner rig's exclusive lock in preflight, before the
  borrow mutates its xmrig — released by the kernel after the EXIT-trap
  restore runs.
- selftest.sh (tier 1, sandboxed paths, no root): second exclusive acquire
  exits 75 naming the holder; shared+shared coexist; shared vs exclusive
  excludes both ways; RIG_LOCK_WAIT=1 blocks then acquires; the sidecar is
  written on acquire and removed on normal exit. Skips only where flock
  doesn't exist.
- docs/release-server.md: the static allocation table (miner-0=rigforge,
  miner-1/2=pithead loaners, miner-3..7=prod, gouda=bench+release,
  pithead-prod=prod), the run-lock contract, and the off-box check line.

bench-lease retirement on the boxes is post-merge ops, not this PR.

Closes #430

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test: shared-bench rig flock in the tier-4 harness + allocation docs (#430)
PyPI yanked grpcio 1.82.0 (bad protobuf constraint), so every `uv lock`
warned. Move the floor to 1.82.1, the first non-yanked release above it.
grpcio core has no protobuf dependency, so protobuf stays on 6.x and the
checked-in Tari gRPC stubs (floor GRPC_GENERATED_VERSION 1.78.0) import
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build(deps): move grpcio floor off yanked 1.82.0 (#419)
`version` (plus `-V`/`--version`) prints one line identifying the build from
the provenance export_build_provenance already computes each run — no re-read,
no git call, no network. `doctor` repeats it in its header so every pasted
diagnostics report carries the version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#371: fix CONTRIBUTING's `make test` description — it needs Docker
(lint-proto shells out to buf via docker), and add lint-docs-voice to
the lint-surface list. The test-inventory-check item was already
resolved by #414 (target no longer runs it), so nothing to do there.

#372: add CODE_OF_CONDUCT.md (verbatim Contributor Covenant v2.1),
filling the last empty GitHub community-profile slot. Enforcement
reports route through the same private channel as SECURITY.md. Link it
from CONTRIBUTING. Excluded from lint-docs-voice (verbatim upstream)
and markdownlint (file-level inline disable) like other verbatim files.

#373: pin build/tor/Dockerfile to alpine:3.24 (the digest's minor line)
instead of floating `latest`, and drop the follow-up comment; make
build-pruned-chain.sh's SRC_DIR a required var instead of a maintainer's
personal, pre-rename-repo path default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#426)

The release pipeline needed a manual relaunch on most recent cuts and died
early on a reimaged box. Two release.sh robustness fixes:

- #429: wrap the registry reads (manifest_digest digest capture + the smoke
  stage's raw manifest read) in a bounded retry-with-backoff via a shared
  retry_registry_read helper, so GHCR's read-after-push consistency lag no
  longer kills stage 4. Defaults to 5 tries / 3s backoff, tunable via
  PITHEAD_REGISTRY_READ_RETRIES / _BACKOFF; a genuinely-missing image still
  dies after the retries exhaust. The inspect call is a stubbable
  buildx_inspect function so the retry path is unit-testable.

- #426: preflight now verifies the lint/test toolchain (shellcheck, shfmt,
  node/npx, uv/uvx) is on PATH before building, failing fast with the fix and
  a pointer to docs/release-server.md instead of dying mid-gate with a bare
  "shellcheck: not found". Only enforced on the paths that run the test gate.

Adds a "provision the lint/release toolchain" snippet to docs/release-server.md
(pinned versions matching CI), a CHANGELOG [Unreleased] entry, and tier-1 shell
tests for both the retry (stubbed inspect fails N times then succeeds, plus the
exhaustion path) and the preflight check (PATH missing a tool).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat: add `pithead version` and version in doctor's header (#386)
`pithead status` now re-renders the dashboard's own per-chain initial-sync
progress from /api/state (127.0.0.1:8000, host-local, no auth): while a chain
is syncing it prints the percent and blocks remaining inline, instead of only
"check the dashboard". No ETA — block rate isn't sampled, so blocks-remaining
is the honest figure. Lines are skipped once both chains are synced or when the
dashboard app isn't answering yet, so status degrades quietly.

`stack_up` now ends with a one-time "what happens next" epilogue explaining the
sync-then-mine hold (#35) and where to watch progress, keyed on a marker file
beside .env so it shows once, not on every restart.

Tier-1 shell tests stub curl with a canned /api/state body (real jq parses it);
docs (operations.md, getting-started.md) and CHANGELOG updated.

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

fix(release): retry GHCR reads and preflight the lint toolchain (#429, #426)
…gress-384

status: per-chain sync progress + first-run epilogue (#384)
The v1.4 spine: edit config.json from the dashboard, with the host — never
the container — validating and applying.

- pithead: dashboard.control.enabled (default off, fails closed without a
  dashboard password), ./data/control spool (requests/ container-rw;
  staged/results/audit host-owned), `apply --dry-run [--porcelain]` +
  PITHEAD_CONFIG_FILE, `control-run-pending` runner dispatching only
  apply --dry-run/--porcelain and apply -y on UUID-validated JSON intents,
  systemd path-unit trigger, Caddy X-Auth-User actor header.
- dashboard: control_service (secret-masking round trip, atomic spool
  writes), routes registered only when enabled (404 off; X-Pithead-Control
  CSRF header), Configuration view with preview → typed-confirm → commit.
- compose: spool mounts with the rw/ro trust-boundary split; config.json
  prefill read-only.
- tests: tier-1 shell (dry-run, runner claim/validate/commit/expiry,
  fail-closed flag), pytest (masking, routes, polling; patch coverage
  100%), compose mount invariants, node tests for the form logic.
- docs: configuration/dashboard/operations/SECURITY + changelog.

Refs #33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security-critical fixes for the internet-facing (Tor onion), root-privileged
control channel before it leaves draft. Each ships with a test at the lowest
honest tier and a house-voice CHANGELOG/SECURITY/doc update.

Must-fix (security):
- CRITICAL: reject control characters/newlines in EVERY config string leaf,
  centrally in parse_and_validate_config (the chokepoint preview + commit both
  run), closing the .env line-injection → root image-substitution RCE.
- HIGH: fail closed — the control channel on a published onion now requires
  dashboard.onion.client_auth (mirrors the onion-without-password refusal).
- HIGH: control_approval_gate fails closed on destructive commits (re-derived
  host-side, not trusted from the container), pointing at #338; hook left for
  #338 to drop in out-of-band Telegram approval. Non-destructive commits pass.
- MEDIUM: chmod 600 the staged copy carrying merged secrets; SECURITY.md records
  the config.json bind mount (not the API masking) as the real secret boundary.
- MEDIUM: bound the root-runner DoS — refuse >64 KB intents before jq parses,
  cap 50 intents/run, sweep staged/ + requests/ older than an hour.

Grafts from #437 / #438:
- Refuse symlinked/non-regular claimed request files (never follow them).
- Merge config.reference.json under the sparse config so the form shows the full
  schema; secrets masked after the merge; missing reference degrades gracefully.
- Poll loop rides out the transient fetch failure of a mid-apply container
  recreate instead of dropping to an error state.
- Mask healthchecks.ping_url in the config API (capability secret).
- Strict canonical uuid4 id gate (version + variant nibbles) on the runner.

Housekeeping: corrected the PR-body test counts.

Refs #33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…close test gaps

The commit approval gate re-derived destructiveness from describe_change's
DEST rows, but that flag labels only the enable/change direction — disabling
the dashboard login, downgrading onion client-auth, clearing the stratum
password or repointing the Telegram bot produced zero DEST rows and applied
as root without approval. The gate now default-denies an explicit
security-sensitive env-key set (CONTROL_SENSITIVE_KEYS: auth, onion, the
control channel itself, payout wallets, Tor egress firewall, clearnet sync,
stratum password, RPC/stratum binds, Telegram credentials) in EVERY
direction, computed from the same host-side dry-run a preview runs — the
security decision no longer shares describe_change's cosmetic INFO/DEST
list, and no container-supplied flag is consulted. The DEST refusal stays as
the second, independent check; #338 remains the single plug point.

Also:
- stage the preview copy under umask 077 (no create-then-chmod window) and
  assert mode 600 in the tier-1 suite
- fix provision_onion_client_auth aborting the whole apply on hosts without
  GNU stat (BSD stat fallback + if-form for the trailing chown guard),
  surfaced by the new onion-baseline test
- tests: security-control disable/downgrade commits are refused with
  config.json untouched (auth off, client-auth downgrade, stratum-password
  clear, bot-token repoint); forged destructive:false in the request AND a
  tampered result file are both still refused; unchanged sensitive keys do
  not trip the gate; per-run 50-intent cap processes exactly 50 and leaves
  the overflow; non-v4 uuid ids are discarded
- correct the CI invocation comment in configview.test.mjs (*.test.mjs glob)
- SECURITY.md + docs/dashboard.md describe the default-deny gate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The security re-review found the "default-deny" gate was a blocklist:
P2POOL_CLEARNET, XVB_TOR_ENABLED and HEALTHCHECKS_PING_URL were absent
from the sensitive set (a container could deanonymize the host IP or
repoint the dead-man's switch), and any future env key would silently
pass until someone remembered to enumerate it.

The gate now refuses any commit that changes an env key NOT in
CONTROL_DASHBOARD_EDITABLE_KEYS — operational tuning only, and only
keys derived from validated enums, booleans or numbers. Wallets, auth,
onion exposure, the control channel, Tor egress/clearnet toggles, node
endpoints, the XvB pool URL and donor id, credentials, and the
WALLET_CHANGED/CLEARNET_EXPOSED tamper-evidence alert toggles all stay
host-CLI-only, as does any key added later. Refusal keys off a
violation count so a blank porcelain row cannot slip past an emptiness
test. Docs updated; new refusal tests verified red under a neutered
gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CHANGELOG [Unreleased] unioned by hand; read-back verified all 18
entries from both parents survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#33 config editor + host-mutation control channel (candidate A)
…374)

The backup archive holds the stack's full secret material (.env, onion
private keys, the dashboard DB); chmod 600 protects it on the local disk
only. Encrypt it by default:

- `backup` prompts for a passphrase (twice, read -rs) and streams
  tar | openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -salt into a
  .tar.gz.enc — no plaintext archive ever exists on disk, and the
  passphrase travels over fd 3, never argv or a file. A failed pipeline
  removes the partial ciphertext.
- PITHEAD_BACKUP_PASSPHRASE covers unattended (--yes/cron) runs; --yes
  with no env var, an empty passphrase, or --no-encrypt falls back to
  the old plaintext tar.gz with a loud warning — never a silent lockout
  of the operator's own onion keys.
- `restore` detects the format by magic bytes (Salted__ vs gzip), so
  every pre-v1.4 plaintext archive restores unchanged with no flag. A
  cheap pre-flight decrypts only the first bytes and checks the gzip
  magic, so a wrong passphrase fails before tar writes anything to /.

AES-256-CBC because `openssl enc` has no portable AEAD mode (GCM is
refused by the enc utility); PBKDF2 at 600k iterations instead of the
one-round EVP_BytesToKey default. Verified against OpenSSL 3.x (Ubuntu
LTS) and LibreSSL 3.3 (macOS).

Tests (tier 1, tests/stack/run.sh): env-var and prompted encrypt →
restore round-trips, wrong passphrase fails loudly with live files
untouched, passphrase mismatch aborts, --no-encrypt + legacy plaintext
round-trip, failed encryption leaves no file behind, garbage archives
refused, encrypted restore without a passphrase fails clean.

Closes #374

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

CHANGELOG [Unreleased] unioned by hand; read-back verified all entries
from both parents survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend read_only: true + minimal tmpfs scratch from the three already-
hardened services (docker-proxy, docker-control, caddy) to the six that
matter most: tor, monerod, tari, p2pool, xmrig-proxy and the dashboard.
Each service keeps exactly its verified write paths — the bind-mounted
data dir plus a size-capped, noexec /tmp tmpfs (xmrig-proxy also gets a
uid-1000 tmpfs over its image-only home in case the binary persists an
API-driven config change). A compromised process can no longer stage
tools in, or persist changes to, its container image.

Write paths verified per service against build/*/ entrypoints, the
rendered configs, and (for tari) the pinned upstream image's
start_tari_app.sh. tmpfs mount modes/ownership are explicit because a
bare tmpfs mounts root-owned (verified against Docker 29).

Tests: tests/stack/test_compose.sh now asserts read_only + the exact
tmpfs target set per service for all nine (red-checked: removing
read_only from one service fails two assertions). The tier-3 mini-stack
dashboard mirrors the production posture, so the control-plane e2e now
proves the real dashboard code under a read-only rootfs. tor was
additionally boot-tested locally under read_only to a 100%-bootstrapped
healthcheck. Full runtime validation of monerod/tari/p2pool/xmrig-proxy
is tier-4 (gouda) before release.

Closes #377

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…als in one command

One command rotates the secrets that are otherwise generated once and live
forever: the local Monero RPC password (skipped in remote mode), the stratum
access-password when p2pool.stratum_password is "auto" (announced loudly —
every rig must update its 'pass'), and PROXY_AUTH_TOKEN (always). Before
anything changes, owner-only .bak-<timestamp> copies of config.json and .env
keep the old values recoverable; the #125 apply-incomplete marker makes a
failed recreate retryable via 'pithead apply'. Containers are recreated via
compose up (never restart, which would reuse p2pool's old --rpc-login args).

Closes #378

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chaining: when every argument is a bare subcommand name, pithead runs
them left-to-right, validating the whole chain before anything executes.
Rejected up front (nothing runs): non-chainable commands (setup, logs,
restore, reset-dashboard, info commands), duplicates, more than one of
up/down/restart, and down anywhere but last. Fails fast: the first
non-zero step stops the chain, the report names what ran and what
didn't, and that step's exit code propagates. Any flag/service/path
argument keeps the invocation on the single-command path, unchanged.

Completion: pithead-completion.bash (repo root, shipped in the release
bundle) completes subcommands and `logs <service>` (service names read
from the adjacent docker-compose.yml). bash sources it directly; zsh
via compinit + bashcompinit. The command list lives in one place
(PITHEAD_COMMANDS); tests pin the completion list AND the dispatch case
labels to it, so none of the three can drift.

Tier 1 coverage in tests/stack/run.sh: validation rules, execution
order, fail-fast + exit-code propagation, reject-runs-nothing (stubbed
docker log stays empty), single-command regression, completion
sourcing/suggestions, and the drift guard.

Closes #94

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bility (#349)

Two logs, one operator-security story: spot an attack on the Tor-published
dashboard and rotate the onion/password before it lands.

Config-change audit (extends #33): control_audit now records the changed
env-key NAMES (from the same host-side dry-run the approval gate re-derives)
— never values, several near-allowlist keys are secrets — and trims the log
to its newest 2000 entries past 512 KiB. Access log: every generated Caddy
vhost writes a JSON access log (4 MiB x 3 files via Caddy's native rolling,
mode 0644, credential headers redacted by Caddy's default) to a host dir the
dashboard mounts read-only.

The dashboard's Configuration view surfaces both: recent config changes
(only with the control channel on; /api/audit registered conditionally like
the other #33 routes) and recent accesses with a 24 h failed-login count
plus a rotate nudge at >=5 401s — over Tor there is no source IP, the rate
of failures IS the signal. Every field read from either log is treated as
hostile input and whitelisted to a safe ASCII charset server-side before it
can reach the browser (log injection rendered into the dashboard would be
stored XSS against the operator).

Tests at tier 1: shell black-box (keys-not-values, bounded growth, Caddyfile
log block), pytest (routes + sanitizer, hostile <script> content served
inert — shown red with the guard removed), node --test (panel states).
Caddyfile log block validated against the pinned caddy:2.11.4 image.

Closes #349

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VijitSingh97 and others added 22 commits July 10, 2026 19:59
Security review findings:
- MEDIUM (rollback replay): a cosign signature binds bytes, not a version,
  so an attacker controlling the release response could serve an OLDER
  genuinely-signed bundle at the requested tag's URL and silently downgrade
  the stack to a patched-vulnerable release — the signature still verifies.
  The dashboard upgrade path now reads the bundle's own pithead/VERSION
  (without extracting) and refuses unless it matches the host-derived tag.
  Verified: without the guard the older bundle extracts and the downgrade
  succeeds.
- LOW (ANSI injection): cosign stderr echoes registry-supplied bytes and
  error() prints via echo -e, so a hostile registry could inject terminal
  escapes; the snippet is now control-char-stripped.
- Aligned the test fake bundle to the real pithead/ top-dir layout.
- SECURITY.md: state the rollback guard; note the CLI verify-then-pull
  window (filed #451) honestly.

Deferred as follow-ups: #451 (verify-then-pull TOCTOU on the image path,
MEDIUM), #452 (verify on first install, LOW). make lint + make test green
(933 stack), rollback guard red-checked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-heal-424

Tor guard self-heal: restart tor when clearnet egress is stuck (#424)
Sign release images and bundle with cosign; verify fail-closed in upgrade (#376)
VERSION 1.3.1 -> 1.4.0, dashboard pyproject in lockstep, uv.lock
regenerated, CHANGELOG [Unreleased] promoted to [1.4.0] with the release
lede. Date 2026-07-12 is a placeholder — confirm at cut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#374 made `pithead backup` refuse an unattended run without a passphrase
(no silent plaintext archive of onion keys). The tier-4 harness takes
throwaway rollback backups via `pithead backup -y` — correct code now
refuses them, failing "safety backup created" + "backup succeeded". These
bench backups are ephemeral, so pass the explicit --no-encrypt opt-out
(the documented non-interactive path). Not shipped code (tests/ isn't in
the bundle); pure harness compat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…up-374-compat

test(e2e): --no-encrypt on the tier-4 harness throwaway backups (#374 compat)
The automated tier-4 matrix had zero coverage of any v1.4 feature — the
tier-4-only behaviors lived only in the hand-run #390 checklist, which
regresses silently between releases. Add a --hardening phase (local mode
only), wired into --mode matrix:

- #377: read-only rootfs is LIVE — each hardened service must reject a
  write to / (the writable /tmp tmpfs is excluded).
- #33: the control channel end-to-end through the REAL systemd path unit
  (tier-1 runs control-run-pending by hand) — enable control, let apply
  install pithead-control.path, drop a spooled allowlisted change and
  assert the UNIT applies + audits it, then a sensitive (wallet) change
  and assert it's refused host-side with .env untouched.
- #424: the heal ACTION (restart tor) brings the stack back healthy (the
  stuck-guard trigger is guard-selection luck, not reproducible).

Restores the baseline itself (re-apply with control off uninstalls the
path unit) so the root systemd unit never outlives the phase.

Static-checked (shellcheck clean, bash -n, selftest 120/0). LIVE-VALIDATION
PENDING on gouda — the path-unit timing + spool round-trips are only proven
against a real box; this commit is the draft under review, not a passed run.

Refs #457. Folds toward the #454 harness-robustness work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The heal action's whole point is restoring clearnet egress through fresh
circuits — asserting only "stack healthy after restart" doesn't prove that.
Probe egress with the stack's OWN mechanism (the doctor/healer curl: a
generate_204 through the tor container's SOCKS via --socks5-hostname, which
also resolves DNS through Tor).

Gated on a pre-restart baseline so live-Tor-network flakiness can't
false-fail the gate: a genuinely-bad Tor exit (outside our code — the exact
#424 condition) makes the baseline probe fail, and we then skip-with-warn
rather than fail; we assert RECOVERY only when egress was working to begin
with. Post-restart polls up to 120s for circuits to rebuild.

Probe confirmed live on gouda (generate_204 -> HTTP 204). shellcheck clean,
bash -n OK. Full phase still pending live validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ot the stack's SOCKS

Testing egress by curling the stack's OWN tor SOCKS is testing from the
inside with the stack's plumbing. Replace it with a black-box probe: a
dedicated, isolated Tor-client image (tests/integration/tor-client/, pinned
Alpine + tor + curl, non-root) that reaches the dashboard onion over the
REAL Tor network exactly as a remote user's Tor Browser would — sharing no
network, volume, or SOCKS with the stack.

The #424 heal test now asserts the onion is reachable from this external
client BEFORE and AFTER the tor restart (proving tor rebuilt circuits +
republished its descriptor), gated on the baseline so a bad live Tor network
can't false-fail. Doubles as the #343/#360 from-outside privacy-surface test
and automates the onion-client-auth item off the #390 manual checklist.

Secret handling: the client-auth key is piped pithead->container over stdin
entirely on the box — never an argv, an ssh arg, `docker inspect`, or a
shared-fs file. Validated live on gouda: the external client reached the
running dashboard onion -> HTTP 401 (Caddy's auth challenge = full inbound
path works from outside). shellcheck clean, bash -n, probe.sh shellcheck clean.

Honest scope: onion reachability is INBOUND; the clearnet-EXIT half of #424
isn't externally observable and stays with the doctor egress check (the
stack self-checking its own egress).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live validation caught a false pass: containers run non-root (#255), so
`touch /` on a WRITABLE rootfs already fails with EACCES ("Permission
denied"). The probe treated any failure as a pass — so it would green-light
a service that silently lost read_only. Now it asserts the error is
specifically "Read-only file system" (EROFS), which only a read-only mount
returns. Verified on gouda: caddy (read_only) -> EROFS -> pass; dashboard on
the v1.3.1 baseline (writable) -> Permission denied -> correctly NOT a pass.

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

Two harness-reliability fixes so the gate stops crying wolf:
- pool type: p2pool classifies by peer port, so nano (tiny sidechain, slow
  to find peers over Tor) can stay "Unknown" past wait_pool_ready's window.
  Unknown is a peer-discovery-timing state, not a misclassification — warn on
  it instead of failing; a WRONG determinate type (Main when we set Mini) is
  a real bug and still fails. This is the single flaky failure that survived
  the v1.4 gate re-runs (different scenario each time).
- results dir: wipe stale per-scenario artifacts at run start; e2e.sh
  preserves results/ across runs, so an old capture (a different branch)
  lingered and read as this run's state while diagnosing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This reverts commit d9b9e12.
…remature-bump

Revert the premature v1.4.0 version bump from develop
The first-party images pull by mutable :vX.Y.Z tag, so a re-pointed tag or
leaked GHCR token could substitute a root-running image. Close that vector
the cheap way — no off-repo secret to manage: make_bundle now rewrites the 5
first-party image lines in the BUNDLED compose to @sha256:<digest> (the
immutable digest promote just published), so installs pull content-addressed.
A post-condition refuses to ship a partially-pinned bundle.

cosign signing becomes OPT-IN defense-in-depth rather than a release blocker:
preflight computes COSIGN_ENABLED (cosign binary + COSIGN_KEY + committed
cosign.pub all present) and warns+skips instead of die-ing when absent;
sign_images/sign_bundle and the .sig upload are gated on it. A normal cut
needs nothing off-repo; an operator who wants signatures too can add a key.

Validated: the digest-pin sed applies cleanly to the real compose (all 5
lines pinned, post-condition passes); shellcheck clean; bash -n. Full
exercise is at real release time (needs promote's digests).

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

Live validation: 10/11 hardening assertions passed on real v1.4 code
(EROFS on all 6 services, external-Tor-client onion reachability before/after
the #424 restart, the #33 path unit firing + sensitive-key refusal). The one
miss was test-only: the allowlisted-change check read TELEGRAM_EVENT_NODE_DOWN,
which only renders when telegram is configured, so it was empty on the
telegram-off baseline. Switch to DASHBOARD_CHECK_UPDATES (allowlisted,
rendered unconditionally, cosmetic). Also bump the phase's baseline-restore
wait 180s->240s: that apply follows a control enable/disable + tor restart,
so it needs more settle time (180s timed out live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#376)

CI caught two things my first cut broke:
1. Digest bug: get_digest stores a FULL ref (repo@sha256:...), so appending
   it produced pithead-tor:tag@ghcr.io/.../pithead-tor@sha256:... — a broken
   double-repo ref at real release time. Now extract the bare @sha256 and
   validate it's a sha256 digest before pinning.
2. Existing release tests assumed the old behavior: make_bundle now needs
   digests set (it digest-pins), and sign_images is opt-in (skips without
   COSIGN_ENABLED). Updated the make_bundle test to seed digests + assert the
   bundle is digest-pinned with the bare-sha256 format, and the sign test to
   set COSIGN_ENABLED plus a new signing-off no-op assertion.

make lint (shellcheck+shfmt) + the release-tooling suite green locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ptin-digest-pin

release: digest-pin bundle images + make cosign opt-in (#376)
…ening-phase-457

test(e2e): tier-4 hardening phase for the v1.4 surfaces (#457)
VijitSingh97 and others added 2 commits July 11, 2026 17:21
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
develop-v1.5's wallet-healthcheck.sh uses env-var interpolation (`-u
"${VAR:-wallet}:..."`), not a hardcoded credential; the all-branches
fetch-depth:0 scan flagged it against the old empty-default-only regex. Both
sides must still be ${VAR:-...} expansions, so a real -u "admin:hunter2" is
still caught. (Same fix as #462 on develop-v1.5.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VijitSingh97 and others added 2 commits July 11, 2026 17:30
Surface the headline v1.4 capabilities from the top-level entry points so
they're reachable without reading the CHANGELOG:

- README "What it does": the dashboard bullet now names the opt-in control
  channel (edit config, one-click upgrade, audit/access logs, gated host-side).
- README "Common commands": add `version` and `rotate-secrets` rows; note the
  backup archive is passphrase-encrypted by default (`--no-encrypt` opt-out).
- README + docs/README index: dashboard/operations rows point at config
  editing, one-click upgrades, audit logs, secret rotation, and intruder watch.
- docs/README quick links: jump to the Configuration view and Watching for
  intruders.
- dashboard.md Sync Mode: mention `./pithead status` per-chain progress (#384)
  alongside the log tail, matching getting-started.

The deep docs (dashboard.md control channel + audit panels, operations.md
rotate-secrets/chaining/completion/intruders, configuration.md control keys,
releasing.md signed releases, architecture.md read-only roots, SECURITY.md
control-channel model) already covered these; this makes them discoverable.

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

v1.4 documentation coverage + discoverability
@VijitSingh97
VijitSingh97 merged commit 59bddf2 into main Jul 11, 2026
16 checks passed
@VijitSingh97
VijitSingh97 deleted the release/v1.4.0 branch July 17, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant