23 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).
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.