igneum/relay
igneum-labs 690e7dc12a Merge commit '1065b81' into release-0.3.17
# Conflicts:
#	.github/workflows/ci.yml
2026-10-06 23:10:56 +00:00
..
api Merge commit '1065b81' into release-0.3.17 2026-10-06 23:10:56 +00:00
clients relay: three auth tiers, signed run tasks, machine secrets, retention; clients on headers; TZ=UTC and curl -K checks (X23 X24 X25 X26 X27 X28 X29 G13 G14) 2026-10-05 18:46:13 +00:00
lib Merge commit '1065b81' into release-0.3.17 2026-10-06 23:10:56 +00:00
playbooks Merge commit '1065b81' into release-0.3.17 2026-10-06 23:10:56 +00:00
test Merge commit '1065b81' into release-0.3.17 2026-10-06 23:10:56 +00:00
.gitignore Relay: text, files and runnable tasks between the Mac, the PCs and the phone (relay.igneum.network) 2026-10-04 10:04:30 +00:00
apple-touch-icon.png Brand: one master mark (black square, no circle) for every icon, favicon and profile picture 2026-10-04 11:39:55 +00:00
favicon-32.png Brand: one master mark (black square, no circle) for every icon, favicon and profile picture 2026-10-04 11:39:55 +00:00
favicon.ico Brand: one master mark (black square, no circle) for every icon, favicon and profile picture 2026-10-04 11:39:55 +00:00
index.html Brand: one master mark (black square, no circle) for every icon, favicon and profile picture 2026-10-04 11:39:55 +00:00
package-lock.json Relay: text, files and runnable tasks between the Mac, the PCs and the phone (relay.igneum.network) 2026-10-04 10:04:30 +00:00
package.json Relay: text, files and runnable tasks between the Mac, the PCs and the phone (relay.igneum.network) 2026-10-04 10:04:30 +00:00
README.md relay: three auth tiers, signed run tasks, machine secrets, retention; clients on headers; TZ=UTC and curl -K checks (X23 X24 X25 X26 X27 X28 X29 G13 G14) 2026-10-05 18:46:13 +00:00
robots.txt Relay: text, files and runnable tasks between the Mac, the PCs and the phone (relay.igneum.network) 2026-10-04 10:04:30 +00:00
ui.html app: GPU hot-plug (re-detection every minute, WM_DEVICECHANGE on Windows), faulty cards listed with the problem code, integrated GPUs off by default, the card list in the console 2026-10-05 17:45:05 +00:00
vercel.json relay: /wake long-poll for the apps' remote jobs (public GET held 45 s, authenticated POST of the stamp) 2026-10-05 08:22:02 +00:00

Igneum relay and console

Text, files and tasks between the project lead's devices without Gmail: the Mac, PC1, PC2 and the phone post to one feed and read from it. Vercel project igneum-relay, served at https://relay.igneum.network. Built 4 October 2026. Since the evening of 4 October 2026 the same private page is the Igneum console (ui.html): seven tabs, phone first, refreshed every 15 s. The relay feed and drop box are its last tab.

Scope since 4 October 2026 (afternoon): the relay stays for the Mac and for humans (notes, files, tasks for a person or a Claude session on a PC). Commands and files for the PCs themselves go over the line to the Igneum Miner app instead: signed jobs published next to the update manifest (packaging/ota/publish-jobs.sh, read back with tools/jobs.mjs, documented in packaging/ota/README.md, "Remote jobs"). The app jobs replace the PC agent (igneum-agent.bat): PC 2 has no Claude session and nobody at the keyboard, and both PCs report the same hostname (DESKTOP-KMCV30N), which the relay's registration cannot tell apart; the app's per-install machine id can. The playbooks under relay/playbooks/ stay as the relay form of the same runs (shard-test.ps1 is the model for a run job) and are parse-checked by windows.yml.

The console

