logs.mjs --rotation reads the old fingerprints from the dated .old-* files once the .next files are gone; fresh-repo.sh scrubs the dated secret files too; ship-app.mjs mirrors payload-inputs.json.sig. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
44 KiB
Rotation phase 2: the 0.3.6 handover, the deletion of the old folder and key, the fresh repository (5 October 2026)
Internal. Phase 1 (4 October, 19:25 UTC) generated the NEXT intake key and the NEXT downloads token into
~/.config/igneum/log-intake-key.next and ~/.config/igneum/dl-token.next, taught site/api/log.mjs to accept
LOG_INTAKE_KEY_NEXT next to LOG_INTAKE_KEY, and staged a second downloads folder dl/<new token>/ with the
installers of the day. Phase 2 is this file: the 0.3.6 build carries the new values, every installed 0.3.5 is carried
across while the old folder still serves, then the old folder and the old key die, and only then the history is
rewritten into a fresh repository (owner's decision, 5 October 2026; docs/plans/history-rewrite.md, option B).
REMINDER, 7 October 2026: once node tools/logs.mjs --rotation shows Sam's Mac 3a9bf309 on 0.3.8 (moved), run section 8f: the old intake key off the site, the relay and GitHub, the old folder off the downloads host; the old files wait in ~/igneum-dl-old-20261005 until 12 October.
No value is written here. Each is named by its fingerprint, the first 8 hex of sha256 over the trimmed value
(tr -d '[:space:]' < ~/.config/igneum/<file> | shasum -a 256 | cut -c1-8; the app logs the same 8 characters):
| Value | File | Fingerprint | Where it lives today |
|---|---|---|---|
| old intake key | ~/.config/igneum/log-intake-key |
e2005de8 |
every installed app's igneum-app.json (0.3.0 to 0.3.5); the site project's LOG_INTAKE_KEY; 6 tracked files until this branch, 8 commits of the history |
| new intake key | ~/.config/igneum/log-intake-key.next |
477bb0ef |
nowhere yet (the site accepts it once LOG_INTAKE_KEY_NEXT is set) |
| old downloads token | ~/.config/igneum/dl-token |
df66a82c |
every installed app's manifest URL; dl/<old>/ holds every version 0.1.0 to 0.3.5, the jobs file, the CI inputs; the DL_TOKEN repository secret; 1 commit of the history (docs/plans/morning-2026-10-04.md, masked on this branch) |
| new downloads token | ~/.config/igneum/dl-token.next |
ed9c4d2e |
dl/<new>/ with the 0.3.3 installers and the WSL2 zip only, no manifest, no jobs file, no CI inputs |
1. What this branch changes (rotation-2)
| File | Change |
|---|---|
packaging/mac/packaged-config.sh |
no key literal any more. IGNEUM_INTAKE_KEY_FILE and IGNEUM_DL_TOKEN_FILE name the files; each defaults to the .next file when it exists, else the plain file. Prints file names, lengths and fingerprints, never values. --test runs its 23 checks on temporary files |
packaging/windows/make-payload.sh |
sources packaged-config.sh and calls write_packaged_config (it used to sed the key out of that file) |
.github/workflows/windows.yml |
a "packaged configuration" step writes the repository secrets DL_TOKEN, DL_TOKEN_NEXT, LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT to the same files under ~/.config/igneum on the runner, so make-payload.sh picks them exactly as on the Mac; LOG_INTAKE_KEY or LOG_INTAKE_KEY_NEXT is now required (the key no longer comes from the tree) |
app/igneum-app/src/config.rs |
Packaged::with_env_overrides() honours the same two variables on a running engine (a developer run, or a package built with the old values); describe() is the new header line config: intake <url> key <fp> (<source>); manifest <url with the token masked> folder <fp> (<source>); fingerprint8, manifest_url_for_token, token_of_manifest_url, read_secret_file; 6 new unit tests |
app/igneum-app/src/main.rs, engine.rs |
the overrides applied at load; the config: line logged right after the IGNEUM-APP header at every engine start (so every upload carries it) |
tools/ship-app.mjs |
--dl-both: a mirror step copies the version's files and the folder-level files into dl/<dl-token.next>/, the manifest step publishes a second manifest there (--dest, --base-url, carrying the first manifest's override, tuning and min_supported_version, compared field by field), one deploy, verify checks both folders; self-test covers the three helpers |
tools/logs.mjs |
--rotation: every app machine's version and header fingerprints against the .next files, exit 1 while any machine is behind; --self-test |
infra/gpu-bench/upload.sh, proving/windows-wsl2/prove-block.sh, prove-shard.sh, proto-cuda/windows-app/upload-log.bat, proto-cuda/windows-miner/upload-log.bat |
the key literal removed: environment (IGNEUM_LOG_KEY or IGNEUM_INTAKE_KEY, which the app's job runner already sets), IGNEUM_INTAKE_KEY_FILE, or igneum-log-key.txt next to the .bat; the tree carries neither value now (git grep of both reads 0 files) |
tools/repo/fresh-repo.sh |
the history rewrite of docs/plans/history-rewrite.md section 2 as one script with the verification greps and the printed push commands (section 6 below) |
docs/plans/history-rewrite.md |
brought over from testnet-prep unchanged, so this branch carries the plan it executes |
2. The handover, as designed
An installed app reads igneum-app.json next to its engine (macOS Contents/Resources, Windows the install folder):
the manifest URL and the intake key. The OTA path replaces the whole bundle or runs the whole installer, and both
carry a new igneum-app.json, so the values travel with the version. Nothing is cached in the app data folder.
| Step | 0.3.5 on a machine (old folder, old key) | 0.3.6 (new folder, new key) |
|---|---|---|
| hourly check | fetches dl/<old>/igneum-app-latest.json: 0.3.6 is there (published in BOTH folders), signed by the same key |
fetches dl/<new>/igneum-app-latest.json: itself |
| download | the URL inside the old folder's manifest, dl/<old>/Igneum-Miner-0.3.6.dmg or -Setup-0.3.6.exe (byte-identical to the new folder's copy) |
nothing |
| apply | the new bundle or installer brings igneum-app.json with the NEW manifest URL and the NEW key |
|
| after restart | reports to the intake with the new key (LOG_INTAKE_KEY_NEXT accepts it); its header reads key 477bb0ef and folder ed9c4d2e; next check hits the NEW folder |
the same |
| jobs | igneum-jobs.json is read next to the manifest, so the mirror step copies the jobs file and its signature into the new folder; any job published while both folders live goes to both (section 3d) |
The window: every 0.3.5 machine must apply 0.3.6 before the old folder goes. Machines apply in their own minute of the
hour and only when the node is synced, so the window is hours, not minutes. The old folder and the old key stay until
node tools/logs.mjs --rotation reads 0 behind (section 4). Nothing is deleted on a schedule.
3. The publish, exactly
Everything below runs from the main checkout after this branch is merged to master. DLSITE is the downloads folder
(~/.config/igneum/dlsite-dir), OLD and NEW the two tokens read from their files; neither is ever typed.
3a. Before the cut (by hand, once)
# the site must accept both keys (names only are listed; the value is piped from the file)
cd site && npx --yes vercel@latest --global-config ~/.config/igneum/vercel link --scope igneum --project igneum --yes
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env ls --scope igneum # LOG_INTAKE_KEY must be there; is LOG_INTAKE_KEY_NEXT?
tr -d '[:space:]' < ~/.config/igneum/log-intake-key.next | npx --yes vercel@latest --global-config ~/.config/igneum/vercel env add LOG_INTAKE_KEY_NEXT production --scope igneum
cd .. && git push origin master # or any production deploy: the function reads the variable at the next deploy
# confirm: a POST with the NEXT key is accepted (200 with an id), the old one still is too
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key.next)" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"next key accepted"}' https://igneum-six.vercel.app/api/log
# the Windows build on GitHub needs the same files (the runner writes the secrets to ~/.config/igneum; windows.yml)
gh auth switch --user igneum-labs
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum
tr -d '[:space:]' < ~/.config/igneum/log-intake-key.next | gh secret set LOG_INTAKE_KEY_NEXT --repo igneum-network/igneum
tr -d '[:space:]' < ~/.config/igneum/dl-token.next | gh secret set DL_TOKEN_NEXT --repo igneum-network/igneum
gh secret list --repo igneum-network/igneum # DL_TOKEN, DL_TOKEN_NEXT, LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT
# the new folder exists and is empty of a manifest (the mirror step fills it)
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"; NEW="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.next)"
ls "$DLSITE/dl/$NEW"
packaging/mac/packaged-config.sh --test # 23 checks
3b. The cut: one command, both folders
node tools/ship-app.mjs 0.3.6 --node <fork worktree> --notes "<one line>" --dl-both --dry-run # the plan, nothing written
node tools/ship-app.mjs 0.3.6 --node <fork worktree> --notes "<one line>" --dl-both
With --dl-both the steps are: preflight (also: dl-token.next present and different, the folder exists) > bump >
inputs > commit and push (the Windows build starts; its payload step runs packaged-config.sh --test and packages
the .next values because the _NEXT secrets are set) > ci > fetch (installer and zip into the OLD folder) > dmg
(build-dmg.sh packages igneum-app.json from the .next files by default) > copy (DMG into the OLD folder) >
mirror (DMG, installer, zip, igneum-windows-ci.json, igneum-jobs.json and .sig, payload-inputs.{zip,json,sha256},
igneum-prove-wsl2.zip into the NEW folder, sha256-checked) > manifest (section 3c, both) > deploy (one) > verify
(both folders: HEAD and GET of every file, both manifests byte-identical to the local ones and verifying, every
platform URL inside its own folder) > console.
The build itself prints which files it packaged, for example intake key: ~/.config/igneum/log-intake-key.next (31 chars, fingerprint 477bb0ef) and manifest: ~/.config/igneum/dl-token.next (10 chars, fingerprint ed9c4d2e) -> https://dl.igneum.network/dl/<token>/igneum-app-latest.json.
A build that says e2005de8 or df66a82c packaged the old values: stop, the .next files were not found.
3c. The same by hand with packaging/ota/publish-manifest.sh (what the manifest step runs)
publish-manifest.sh writes the OLD folder by default (it reads ~/.config/igneum/dl-token); --dest <folder> and
--base-url <url> aim it at the NEW folder. With --dest it carries consensus.override, tuning and
min_supported_version over from the manifest already in THAT folder, which is none, so the second call must pass
what the first manifest carries. --deploy is refused with --dest; one deploy of the whole folder follows.
Run by hand, publish-manifest.sh prints the manifest it wrote, URLs included, so the terminal shows the token
path (it always has); ship-app.mjs scrubs both tokens from every line, which is the reason to prefer 3b.
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"
OLD="$(tr -d '[:space:]' < ~/.config/igneum/dl-token)"; NEW="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.next)"
V=0.3.6; NOTES="<one line>"
# 1. the OLD folder (default dest and base): the files are there from fetch and copy
packaging/ota/publish-manifest.sh --version $V --notes "$NOTES" --no-deploy \
--mac "$DLSITE/dl/$OLD/Igneum-Miner-$V.dmg" --win "$DLSITE/dl/$OLD/Igneum-Miner-Setup-$V.exe"
# 2. the NEW folder: the same bytes copied in, the same override, tuning and min_supported read from the first manifest
cp "$DLSITE/dl/$OLD/Igneum-Miner-$V.dmg" "$DLSITE/dl/$OLD/Igneum-Miner-Setup-$V.exe" "$DLSITE/dl/$OLD/igneum-windows-app.zip" \
"$DLSITE/dl/$OLD/igneum-windows-ci.json" "$DLSITE/dl/$OLD/igneum-jobs.json" "$DLSITE/dl/$OLD/igneum-jobs.json.sig" \
"$DLSITE/dl/$OLD/payload-inputs.zip" "$DLSITE/dl/$OLD/payload-inputs.json" "$DLSITE/dl/$OLD/payload-inputs.sha256" \
"$DLSITE/dl/$OLD/igneum-prove-wsl2.zip" "$DLSITE/dl/$NEW/"
M="$DLSITE/dl/$OLD/igneum-app-latest.json"
OVERRIDE="$(python3 -c 'import json,sys; o=json.load(open(sys.argv[1])).get("consensus",{}).get("override"); print(json.dumps(o,sort_keys=True,separators=(",",":")) if o else "")' "$M")"
MINSUP="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("min_supported_version",""))' "$M")"
python3 -c 'import json,sys; t=json.load(open(sys.argv[1])).get("tuning"); open(sys.argv[2],"w").write(json.dumps(t)) if t else None' "$M" /tmp/tuning-$V.json
packaging/ota/publish-manifest.sh --version $V --notes "$NOTES" --no-deploy \
--dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW" \
--mac "$DLSITE/dl/$NEW/Igneum-Miner-$V.dmg" --win "$DLSITE/dl/$NEW/Igneum-Miner-Setup-$V.exe" \
${OVERRIDE:+--override "$OVERRIDE"} ${MINSUP:+--min-supported "$MINSUP"} $([ -s /tmp/tuning-$V.json ] && echo --tuning /tmp/tuning-$V.json || echo --no-tuning)
rm -f /tmp/tuning-$V.json
# the two manifests must differ only in published_at and the folder inside the URLs
diff <(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); m.pop("published_at"); print(json.dumps(m,sort_keys=True,indent=1).replace(sys.argv[2],"T"))' "$M" "$OLD") \
<(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); m.pop("published_at"); print(json.dumps(m,sort_keys=True,indent=1).replace(sys.argv[2],"T"))' "$DLSITE/dl/$NEW/igneum-app-latest.json" "$NEW") && echo "same fields"
# 3. one deploy, then both live checks (each: reachable, byte-identical to the local file, signature verifies)
(cd "$DLSITE" && npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes)
packaging/ota/publish-manifest.sh --verify-only
packaging/ota/publish-manifest.sh --verify-only --dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW"
3d. Jobs while both folders live
packaging/ota/publish-jobs.sh writes dl/<old>/igneum-jobs.json by default and takes the same --dest and
--base-url. Until the old folder is deleted, every add or expire is published twice, the second time with
--dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW", then one deploy. A job whose
zip_url names the old folder keeps working until that folder goes; publish new jobs with URLs in the new folder.
4. Verification: every machine's header shows the new intake path
The engine logs two header lines at every start, and the restart after an OTA apply logs them again, so the latest upload of every machine carries them:
IGNEUM-APP version=0.3.6 machine=<id8> platform=<os> node=<igneumd version>
config: intake https://igneum-six.vercel.app/api/log key 477bb0ef (packaged); manifest https://dl.igneum.network/dl/<token>/igneum-app-latest.json folder ed9c4d2e (packaged)
node tools/logs.mjs --rotation # one row per app machine: version, key fp, folder fp, sources, state; exit 1 while any is behind
node tools/logs.mjs <run_id> | grep -E 'IGNEUM-APP|config: intake' # one machine in full
Done means: every row moved (key 477bb0ef, folder ed9c4d2e, version 0.3.6), none OLD, and the unknown rows
(a machine whose last upload predates this header, or a machine that has stopped for good) accounted for by name.
Today the table shows 6 app machines (three win-, three mac-), all 0.3.5 or older, all unknown because 0.3.5
has no config: line. The console's Machines tab (relay/) shows the versions the same way.
Also check, once, that the intake stores an upload under the new key from a real machine (the row's last_received
moves after the restart), and that node tools/logs.mjs lists no new rotation-check rows beyond the one from 3a.
5. The deletion, after section 4 reads 0 behind
In this order, each step checked before the next:
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"
OLD="$(tr -d '[:space:]' < ~/.config/igneum/dl-token)"; NEW="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.next)"
node tools/logs.mjs --rotation || { echo "machines still behind"; false; }
# 1. the old folder: gone from the downloads host (one deploy); the new one still serves
mv "$DLSITE/dl/$OLD" "$HOME/igneum-dl-old-$(date -u +%Y%m%d)" # kept outside the site for a week, then rm -rf
(cd "$DLSITE" && npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes)
curl -s -o /dev/null -w '%{http_code}\n' "https://dl.igneum.network/dl/$OLD/igneum-app-latest.json" # 404
packaging/ota/publish-manifest.sh --verify-only --dest "$DLSITE/dl/$NEW" --base-url "https://dl.igneum.network/dl/$NEW" # still live
# 2. the local files: the NEXT values become the plain ones (every script's default), the old ones kept dated
mv ~/.config/igneum/log-intake-key ~/.config/igneum/log-intake-key.old-$(date -u +%Y%m%d)
mv ~/.config/igneum/log-intake-key.next ~/.config/igneum/log-intake-key
mv ~/.config/igneum/dl-token ~/.config/igneum/dl-token.old-$(date -u +%Y%m%d)
mv ~/.config/igneum/dl-token.next ~/.config/igneum/dl-token
packaging/ota/publish-manifest.sh --verify-only # now reads the new token by default: live, verified
# 3. the intake: the old key dropped (LOG_INTAKE_KEY becomes the new value, LOG_INTAKE_KEY_NEXT removed), redeployed
cd site
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY production --scope igneum --yes
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | npx --yes vercel@latest --global-config ~/.config/igneum/vercel env add LOG_INTAKE_KEY production --scope igneum
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY_NEXT production --scope igneum --yes
cd .. && git push origin master # or a production deploy
# the old key is dead (401), the new one lives (200)
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key.old-$(date -u +%Y%m%d))" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"old key must be refused"}' https://igneum-six.vercel.app/api/log
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key)" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"new key accepted"}' https://igneum-six.vercel.app/api/log
# 4. the GitHub secrets: the plain names carry the new values, the _NEXT names go
tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum
gh secret delete DL_TOKEN_NEXT --repo igneum-network/igneum; gh secret delete LOG_INTAKE_KEY_NEXT --repo igneum-network/igneum
# 5. the app machines keep reporting (a last look, an hour later)
node tools/logs.mjs --rotation
The relay is not involved: since round 4 (X23) it has its own key (~/.config/igneum/relay-key, RELAY_KEY), and
the intake key only reports. The old launcher packages under proto-cuda/windows-app and windows-miner (0.2.0)
carried the old key in upload-log.bat; those machines are the unknown rows of section 4 and upload nothing after
step 3, which is the intent.
6. The fresh repository, after section 5
The old values are dead values once section 5 is done; only then is the rewrite worth running. The owner's two
settings come first: rename the GitHub login igneum-labs to a neutral handle (the numeric noreply id 337424239
stays, so the rewritten author line is <new> <337424239+<new>@users.noreply.github.com>), and remove the second
login from the organisation's owners. Then, from the main checkout with every agent frozen:
gh pr list --repo igneum-network/igneum # must be empty
git worktree list > ~/igneum-worktrees-$(date -u +%Y%m%d).txt
IGNEUM_FILTER_REPO=<dir>/git_filter_repo.py tools/repo/fresh-repo.sh --new-login <new handle> --new-repo igneum-network/<name> \
[--public-claude-md <scrubbed CLAUDE.md>] --work ~/igneum-rewrite
The script clones origin afresh (mirror, no hardlinks), reads the personal identities and the second login from
the history and the four secret values from ~/.config/igneum, writes the rule files 0600 and removes them after
the pass, runs the one git-filter-repo invocation of docs/plans/history-rewrite.md section 2 (drop the four
internal files, replace the secrets and the names, mailmap both personal identities to the login, every offset to
+0000, optionally the public CLAUDE.md in every commit), then demands zero for: secret lines in any blob,
identity lines in any blob, identity lines in commit metadata, stamps not +0000, commits touching the dropped
files, identities other than the login, the old login in any blob (with --new-login). It prints the push commands
and runs none of them: gh repo create igneum-network/<name> --private, git remote add origin in the clone,
git push --mirror origin, then the GitHub secrets (section 3a), the Vercel Git connection moved to the new
repository, the old repository archived, the main checkout re-cloned and every worktree re-created from its
rewritten branch.
Dry run of 5 October 2026 (throwaway mirror clone of the main checkout under the session scratchpad, nothing pushed)
| Count | Before | After |
|---|---|---|
| commits (all refs) | 410 | 353 (57 commits that only touched the four dropped files are gone) |
| refs | 49 | 29 (filter-repo drops the remote-tracking refs of the mirror) |
| author and committer identities | 3 | 1 (igneum-labs <337424239+[removed]>) |
| stamps not +0000 | 660 of 820 | 0 of 706 |
| commits touching the four dropped files | 53 | 0 |
| secret lines in any blob (old key, new key, old token, new token) | 21 | 0 |
| identity lines in any blob (first name outside the login, surname, personal addresses, second login, the other businesses) | 1839 | 0 |
| identity lines in commit metadata | 171 | 0 |
| standing login lines in any blob | 94 | 61 (0 with --new-login after the rename) |
| pass run time | 67 s; 4 min with the clone and the greps |
The report sits next to the clone (<work>/report.txt, with commit-map, 411 lines). The throwaway clone was
removed after the run; nothing left the Mac.
7. The order for the afternoon
- Merge
rotation-2into master (the Windows build on that push packages with the_NEXTsecrets only when they exist: set them first, section 3a, or expect the payload step to fail on the missingLOG_INTAKE_KEY). - Section 3a: the site's
LOG_INTAKE_KEY_NEXT, the four repository secrets, the curl check. - Section 3b:
--dry-run, then the cut with--dl-both. - Section 4 through the afternoon:
node tools/logs.mjs --rotationuntil 0 behind (the slot rule means an hour or two for a synced fleet; a machine that is off waits for its owner). - Section 5: the deletion, in order.
- The owner's two GitHub settings (login rename, one owner); then section 6, the fresh repository, from a frozen tree.
8. Execution, 5 October 2026 (owner: "3 execute"; from 15:25 UTC, branch rotation-3)
The order the owner set differs from section 5 in one place: the old intake key stays until Sam's Mac is on 0.3.8, because that machine uploads with the old key until it updates. So steps 1, 2, 4 and 5 of section 5 ran today in the owner's order (folder, local files, relay, GitHub), and the intake swap (section 5 step 3, plus the same swap on the relay) is written out in 8f for 7 October.
8a. State at the start (node tools/logs.mjs --rotation, 15:25 UTC)
| Machine | Version | Key | Folder | Last upload (UTC) | State |
|---|---|---|---|---|---|
| mac-d937c69d (this Mac) | 0.3.8 | 477bb0ef | ed9c4d2e | 15:24:45 | moved |
| win-1ccfe586 (PC 2) | 0.3.8 | 477bb0ef | ed9c4d2e | 15:24:29 | moved |
| win-ae432dc7 (PC 1) | 0.3.8 | 477bb0ef | ed9c4d2e | 15:25:10 | moved |
| win-37ba0461 | 0.3.7 | 477bb0ef | ed9c4d2e | 13:52:20 | moved |
| mac-3a9bf309 (Sam's Mac, asleep) | 0.3.5 | - | - | 09:43:45 | unknown: reads the OLD folder and uploads with the OLD key |
| mac-MacBook-Pro | ? | - | - | 4 Oct 01:46:13 | unknown: a pre-header upload, stopped for good; accounted for |
Only Sam's Mac is behind. Its 0.3.5 checks the OLD manifest once an hour when its node is synced, downloads the file
named inside that manifest, and the new bundle carries the NEW manifest URL and the NEW key (section 2). Its job
runner polls dl/<old>/igneum-jobs.json every 10 minutes; a missing file is a quiet error in 0.3.5
(jobrun.rs:338, "no jobs file"), so the strip below costs it nothing.
8b. The OLD folder, stripped to what 0.3.5 needs (section 5 step 1, narrowed)
Kept, 4 files: igneum-app-latest.json (861 bytes, version 0.3.8, published 13:33:20Z, both platform URLs inside it
name the OLD folder), igneum-app-latest.json.sig, Igneum-Miner-0.3.8.dmg (41,247,419) and
Igneum-Miner-Setup-0.3.8.exe (19,794,320).
Before the strip, one copy into the NEW folder: payload-inputs.json.sig (129 bytes, byte-identical). The NEW folder
had never received it: tools/ship-app.mjs FOLDER_FILES carried the zip, the json and the sha256 but not the
signature, and .github/workflows/windows.yml fetches all four from the DL_TOKEN folder, which is the NEW folder
from 8e on. Without the copy the next Windows build would have failed at the inputs step. The list is fixed on this
branch (8h).
Removed: 56 files, 947,635,881 bytes, moved (not deleted) to ~/igneum-dl-old-20261005 (mode 0700), to be removed
on 12 October (8f step 6):
| File | Bytes | Modified (BST) | sha256 (first 12) |
|---|---|---|---|
Igneum-Miner-0.1.0.dmg |
16994336 | 2026-10-03T23:28 | 69904f6191e0 |
Igneum-Miner-0.2.0.dmg |
19924804 | 2026-10-04T08:57 | 2cb1c1d657bc |
Igneum-Miner-0.3.0.dmg |
20320453 | 2026-10-04T12:46 | 670d26b49904 |
Igneum-Miner-0.3.1.dmg |
20320855 | 2026-10-04T14:27 | 4b29d91cdd05 |
Igneum-Miner-0.3.2.dmg |
20519491 | 2026-10-04T15:17 | 26fdc1e854ea |
Igneum-Miner-0.3.3.dmg |
39775332 | 2026-10-04T17:56 | 3177bced3698 |
Igneum-Miner-0.3.4.dmg |
39798913 | 2026-10-04T21:20 | 1b346811b807 |
Igneum-Miner-0.3.5.dmg |
40027384 | 2026-10-05T07:39 | 2dad2b2615a6 |
Igneum-Miner-0.3.6.dmg |
40156607 | 2026-10-05T10:34 | fddf3580353d |
Igneum-Miner-0.3.7.dmg |
40156739 | 2026-10-05T11:16 | 8ee96b3b054f |
Igneum-Miner-Setup-0.3.0.exe |
44005195 | 2026-10-04T12:46 | 42b3f167ba25 |
Igneum-Miner-Setup-0.3.1.exe |
44013598 | 2026-10-04T14:32 | abc5b6226582 |
Igneum-Miner-Setup-0.3.2.exe |
44138229 | 2026-10-04T15:17 | f5e0763ff126 |
Igneum-Miner-Setup-0.3.3.exe |
44275245 | 2026-10-04T16:10 | 739f7e8af968 |
Igneum-Miner-Setup-0.3.4.exe |
44304304 | 2026-10-04T21:14 | 756ee2c0af7f |
Igneum-Miner-Setup-0.3.5.exe |
44447899 | 2026-10-05T07:38 | 0184abecaf99 |
Igneum-Miner-Setup-0.3.6.exe |
19536899 | 2026-10-05T10:34 | ca0fb9197bee |
Igneum-Miner-Setup-0.3.7.exe |
19784297 | 2026-10-05T11:16 | 2bacba64628b |
Igneum-Wallet-0.1.1.dmg |
19565360 | 2026-10-05T09:24 | 02446a480dae |
Igneum-Wallet-0.1.2.dmg |
19582462 | 2026-10-05T10:13 | 4ddfd7a07ac1 |
Igneum-Wallet-0.1.3.dmg |
19598469 | 2026-10-05T14:21 | d5b7945e4fe8 |
build-inputs-t035.json |
705 | 2026-10-05T10:17 | da48d4fbb7c4 |
build-inputs-t035.sha256 |
65 | 2026-10-05T10:17 | caa267821f17 |
build-inputs-t035.zip |
7223133 | 2026-10-05T10:17 | d1dd841d9872 |
build-inputs-t036.json |
701 | 2026-10-05T10:17 | a0cdfe1a83c8 |
build-inputs-t036.sha256 |
65 | 2026-10-05T10:17 | 37794f58f636 |
build-inputs-t036.zip |
7244256 | 2026-10-05T10:17 | c01a7525a903 |
build-inputs-tests.json |
1149 | 2026-10-05T10:04 | 0aa92c6a10f8 |
build-inputs-tests.sha256 |
65 | 2026-10-05T10:04 | 216760a99a61 |
build-inputs-tests.zip |
8206608 | 2026-10-05T10:04 | f60713b133ec |
build-inputs.json |
1046 | 2026-10-05T10:12 | ed3e06ef5fdf |
build-inputs.sha256 |
65 | 2026-10-05T10:12 | 0c0c2a0c186c |
build-inputs.zip |
8206958 | 2026-10-05T10:12 | 7c63df808331 |
igneum-devnet-mac.dmg |
17750579 | 2026-10-03T22:55 | c5b49d3c0097 |
igneum-jobs.json |
64416 | 2026-10-05T15:04 | 6812e147601f |
igneum-jobs.json.sig |
129 | 2026-10-05T15:04 | 3c3fbbeb258b |
igneum-mine-test-v2.zip |
4442068 | 2026-10-03T22:52 | 16abd0ff2a42 |
igneum-mine-test-v3.zip |
4442367 | 2026-10-03T22:52 | 646d6d4b659c |
igneum-mine-test.zip |
4431923 | 2026-10-03T22:52 | ab1951f1450d |
igneum-node-windows-v4.zip |
32973919 | 2026-10-04T08:57 | 8fbdc646596e |
igneum-node-windows.zip |
29454819 | 2026-10-03T22:52 | beadad43d1f0 |
igneum-prove-wsl2-038.zip |
1425452 | 2026-10-05T14:28 | 98c246146e89 |
igneum-prove-wsl2.zip |
295176 | 2026-10-05T11:48 | 75e3a96fed84 |
igneum-wallet-latest.json |
473 | 2026-10-05T14:21 | dc9bf8ac2bf5 |
igneum-wallet-latest.json.sig |
129 | 2026-10-05T14:21 | 1568dbdc33d0 |
igneum-windows-app.zip |
27591281 | 2026-10-05T14:33 | 8f08a3040ac7 |
igneum-windows-ci.json |
199 | 2026-10-05T14:33 | f8e099c8c2c3 |
igneum-windows-v4.zip |
64286803 | 2026-10-04T12:05 | a936c0f80715 |
igneum-windows.zip |
22885438 | 2026-10-03T22:52 | 862d61098c4b |
igneum-worker-cuda.exe |
1121792 | 2026-10-04T11:12 | 3f3f8337d53b |
mingw-runtime-gcc13.zip |
8338215 | 2026-10-05T10:52 | 7f030f253571 |
mingw-runtime-x64.zip |
9327832 | 2026-10-05T10:45 | b6671e811559 |
payload-inputs.json |
995 | 2026-10-05T14:25 | 403d0108b1b8 |
payload-inputs.json.sig |
129 | 2026-10-05T14:25 | a667d898e29e |
payload-inputs.sha256 |
65 | 2026-10-05T14:25 | e627981e8b09 |
payload-inputs.zip |
26669995 | 2026-10-05T14:25 | b587ed19fac4 |
The deploy: one vercel deploy --prod of the downloads folder (igneum-dl), 15:29 UTC. Verified straight after:
| Check | Result |
|---|---|
packaging/ota/publish-manifest.sh --verify-only (OLD folder, the default token at that moment) |
version 0.3.8, byte-identical, signature OK, try 1 of 12 |
the same with --dest "$DLSITE/dl/$NEW" --base-url https://dl.igneum.network/dl/$NEW |
version 0.3.8, byte-identical, signature OK, try 1 of 12 |
OLD folder, removed: igneum-jobs.json, Igneum-Miner-0.3.7.dmg, build-inputs.zip, igneum-wallet-latest.json, payload-inputs.zip |
404, 404, 404, 404, 404 |
| OLD folder, kept: the manifest, its signature, the 0.3.8 DMG, the 0.3.8 installer | 200, 200, 200, 200 |
NEW folder: payload-inputs.json.sig, igneum-jobs.json, igneum-jobs.json.sig, payload-inputs.zip |
200, 200, 200, 200 |
Jobs whose zip_url names the OLD folder (the build jobs of the morning, fetch-prove-038, rollback-035-pcs) have
all run on their machines; a machine never re-runs a job it has finished, so nothing waits on those URLs. The wallet
manifest and DMGs left the OLD folder with everything else; the NEW folder carries igneum-wallet-latest.json and
Igneum-Wallet-0.1.3.dmg (mirrored 14:22 UTC). The wallet publisher is not on master (the wallet branch): if it reads
dl-token as every other publisher does, it writes the NEW folder from now on; confirm at the next wallet publish.
8c. The local files (section 5 step 2, as written)
mv ~/.config/igneum/log-intake-key ~/.config/igneum/log-intake-key.old-2026-10-05
mv ~/.config/igneum/log-intake-key.next ~/.config/igneum/log-intake-key
mv ~/.config/igneum/dl-token ~/.config/igneum/dl-token.old-2026-10-05
mv ~/.config/igneum/dl-token.next ~/.config/igneum/dl-token
| File | Mode | Fingerprint |
|---|---|---|
log-intake-key |
0600 | 477bb0ef |
log-intake-key.old-2026-10-05 |
0600 | e2005de8 |
dl-token |
0600 | ed9c4d2e |
dl-token.old-2026-10-05 |
0600 | df66a82c |
No .next file is left. Every script reads the NEW folder and the NEW key by default from here: packaging/mac/packaged-config.sh --test
all checks passed; packaging/ota/publish-manifest.sh --verify-only (now the NEW folder) 0.3.8, byte-identical,
signature OK; publish-jobs.sh has no next folder to mirror to, and the job another agent published at 15:32 UTC
landed in the NEW folder only, as intended. Two tools read the renamed files wrongly and are fixed on this branch (8h):
tools/logs.mjs --rotation printed the old fingerprints as 477bb0ef/ed9c4d2e (it took "old" from the plain files),
and tools/repo/fresh-repo.sh built its secret rules from the four plain names only, so the rewrite would have left
the OLD key and the OLD token in the history. The dated files stay until the fresh repository is pushed (8g step 10).
8d. The intake and the relay (section 5 step 3, split)
The site project igneum is untouched today: LOG_INTAKE_KEY (old) and LOG_INTAKE_KEY_NEXT (new) both stay, and
a POST to /api/log answered 200 with the new key and 200 with the old key (baseline for 8f; label
rotation-check). Sam's Mac keeps uploading with the old key until it has applied 0.3.8.
The relay project igneum-relay (relay/lib/relay.mjs authed() accepts LOG_INTAKE_KEY and LOG_INTAKE_KEY_NEXT
since this morning): LOG_INTAKE_KEY_NEXT added (production, piped from ~/.config/igneum/log-intake-key), and
DL_TOKEN moved to the NEW token (relay/api/console.mjs reads the jobs file, the manifest and the CI record from
that folder, and the OLD folder no longer carries them). Deployed from the main checkout's relay/ on master 23d11d5
with the command of relay/README.md (vercel deploy --prod --yes --scope igneum), 15:31 UTC.
POST https://relay.igneum.network/api/relay?fn=upload with header x-igneum-key |
Answer |
|---|---|
| the NEW key | ok: true, api_version: 11, 200 |
| the OLD key | ok: true, api_version: 11, 200 |
| a wrong key | 401 |
Relay env after: DL_TOKEN, LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT, RELAY_KEY, RELAY_TOKEN, DATABASE_URL,
BLOB_READ_WRITE_TOKEN. node tools/console.mjs jobs lists the jobs through the NEW folder.
8e. The GitHub secrets (section 5 step 4, the token half)
gh auth status: igneum-labs active. DL_TOKEN set 15:32:05Z from ~/.config/igneum/dl-token (the NEW token);
DL_TOKEN_NEXT deleted. gh secret list: DL_TOKEN (15:32:05Z), LOG_INTAKE_KEY (08:36:35Z, old), LOG_INTAKE_KEY_NEXT
(08:36:36Z, new). On the runner (windows.yml): the payload inputs and the manifest folder come from DL_TOKEN, the
NEW folder (which now carries payload-inputs.json.sig, 8b); packaged-config.sh packages the .next key, the new
one, while LOG_INTAKE_KEY_NEXT exists, and the plain LOG_INTAKE_KEY once 8f has run.
8f. Deferred to 7 October: the old key and the old folder, exact commands
Condition, checked first and never skipped: Sam's Mac reports 0.3.8 with the new fingerprints. If it is still asleep on 7 October, wait; nothing below runs on a schedule.
cd /Users/joshm/Projects/igneum && git checkout master && git pull
node tools/logs.mjs --rotation | grep 3a9bf309 # must read: 0.3.8 477bb0ef ed9c4d2e ... moved
gh auth status # igneum-labs active
# 1. the site: LOG_INTAKE_KEY becomes the new value, LOG_INTAKE_KEY_NEXT goes, then a production deploy
cd site
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY production --scope igneum --yes
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | npx --yes vercel@latest --global-config ~/.config/igneum/vercel env add LOG_INTAKE_KEY production --scope igneum
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY_NEXT production --scope igneum --yes
cd ..
git push origin master # the GitHub integration deploys; if master has nothing new: the hand deploy of packaging/README-ship.md
# the old key is dead (401), the new one lives (200)
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key.old-2026-10-05)" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"old key must be refused"}' https://igneum-six.vercel.app/api/log
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key)" -H 'Content-Type: application/json' -d '{"label":"rotation-check","machine":"mac","run_id":"rotation-check","lines":"new key accepted"}' https://igneum-six.vercel.app/api/log
# 2. the relay: the same swap, then its own deploy (relay/README.md)
cd relay
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY production --scope igneum --yes
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | npx --yes vercel@latest --global-config ~/.config/igneum/vercel env add LOG_INTAKE_KEY production --scope igneum
npx --yes vercel@latest --global-config ~/.config/igneum/vercel env rm LOG_INTAKE_KEY_NEXT production --scope igneum --yes
npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes --scope igneum
cd ..
# old key 401, new key 200 (the answer carries a Blob client token: print the status only)
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key.old-2026-10-05)" -H 'Content-Type: application/json' -d '{"name":"rotation-check.txt","size":10}' 'https://relay.igneum.network/api/relay?fn=upload'
curl -s -o /dev/null -w '%{http_code}\n' -X POST -H "x-igneum-key: $(tr -d '[:space:]' < ~/.config/igneum/log-intake-key)" -H 'Content-Type: application/json' -d '{"name":"rotation-check.txt","size":10}' 'https://relay.igneum.network/api/relay?fn=upload'
# 3. GitHub: LOG_INTAKE_KEY carries the new value, LOG_INTAKE_KEY_NEXT goes
tr -d '[:space:]' < ~/.config/igneum/log-intake-key | gh secret set LOG_INTAKE_KEY --repo igneum-network/igneum
gh secret delete LOG_INTAKE_KEY_NEXT --repo igneum-network/igneum
gh secret list --repo igneum-network/igneum # DL_TOKEN, LOG_INTAKE_KEY
# 4. the OLD folder: its last 4 files join the holding folder, one deploy, 404
DLSITE="$(tr -d '[:space:]' < ~/.config/igneum/dlsite-dir)"; OLD="$(tr -d '[:space:]' < ~/.config/igneum/dl-token.old-2026-10-05)"
mkdir -p ~/igneum-dl-old-20261005/last-manifest && mv "$DLSITE/dl/$OLD"/* ~/igneum-dl-old-20261005/last-manifest/ && rmdir "$DLSITE/dl/$OLD"
ls "$DLSITE/dl" | wc -l # 1
(cd "$DLSITE" && npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes)
curl -s -o /dev/null -w '%{http_code}\n' "https://dl.igneum.network/dl/$OLD/igneum-app-latest.json" # 404
packaging/ota/publish-manifest.sh --verify-only # the NEW folder: live, byte-identical, signature OK
# 5. an hour later: every row moved (mac-MacBook-Pro stays unknown: stopped 4 October)
node tools/logs.mjs --rotation
# 6. on 12 October, and only after 8g step 10 has pushed the fresh repository (the rewrite reads the dated files)
rm -rf ~/igneum-dl-old-20261005
rm -P ~/.config/igneum/log-intake-key.old-2026-10-05 ~/.config/igneum/dl-token.old-2026-10-05
After step 3 the 0.2.0 launcher machines (proto-cuda/windows-app, windows-miner, the old key in upload-log.bat)
upload nothing, which is the intent of section 5.
8g. The owner's checklist: the login rename and the fresh repository (about twenty minutes)
Before: 8f done (the old values are dead values), gh auth status igneum-labs active, and
~/.config/igneum/pytools/git_filter_repo.py present (copied today from the dry run's download, version a40bce5;
if missing: python3 -m pip install --target ~/.config/igneum/pytools git-filter-repo). Browser work in the
"[user] (igneum.network)" Chrome profile. <new> is the handle: letters, digits, hyphens, no name.
- (1 min) Pick
<new>and the repository name.igneum-coreis the script's default; the commands below use it. - (3 min) GitHub, signed in as igneum-labs: Settings > Account > Change username:
igneum-labsto<new>. The noreply id 337424239 stays, so the commit address becomes337424239+<new>@users.noreply.github.com. Then the organisation igneum-network > People: [second-owner-login] leaves the owners (remove from the organisation). Check the profile and the organisation: no name, no location, no personal avatar, 2FA on, the public members list empty. Nothing on this Mac breaks: the token survives a rename andgh auth token --user igneum-labsreads it by the stored name; the tidy-up is step 10. - (2 min) Freeze: every agent commits and pushes its branch and stops; no ship, no merge, no job publish that
commits. Then, from the main checkout:
gh pr list --repo igneum-network/igneum(must be empty) andgit -C ~/Projects/igneum worktree list > ~/igneum-worktrees-$(date -u +%Y%m%d).txt(42 worktrees today). The freeze holds until step 10 is done. - (4 min) The pass, from the main checkout on master, nothing pushed by the script:
Addcd ~/Projects/igneum && git checkout master && git pull IGNEUM_FILTER_REPO=~/.config/igneum/pytools/git_filter_repo.py tools/repo/fresh-repo.sh --new-login <new> --new-repo igneum-network/igneum-core --work ~/igneum-rewrite--public-claude-md <file>when the scrubbedCLAUDE.mdofdocs/fud-fixes.mdsection 5 step 2 exists; without it the privateCLAUDE.mdstays in every commit and the repository must stay private. Expected, against the dry run of section 6: about 353 commits, 1 identity (<new>), 0 stamps off+0000, 0 commits touching the four dropped files, 0 secret lines (the rules now carry 4 values: 2 current, 2 dated), 0 identity lines, 0 lines of the old login, thenclean.and the printed push commands. OnNOT CLEAN: stop, keep~/igneum-rewrite/report.txt, ask. - (2 min) The push, the script's printed lines:
On GitHub: default branch master, the commit count of step 4, authorgh repo create igneum-network/igneum-core --private --description 'Igneum: the GPU-mined zkEVM L1' --disable-wiki cd ~/igneum-rewrite/clone && git remote add origin https://github.com/igneum-network/igneum-core.git && git push --mirror origin<new>on the newest and the oldest commit. - (1 min) The two secrets on the new repository (two after 8f):
tr -d '[:space:]' < ~/.config/igneum/dl-token | gh secret set DL_TOKEN --repo igneum-network/igneum-core, the same forlog-intake-keyintoLOG_INTAKE_KEY, thengh secret list --repo igneum-network/igneum-core. - (3 min) Vercel, team igneum, project igneum > Settings > Git: disconnect
igneum-network/igneum, connectigneum-network/igneum-core, production branch master. Then one push to master from the new checkout (step 10) triggers the first deploy;curl -sI https://igneum.network | head -1reads 200. The relay and the downloads folder deploy by CLI and have no Git link: nothing to re-link. - (1 min) Archive
igneum-network/igneum(Settings > Archive this repository). Private, never deleted the same day. - (3 min) Re-clone and re-identify:
The privatecd ~/Projects && mv igneum igneum-old-history && git clone https://github.com/igneum-network/igneum-core.git igneum cd igneum && git config user.name <new> && git config user.email 337424239+<new>@users.noreply.github.com gh auth logout --user igneum-labs && gh auth login --web --hostname github.com # as <new> git config credential.https://github.com.helper '' && git config --add credential.https://github.com.helper '!f() { echo username=<new>; echo "password=$(gh auth token --user <new> 2>/dev/null)"; }; f' mv ../igneum-old-history/vendor ./vendor # gitignored: the fork worktrees and their targets git commit --allow-empty -m "fresh repository: first push" && git push origin master # the deploy of step 7CLAUDE.mdnames the login and the repository: update both lines in the same commit (or the scrubbed file of step 4). - (0 min, each agent) Unfreeze: every agent removes its old worktree directory and re-creates it from the new
checkout,
git -C ~/Projects/igneum worktree add ../igneum-wt-<name> <branch>; never a merge of an old-id branch into a new one.TZ=UTCin every shell that commits.~/igneum-old-historyand the dated secret files go on 12 October (8f step 6).
8h. What this branch changes (rotation-3)
| File | Change |
|---|---|
tools/logs.mjs |
--rotation: once no .next file exists, the "old" fingerprints come from the newest log-intake-key.old-<date> and dl-token.old-<date> instead of the plain files (which are the new values after the rename); the summary line says which. --self-test passes |
tools/repo/fresh-repo.sh |
the secret rules take every log-intake-key.old-* and dl-token.old-* file next to the four names, so the rewrite scrubs the old values after the rename; the count line no longer says "of 4" |
tools/ship-app.mjs |
FOLDER_FILES carries payload-inputs.json.sig (the mirror step had left the signature behind; windows.yml fetches it). --self-test passes |
| this file | section 8 |