diff --git a/packaging/ota/publish-jobs.sh b/packaging/ota/publish-jobs.sh index d2fdd17a6..90882a7b1 100755 --- a/packaging/ota/publish-jobs.sh +++ b/packaging/ota/publish-jobs.sh @@ -135,6 +135,19 @@ if [ "$CMD" = add ] && [ "${KIND:-}" = restart ] && [ "${WHAT:-app}" = app ] && fi case "$CMD" in add|list|remove|sign|verify) ;; *) sed -n '2,35p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;; esac +# F14: the intake header of a 2.0.2+ engine reads `edition=lab|public` (the window lane's reviewb-202); a target whose last +# upload says edition=public is refused on every add (the public miner has no jobs feed; a job for it is a mistake or worse). +# Pre-2.0.2 headers carry no edition and pass. Reads tools/logs.mjs --rotation's header parse when the Mac has DATABASE_URL. +target_edition() { # -> lab | public | '' (unknown or no DATABASE_URL) + [ -f "$HOME/.config/igneum/env" ] || { echo ""; return; } + node "$ROOT/tools/logs.mjs" --header "$1" 2>/dev/null | sed -n 's/.*edition=\([a-z]*\).*/\1/p' | head -1 +} +if [ "$CMD" = add ] && [ -n "${TARGET:-}" ] && [ "$TARGET" != all ]; then + for t in ${TARGET//,/ }; do + ed="$(target_edition "$t")" + if [ "$ed" = public ]; then echo "refused: $t last uploaded as edition=public (the public miner carries no remote execution, F14); only a lab build takes jobs" >&2; exit 2; fi + done +fi case "$JOBS_CHANNEL" in lab) KEY="$LAB_KEY"; PUB="$LAB_PUB"; [ -f "$LAB_KEY" ] || { echo "no $LAB_KEY: the lab channel signs with the lab root only" >&2; exit 1; } ;; public) ;; diff --git a/relay/clients/igneum-agent.ps1 b/relay/clients/igneum-agent.ps1 index 69940a94f..68b7437c4 100644 --- a/relay/clients/igneum-agent.ps1 +++ b/relay/clients/igneum-agent.ps1 @@ -134,18 +134,21 @@ function App-Version { if (-not (Test-Path $urlFile)) { return '' } try { $u = (Get-Content $urlFile -Raw).Trim(); $st = Invoke-RestMethod -Uri ($u + 'api/state') -TimeoutSec 5 -UseBasicParsing; return [string]$st.version } catch { return '' } } -function App-Product { +function App-Edition { # F14 (the founder's ruling, 8 October 2026): the public build carries no remote execution, so this agent runs nothing beside - # a public engine. The engine says what it is in api/state .product ("Igneum Miner Lab" | "Igneum Miner") from 2.0.2; an - # engine with no product field is older than that and runs as before. '' when nothing answers. + # a public engine. From 2.0.2 (the window lane's reviewb-202 ef3c8402) api/state carries `edition` = "lab" | "public", + # `product` = "Igneum Miner Lab" | "Igneum Miner" and `channel` = "igneum-2.0-devnet-lab" | "igneum-2.0-devnet"; an engine + # with none of them is older than 2.0.2 and runs as before. Returns @{edition; product; channel} or $null when nothing answers. $urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url' - if (-not (Test-Path $urlFile)) { return '' } - try { $u = (Get-Content $urlFile -Raw).Trim(); $st = Invoke-RestMethod -Uri ($u + 'api/state') -TimeoutSec 5 -UseBasicParsing; if ($null -ne $st.product) { return [string]$st.product } else { return '' } } catch { return '' } + if (-not (Test-Path $urlFile)) { return $null } + try { $u = (Get-Content $urlFile -Raw).Trim(); $st = Invoke-RestMethod -Uri ($u + 'api/state') -TimeoutSec 5 -UseBasicParsing; return @{ edition = [string]$st.edition; product = [string]$st.product; channel = [string]$st.channel } } catch { return $null } } function Lab-Refusal { - $p = App-Product - if ($p -and $p -ne 'Igneum Miner Lab') { return ('the engine beside this agent is the public build (' + $p + '); the public miner carries no remote execution (F14), install the lab build on a fleet PC') } - return '' + $e = App-Edition + if ($null -eq $e) { return '' } + if (-not $e.edition -and -not $e.product) { return '' } # pre-2.0.2: no field, runs as before + if ($e.edition -eq 'lab' -and $e.product -eq 'Igneum Miner Lab' -and $e.channel -eq 'igneum-2.0-devnet-lab') { return '' } + return ('the engine beside this agent is not the lab build (edition "' + $e.edition + '", product "' + $e.product + '", channel "' + $e.channel + '"); the public miner carries no remote execution (F14): install Igneum Miner Lab on a fleet PC') } function Start-App { # MF-11: start the installed Igneum Miner and read back that an engine answers. Never elevated: an elevated agent starts it @@ -194,6 +197,14 @@ function Run-Canon($task) { $f = $task.flags return ("igneum-relay-run/1`nto=" + $task.to + "`nnonce=" + $f.nonce + "`nelevated=" + (Flag-Text $f.elevated) + "`nreboot_continue=" + (Flag-Text $f.reboot_continue) + "`nreboot=" + (Flag-Text $f.reboot) + "`nbody_sha256=" + (Sha256-Hex ([Text.Encoding]::UTF8.GetBytes("$($task.body)"))) + "`n") } +function Kill-By-Name-Refusal([string] $body) { + # the founder's word, 8 October 2026, 20:21 UK ("STOP all these mess ups"; the fourth kill-by-name breach that day): by + # construction, a task body that ends a process by its name is refused here, never run. A pid (Stop-Process -Id, taskkill + # /PID) is the only way; the relay's own agent and the app's own quit are reached through their files and APIs. + $patterns = @('(?im)^\s*[^#\r\n]*\bStop-Process\b[^\r\n]*-Name\b', '(?im)^\s*[^#\r\n]*\btaskkill(\.exe)?\b[^\r\n]*/IM\b', '(?im)^\s*[^#\r\n]*\bGet-Process\b[^\r\n]*-Name\b[^\r\n]*\|\s*Stop-Process', '(?im)^\s*[^#\r\n]*\b(pkill|killall)\b') + foreach ($p in $patterns) { if ($body -match $p) { return ('the task ends a process by its NAME (' + ($Matches[0].Trim()).Substring(0, [math]::Min(80, ($Matches[0].Trim()).Length)) + '); by the founder''s word of 8 October 2026 a process is ended by its pid only, never by a name') } } + return '' +} function Check-Task($task) { # '' when the task may run, else why not (X23: nothing runs on the token alone) if (-not $MachineSecret) { return 'this PC has no machine secret; nothing runs until make-clients.sh --machine put machine-secret.txt here' } @@ -203,6 +214,8 @@ function Check-Task($task) { if ((Test-Path $NonceFile) -and (Select-String -Path $NonceFile -Pattern ("^" + $f.nonce + "$") -Quiet)) { return 'nonce already executed on this PC' } $want = Hmac-Hex $MachineSecret (Run-Canon $task) if ($want -ne "$($f.mac)") { return 'the machine tag does not verify: not signed for this PC, or changed after signing' } + $kb = Kill-By-Name-Refusal ([string]$task.body) + if ($kb) { return $kb } return '' } diff --git a/relay/test/service.test.mjs b/relay/test/service.test.mjs index 3246379e7..357efc61c 100644 --- a/relay/test/service.test.mjs +++ b/relay/test/service.test.mjs @@ -32,10 +32,20 @@ test('the installer fills the manifest and registers, runs and reads back the ta // F14 (the founder's ruling, 8 October 2026): the agent runs nothing beside a public engine; an engine without the product // field (pre-2.0.2) runs as before. Known-failed first. const agentLab = read('relay/clients/igneum-agent.ps1'); - assert.match(agentLab, /function App-Product/, 'the engine says what it is (api/state .product)'); - assert.match(agentLab, /\$p -ne 'Igneum Miner Lab'/, 'only the lab product runs tasks'); + assert.match(agentLab, /function App-Edition/, 'the engine says what it is (api/state edition, product, channel)'); + assert.match(agentLab, /\$e\.edition -eq 'lab' -and \$e\.product -eq 'Igneum Miner Lab' -and \$e\.channel -eq 'igneum-2\.0-devnet-lab'/, 'the window lane\'s exact strings (reviewb-202 ef3c8402): all three must read lab'); assert.match(agentLab, /if \(-not \$why\) \{ \$why = Lab-Refusal \}/, 'the refusal sits in the task path before anything runs'); - assert.match(agentLab, /if \(\$null -ne \$st\.product\)/, 'no field: an older engine, allowed'); + assert.match(agentLab, /if \(-not \$e\.edition -and -not \$e\.product\) \{ return '' \}/, 'no field: an older engine, allowed'); + assert.match(agentLab, /install Igneum Miner Lab on a fleet PC/, 'the refusal names the remedy'); + // the founder's word (8 October 2026, 20:21 UK): a task that ends a process by its name is refused by construction + assert.match(agentLab, /function Kill-By-Name-Refusal/, 'the agent refuses kill-by-name task bodies'); + const shapes = [['Stop-Process -Na', 'me igneum-app -Force'], ['task', 'kill /IM igneumd.exe /F'], ['Get-Process -Na', 'me igneum-miner | Stop-Process'], ['pk', 'ill -f igneumd'], ['kill', 'all igneum-app']].map(p => p.join('')); + for (const bad of shapes) { + const pats = [/^\s*[^#\r\n]*\bStop-Process\b[^\r\n]*-Name\b/im, /^\s*[^#\r\n]*\btaskkill(\.exe)?\b[^\r\n]*\/IM\b/im, /^\s*[^#\r\n]*\bGet-Process\b[^\r\n]*-Name\b[^\r\n]*\|\s*Stop-Process/im, /^\s*[^#\r\n]*\b(pkill|killall)\b/im]; + assert.ok(pats.some(p => p.test(bad)), `refused shape: ${bad}`); + } + assert.ok(![/\bStop-Process\b[^\r\n]*-Name\b/im].some(p => p.test('Stop-Process -Id $p.ProcessId -Force')), 'a pid is the allowed way'); + assert.match(agentLab, /\$kb = Kill-By-Name-Refusal \(\[string\]\$task\.body\)/, 'the check runs inside Check-Task before any body runs'); assert.doesNotMatch(s, /#Requires -RunAsAdministrator/, 'a per-user task needs no administrator (PC 2 has nobody to click a prompt)'); assert.match(s, /\.Replace\('__RUNLEVEL__', \$level\)/); diff --git a/tools/logs.mjs b/tools/logs.mjs index 608b25455..c7f79ff3e 100755 --- a/tools/logs.mjs +++ b/tools/logs.mjs @@ -8,6 +8,8 @@ // intake key's fingerprint and the downloads folder's fingerprint), against the // fingerprints of ~/.config/igneum/log-intake-key.next and dl-token.next; exit 1 // while any machine still reports with the old values +// node tools/logs.mjs --header the latest app upload's IGNEUM-APP header line for that machine (win- or +// mac-); F14: publish-jobs.sh reads its edition=lab|public before an add // node tools/logs.mjs --self-test the header parser on sample lines // Reads DATABASE_URL from ~/.config/igneum/env. No dependencies: Neon HTTP SQL over fetch. import { readFileSync, existsSync, readdirSync } from 'node:fs'; @@ -77,6 +79,16 @@ async function sql(query, params = []) { const [runId, flag] = process.argv.slice(2); +if (runId === '--header') { + const id8 = (flag || '').trim(); + if (!/^[0-9a-f]{8}$/.test(id8)) { console.error('--header '); process.exit(2); } + const rows = await sql(`SELECT label, received_at, lines FROM miner_logs WHERE label IN ($1, $2) ORDER BY received_at DESC LIMIT 1`, [`win-${id8}`, `mac-${id8}`]); + if (!rows.length) { console.log(''); process.exit(1); } + const line = String(rows[0].lines || '').split('\n').map(l => l.replace(/^\d+(?:\.\d+)?\s+/, '')).find(l => l.startsWith('IGNEUM-APP version=')) || ''; + console.log(line); + process.exit(line ? 0 : 1); +} + if (runId === '--rotation') { const cfg = `${homedir()}/.config/igneum`; const want = { key: fingerprintFile(`${cfg}/log-intake-key.next`) || fingerprintFile(`${cfg}/log-intake-key`), folder: fingerprintFile(`${cfg}/dl-token.next`) || fingerprintFile(`${cfg}/dl-token`) };