Tab Shows Source
Machines one card per machine: app and node version, height (daa), synced, hash rate, accepted blocks, peers, faults, power and temperatures, last seen; red after 3 min without an upload, grey "stopped (quit|update)" when the app's last upload ends on its own quit lines; a worker card whose STATUS line is over 180 s old is marked stale and left out of the machine total; the OTA state is the newest update line the app logged (downloading, downloaded, staged, installing, updated, failed, current); worker labels nvidia, amd, mac, metal, opencl, other and intel Neon miner_logs (the log intake in site/api/log.mjs): newest upload per label, the last 20 KB parsed server side (miner STATUS lines, node log, the app's stability: lines)
Jobs the signed jobs file with per-machine status (queued, running, done + exit code) and the result line; tap a run for the full upload igneum-jobs.json on the downloads host (fetched server side with DL_TOKEN), results from miner_logs rows whose run_id is job-<id>-<machine>
Builds the OTA manifest (version, notes, platforms, sizes), the last CI fetch, build events, the downloads folder listing igneum-app-latest.json and igneum-windows-ci.json on the downloads host; console_items kind build (posted by packaging/windows/fetch-ci-artifacts.sh and packaging/ota/publish-manifest.sh) and key dl (tools/console.mjs sync-dl)
Chain blocks, identities, hash estimate, difficulty, last lock, finality state, peers, blocks per minute sparkline, events, Hetzner results https://igneum.network/api/live fetched server side; console_items key hetzner (sync-hetzner)
Work log what the agents and the main session post, merged with every relay item, newest first console_items kinds log, build, note; the relay feed
Results the bench log entries (heading + first paragraph), newest first; the FUD ledger counts by status console_items kind bench and key ledger, written by tools/console.mjs sync-bench from docs/bench-log.md and docs/fud-ledger.md
Relay the feed and the drop box, unchanged relay_items, relay_machines

The console function is api/console.mjs, reached through the rewrite /r/<token>/c/<fn>. Every GET answer is cached 10 s in the function instance. No secret reaches the client: the token in the path is the only auth, and DL_TOKEN (the downloads folder) lives in the project env and is used only server side. No GitHub token anywhere: build events come from the Mac-side scripts.

Mac: node tools/console.mjs post --kind log --title "..." --body "..." writes one work-log item (kinds log, build, note); log, machines, chain, jobs, builds, results print the tabs; sync-bench, sync-dl, sync-hetzner or sync push the file-derived data; url prints the link.

The app log (label win-<id8> or mac-<id8>) reaches the intake since b8b349a (4 Oct 2026, 0.3.3); apps before that show app ?. The parsers (labels, miner tail, app tail, the stale mark) live in relay/lib/parse.mjs with no dependencies, and relay/lib/auth.mjs holds the constant-time secret compare; node --test relay/test/parse.test.mjs relay/test/auth.test.mjs runs their tests, and CI runs them in the site job.

The secrets, since 5 October 2026 (night): three tiers, headers, no token in a URL

Secret Where it lives Sent as May
the console token (20 base32 characters) ~/.config/igneum/relay-token, RELAY_TOKEN on the project the x-relay-token header from every tool and client; the URL path /r/<token>/ only for the phone's page (ui.html) and the calls that page makes (/r/<token>/api/<fn>, /r/<token>/c/<fn>, /r/<token>/wake) everything, except that a run task also needs the run signature below
the relay key (48 characters, the relay's own since 4 October 2026) ~/.config/igneum/relay-key, RELAY_KEY x-igneum-key read the feed, items, files, inbox, machines; post notes, files, results; register; ack; done. Never task, run, name, role, secret, delete
the log-intake key (inside every shipped package) LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT x-igneum-key only upload and a drop of kind file or text: the PC apps' build-job outputs (jobrun.rs). No reads, no results, nothing else. RELAY_INTAKE_COMPAT=0 on the project closes this tier once the apps carry a relay key of their own
the run key (Ed25519, node tools/relay.mjs keygen) ~/.config/igneum/relay-run-key (seed, 0600) and .pub; the public half as RELAY_RUN_PUB on the project flags.sig on every run task, over the canonical text in relay/lib/guard.mjs (runCanon: machine, nonce, body sha256, the elevated, reboot_continue and reboot flags) without a verifying signature the relay answers 401 to a run; without RELAY_RUN_PUB every run is refused
a machine secret per PC (64 hex, node tools/relay.mjs secret PC1) ~/.config/igneum/relay-machines/<name> on the Mac; machine-secret.txt beside the agent on that PC (from make-clients.sh --machine); its sha256 on the relay (relay_machines.secret_hash) x-machine-secret on register, result, done; flags.mac on every run (an HMAC-SHA256 tag the agent verifies before it executes, because Windows PowerShell 5.1 has no Ed25519) a result's from is the machine the secret proves (a mismatch is 403); a bound hostname cannot register without it; the agent runs nothing whose tag does not verify, and never the same nonce twice

Review round 4 (X23) found one tier with every power: the intake key inside every package queued PowerShell on PC 1 as administrator. Now the token alone cannot queue a run either: the Mac signs it with the run key (checked by the relay) and tags it with the target's secret (checked by the agent). Compare is constant time (lib/auth.mjs). Blob file URLs carry a random segment and a random suffix; they are not listed anywhere. 120 calls a minute per IP, 10 failed authentications a minute per IP, then 429.

Rotation (what each secret's change breaks, and the order)

Rotate How What stops, until Order
the console token new value in ~/.config/igneum/relay-token and RELAY_TOKEN; a new zip per PC (make-clients.sh --machine) carried by hand; the phone gets the new link every PC client and the phone page, until the new zip and link are in place; the Mac tools at once (they read the file) any time; the old zip on a PC is dead the moment the project env changes
the relay key new value in relay-key and RELAY_KEY; new zips the PC clients' reads and reports until the new zip; tools/build-job.mjs falls back to the token with the token, same zip
the intake key LOG_INTAKE_KEY_NEXT on the relay AND the site intake first, repackage every app with the next key (packaged-config.sh), publish; then move _NEXT to LOG_INTAKE_KEY; then RELAY_INTAKE_COMPAT=0 once no shipped app uploads build outputs with it build-job uploads from every app that still carries the old key; log uploads from every shipped package until it updates the long one: apps update over days, so both values are accepted during the window (docs/plans/rotation-phase-2.md)
the run key node tools/relay.mjs keygen after moving the old relay-run-key aside; RELAY_RUN_PUB on the project every run queued with the old key is refused at the relay; nothing on a PC changes (the PCs hold no run key) any time, in one step
a machine secret node tools/relay.mjs secret PC1 --rotate (rebinds the sha256), make-clients.sh --machine PC1, carry the zip that PC's results and registration until the new zip is there; queued run tasks tagged with the old secret are refused by the agent per machine, any time

What the 5 October rotation already did: the relay token (4 October), the intake key and the dl token (.old-2026-10-05 beside the live files). What is still the project lead's: the hosted igneum-relay-clients.zip off the downloads host (it holds the old token and key; dead values now, but the file is the shape X23 names), RELAY_RUN_PUB on the project after keygen, one secret per PC and the zips carried by hand.

What is stored where

Thing Where Limit
Items (text, title, who, kind, flags, read and done marks) Neon table relay_items (database igneum) body 1 MB
Machines (name, hostname, role, GPU and WSL facts, last seen) Neon table relay_machines
Files Vercel Blob store igneum-relay (public URLs with random path and suffix, London) 50 MB per file through a client token; 4 MB when pushed through the function
The token and keys ~/.config/igneum/relay-token, relay-key, relay-run-key, relay-machines/<name>; project env (RELAY_TOKEN, RELAY_KEY, RELAY_RUN_PUB, LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT) never in the repo
Retention rows older than 30 days are deleted with their blobs, checked on a feed read at most every 10 minutes per instance; delete removes the blob with the row (X26) 30 days

Kinds: text (a note), file, task (for a person or a Claude session on a PC), run (a script the agent executes; signed and tagged, see above), result (what a task produced, linked by task_id; from is the machine the secret proves, flags.unbound marks one from a machine with no secret yet). Roles: miner, prover, bench, mac, phone.

API (/api/relay?fn=<name> with the headers above; /r/<token>/api/<fn> from the phone's page only)

Call Does
GET feed?since=&before=&machine=&limit= items newest first (50 by default, 100 at most) plus every machine with its unread count and whether it is bound
GET item?id= one item with its full body
GET file?id=[&download=1] 302 to the file
`GET inbox?machine=PC1&kind=run task`
POST inbox {machine, kind, ack:true} the same list, marked read (the agents use this)
GET machines names, roles, hostnames, last seen, bound
POST drop JSON {from,to,kind,title,body,file_name,file_url,size,task_id,flags}; or raw bytes with Content-Type: application/octet-stream and x-file-name (4 MB cap). A result needs x-machine-secret when its machine is bound
POST task same fields; kind task or run; run needs one named machine and flags {elevated, reboot_continue, reboot, nonce, sig, mac}; tools/relay.mjs run fills the last three in. 401 without a verifying sig, 409 on a reused nonce
POST upload {name,size} returns a one-hour Blob client token and put_url; PUT the bytes there, then drop with the returned url
POST ack {ids} POST done {id,exit_code} POST delete {id} marks; delete takes the blob with the row (token only)
POST register {hostname,info} a machine checks in; with x-machine-secret the secret names it whatever the hostname says (both PCs report DESKTOP-KMCV30N); info.user and info.dir are dropped
POST secret {name, secret_hash} binds a machine to the sha256 of its secret (token only; tools/relay.mjs secret does it)
POST name {hostname,name} POST role {name,role} naming and roles, from the Mac (token only)

Tests: node --test relay/test/guard.test.mjs relay/test/handler.test.mjs relay/test/clients.test.mjs (the rules, the handler against a fake database and fake blobs, the clients' shape), beside the parse, auth and wake suites; CI runs all six.

Wake (api/wake.mjs, 0.3.6, 5 October 2026): GET /wake?since=<stamp> is public (the apps hold no token) and rate limited, 30 a minute per IP. It holds up to 45 s and answers {stamp, at, added, changed, held_ms} the moment the stored stamp differs from since, else the unchanged stamp at the deadline; without since it answers at once. POST /r/<token>/wake {stamp, added} (or POST /wake with x-relay-token or x-igneum-key) records the stamp; packaging/ota/publish-jobs.sh sends it after every verified deploy, with the ids it added. One row per stamp in Neon table relay_wake (created by the first POST); tools/jobs.mjs status reads the rows for the woken latency. The function's maxDuration is 60 s (vercel.json). Tests: relay/test/wake.test.mjs drives the handler with a fake database and clock.

Mac

node tools/relay.mjs (feed), read <id>, drop "<text>"|<file>, task PC2 "title" [file], run PC2 "title" script.ps1 [--elevated] [--reboot-continue] [--reboot], keygen, secret PC2, watch, inbox PC1 [--ack], machines, role PC2 prover, name DESKTOP-XYZ PC2, ack|done|rm <id>, url. Playbooks live in relay/playbooks/; a playbook reads the downloads base from $env:RELAY_DL_BASE (bash: $RELAY_DL_BASE), which the agent holds; run refuses a body that says __DL_BASE__ or carries the dl token (X26). A script asks for a restart by printing RELAY-REBOOT on a line of its own, and only a task queued with --reboot or --reboot-continue restarts the PC.

PCs

relay/clients/make-clients.sh --machine PC1 bakes the URL, key, token, downloads base and that PC's secret into copies of the clients and writes ~/Desktop/igneum-relay-clients-PC1.zip. Carry it by hand; never through the downloads host. Unzip anywhere on the PC. send.bat for people and Claude sessions (see CLAUDE-PC.md), igneum-agent.bat for the automatic runner: double-click once, leave it open. It registers the PC (GPUs, WSL, nvcc; no username, no folder), polls every 20 s, checks each run task's tag and nonce against its secret (a task that fails is answered with exit 77 and nothing of it runs), runs the rest in order, posts a result (exit code, last 64 KB inline, full log as a file when longer) and marks it done. A script that prints RELAY-REBOOT on its own line, in a task queued with --reboot or --reboot-continue, triggers shutdown /r /t 10; with reboot_continue the agent arms a logon task (highest privileges, RunOnce as a fallback) for that one restart and re-runs the task after it with RELAY_PASS incremented. The agent removes the logon task and the RunOnce key every time it starts and when it exits (Ctrl+C included; a closed window is caught by the next start). Nothing is armed on an ordinary start (X25). The PC must sign in by itself for a restart to be unattended.

An unknown hostname that registers appears in the feed with a "name this machine" box, or node tools/relay.mjs name <hostname> PC2. PC1 is DESKTOP-KMCV30N.

Deploy

cd relay && npx --yes vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes --scope igneum

Env on the project: DATABASE_URL, RELAY_KEY, RELAY_TOKEN, RELAY_RUN_PUB (the run key's public half; without it every run is refused), LOG_INTAKE_KEY and LOG_INTAKE_KEY_NEXT (the intake tier; RELAY_INTAKE_COMPAT=0 closes it), BLOB_READ_WRITE_TOKEN (added by vercel blob create-store), DL_TOKEN (the downloads folder token, for the console; added 4 Oct 2026). DNS: relay CNAME cname.vercel-dns.com in the deSEC zone. The first secret or feed call after the deploy adds relay_machines.secret_hash (ADD COLUMN IF NOT EXISTS, no long lock).

Untested until a PC runs it (4 Oct 2026)

send.ps1, igneum-agent.ps1 and the five PowerShell playbooks were written and syntax-reviewed on the Mac (no pwsh here). The bash twin agent.sh and send.sh ran end to end against the live relay on 4 October. The 5 October (night) rewrite of all four clients (headers, the machine tag check, the disarm, POST inbox, -K) is covered by relay/test/clients.test.mjs on the Mac and by the PowerShell 5.1 parse in windows.yml on the next push; it has not run on a PC. Expect a first-run fix on Windows: Start-Process -Wait exit codes through the wrapper, wsl --install --no-launch on pass 2, the RunOnce path after a reboot, and schtasks /Query /TN IgneumRelayAgent on PC 1 (owed: it must return nothing after the new agent's first start).