From 311eabdb31a25612ab304a04e985eebd43243d8e Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:20:02 +0000 Subject: [PATCH] relay: the lab-install kind (main's ruling 9 Oct 2026): node tools/relay.mjs lab-install queues the one task an agent accepts beside a public engine: a run whose comment-only body pins the lab Setup's name and sha256 and the engine machine id of one of the two house rigs, signed by the lab OTA key (flags.lab_sig) beside the run key and the machine tag; relay/lib/guard.mjs checkLabInstall is the publisher's and the relay's check, relay/clients/lab-install.ps1 the agent's (plus the engine's own id beside it). Tests: relay/test/lab-install.test.mjs (5), relay/clients/lab-install.test.ps1 (9 of 9 on the Windows canary VM). Every other machine refuses as F14 says; the public OTA channel is untouched --- relay/clients/igneum-agent.ps1 | 13 +++++- relay/clients/lab-install.ps1 | 66 ++++++++++++++++++++++++++++++ relay/clients/lab-install.test.ps1 | 30 ++++++++++++++ relay/lib/guard.mjs | 41 +++++++++++++++++++ relay/test/lab-install.test.mjs | 44 ++++++++++++++++++++ tools/relay.mjs | 21 +++++++++- 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 relay/clients/lab-install.ps1 create mode 100644 relay/clients/lab-install.test.ps1 create mode 100644 relay/test/lab-install.test.mjs diff --git a/relay/clients/igneum-agent.ps1 b/relay/clients/igneum-agent.ps1 index 7554e8ada..650251aee 100644 --- a/relay/clients/igneum-agent.ps1 +++ b/relay/clients/igneum-agent.ps1 @@ -26,6 +26,7 @@ $RelayToken = '__RELAY_TOKEN__' $DlBase = '__DL_BASE__' if ($env:RELAY_DL_BASE) { $DlBase = $env:RELAY_DL_BASE } $Here = Split-Path -Parent $MyInvocation.MyCommand.Path +if (Test-Path (Join-Path $Here 'lab-install.ps1')) { . (Join-Path $Here 'lab-install.ps1') } # the one kind accepted beside a public engine (main's ruling, 9 Oct 2026) $StateDir = Join-Path $env:LOCALAPPDATA 'igneum-relay' $TaskDir = Join-Path $StateDir 'tasks' $LogDir = Join-Path $StateDir 'logs' @@ -245,7 +246,8 @@ function Run-Task($task, [int] $pass) { $rebootAllowed = $rebootContinue -or [bool]$task.flags.reboot Log ("task #" + $id + " '" + $task.title + "' pass " + $pass + $(if ($elevated) { " elevated" } else { "" }) + $(if ($rebootContinue) { " reboot_continue" } elseif ($rebootAllowed) { " reboot" } else { "" })) $why = Check-Task $task - if (-not $why) { $why = Lab-Refusal } + $labInstall = [bool]$task.flags.lab_install + if (-not $why) { if ($labInstall -and (Get-Command LabInstall-Refusal -ErrorAction SilentlyContinue)) { $why = LabInstall-Refusal $task $script:Machine (LabInstall-EngineMachine) } else { $why = Lab-Refusal } } if ($why) { Log ("task #" + $id + " REFUSED: " + $why) Add-Content -Path $log -Value ("REFUSED: " + $why) @@ -254,6 +256,15 @@ function Run-Task($task, [int] $pass) { return } Add-Content -Path $NonceFile -Value $task.flags.nonce + if ($labInstall -and (Get-Command Lab-Install -ErrorAction SilentlyContinue)) { + # nothing of the body runs: the agent's own Lab-Install (download, sha, the Setup scheduled detached) is the whole task + $r = Lab-Install $task $DlBase + Log ("task #" + $id + " " + $r.text) + Add-Content -Path $log -Value $r.text + Post-Result $task ([int]$r.code) $log $r.note + try { Api-Post 'done' @{ id = $id; exit_code = [int]$r.code } | Out-Null } catch { Log ("done failed: " + $_.Exception.Message) } + return + } if ("$($task.kind)" -eq 'start-app') { # nothing of the body runs: the agent's own Start-App is the whole task $r = Start-App diff --git a/relay/clients/lab-install.ps1 b/relay/clients/lab-install.ps1 new file mode 100644 index 000000000..07da7a381 --- /dev/null +++ b/relay/clients/lab-install.ps1 @@ -0,0 +1,66 @@ +# lab-install (main's ruling, 9 October 2026, 12:2x UK): the ONE task kind the relay agent accepts beside a PUBLIC-edition engine. +# F14 (the founder, 8 October 2026) stands for everything else: the public miner carries no remote execution. This exception +# exists so the project's two house rigs come back as lab machines after a public update with no hand, and it is narrow by +# construction: the task is a `run` whose body is comment lines only (an older agent executes nothing from it), signed by the +# relay run key and tagged with the machine secret like every run, AND signed by the lab OTA key (flags.lab_sig, the relay's +# check), AND its pinned values live in the signed body: the lab Setup's file name and sha256 and the target's engine machine +# id, which must be one of the two baked house rigs AND the id the engine beside this agent reports. Every other machine +# refuses it exactly as F14 says, so no public user can ever be pushed a lab build; the public OTA channel is untouched. +# The agent runs nothing from the body: Lab-Install downloads the pinned Setup from the agent's own downloads base, checks the +# sha, and schedules the Setup detached (a one-shot task), because the Setup's last step re-registers this very agent as a +# service and ends the running copy. The result says "scheduled"; the read-back (edition lab beside the new agent) is the +# publisher's next task, which the fresh lab-side agent accepts as an ordinary run. +$LabInstallMachines = @('ae432dc7', '1ccfe586') # PC 1, PC 2: the engine machine ids (api/state.machine) of the house rigs +$LabInstallNames = @('PC1', 'PC2') # their relay names (the agent's own $script:Machine) + +function LabInstall-Fields([string] $body) { + # the pinned values, one per comment line: "# key=value"; anything else in the body refuses the task + $f = @{} + foreach ($line in ($body -split "`r?`n")) { + $t = $line.Trim(); if (-not $t) { continue } + if ($t -eq '# lab-install') { $f['kind'] = 'lab-install'; continue } + if ($t -match '^#\s*(setup_name|setup_sha|machine_id)=(\S+)\s*$') { $f[$Matches[1]] = $Matches[2]; continue } + return $null # a line that is not a pinned value: not a lab-install body + } + return $f +} + +function LabInstall-Refusal($task, [string] $machineName, [string] $engineMachineId) { + # '' when the task may run as a lab install, else why not. Every check is a refusal; none is a warning. + $f = LabInstall-Fields ([string]$task.body) + if ($null -eq $f -or $f['kind'] -ne 'lab-install') { return 'lab-install: the body is not the pinned form (comment lines only: # lab-install, # setup_name=, # setup_sha=, # machine_id=)' } + foreach ($k in @('setup_name', 'setup_sha', 'machine_id')) { if (-not $f[$k]) { return ('lab-install: no ' + $k + ' pinned in the signed body') } } + if ($f['setup_name'] -notmatch '^Igneum-Miner-Lab-Setup-[0-9][0-9.]*(-[0-9a-f]{8})?\.exe$') { return ('lab-install: setup_name "' + $f['setup_name'] + '" is not a lab Setup file name') } + if ($f['setup_sha'] -notmatch '^[0-9a-f]{64}$') { return 'lab-install: setup_sha is not 64 hex' } + if ($f['machine_id'] -notmatch '^[0-9a-f]{8}$') { return 'lab-install: machine_id is not 8 hex' } + if ($LabInstallMachines -notcontains $f['machine_id']) { return ('lab-install: machine id ' + $f['machine_id'] + ' is not one of the two house rigs; F14 stands (the public miner carries no remote execution)') } + if ($LabInstallNames -notcontains $machineName) { return ('lab-install: this agent (' + $machineName + ') is not a house rig; F14 stands') } + if (-not $engineMachineId) { return 'lab-install: no engine answers api/state beside this agent, so its machine id cannot be read; nothing installs blind' } + if ($engineMachineId -ne $f['machine_id']) { return ('lab-install: the engine beside this agent is machine ' + $engineMachineId + ', not the pinned ' + $f['machine_id']) } + $sig = [string]$task.flags.lab_sig + if ($sig -notmatch '^[0-9a-f]{128}$') { return 'lab-install: no flags.lab_sig (the Ed25519 signature by the lab OTA key, 128 hex; the relay verifies it against the lab public key)' } + return '' +} + +function LabInstall-EngineMachine() { + $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; return [string]$st.machine } catch { return '' } +} + +function Lab-Install($task, [string] $dlBase) { + # returns @{ code; text; note }. Downloads the pinned Setup, checks the sha, schedules it detached; nothing of the body runs. + $f = LabInstall-Fields ([string]$task.body) + $art = Join-Path $env:LOCALAPPDATA 'igneum\artefacts'; New-Item -ItemType Directory -Force -Path $art | Out-Null + $setup = Join-Path $art $f['setup_name'] + try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri ($dlBase + '/' + $f['setup_name']) -OutFile $setup -UseBasicParsing -TimeoutSec 900 } catch { return @{ code = 3; text = ('lab-install: download failed: ' + $_.Exception.Message); note = 'download failed' } } + $got = (Get-FileHash -LiteralPath $setup -Algorithm SHA256).Hash.ToLower() + if ($got -ne $f['setup_sha']) { Remove-Item -LiteralPath $setup -Force -ErrorAction SilentlyContinue; return @{ code = 3; text = ('lab-install: the Setup sha256 ' + $got + ' is not the pinned ' + $f['setup_sha'] + '; removed, nothing installs'); note = 'sha mismatch' } } + $log = Join-Path $art ($f['setup_name'] + '.install.log') + $tn = 'Igneum lab install' + $tr = '"' + $setup + '" /VERYSILENT /NORESTART /SUPPRESSMSGBOXES /LOG="' + $log + '"' + & schtasks.exe /Create /F /SC ONCE /ST 23:59 /RL LIMITED /TN $tn /TR $tr 2>&1 | Out-Null + $r = & schtasks.exe /Run /TN $tn 2>&1 | Out-String + if ($r -notmatch 'SUCCESS') { return @{ code = 4; text = ('lab-install: the one-shot task did not start: ' + $r.Trim()); note = 'schtasks failed' } } + return @{ code = 0; text = ('lab-install: ' + $f['setup_name'] + ' sha256 ' + $got + ' scheduled detached at ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + ' (task "' + $tn + '", log ' + $log + '); the Setup stops the public app, installs Igneum Miner Lab and re-registers this agent as a service beside it; read edition=lab with the next task'); note = 'scheduled' } +} diff --git a/relay/clients/lab-install.test.ps1 b/relay/clients/lab-install.test.ps1 new file mode 100644 index 000000000..27edfc297 --- /dev/null +++ b/relay/clients/lab-install.test.ps1 @@ -0,0 +1,30 @@ +# The agent-side test of the lab-install kind (relay/clients/lab-install.ps1): three refusals and one acceptance, run on a Windows +# box: powershell -NoProfile -ExecutionPolicy Bypass -File relay\clients\lab-install.test.ps1 (exit 0 = every case as expected). +# The relay-side twin (the lab key's Ed25519 signature) is relay/test/lab-install.test.mjs; this side checks what the agent can +# check without a signature primitive: the pinned body form, the Setup name and sha shape, the two house rigs' machine ids +# against the engine beside it, the lab_sig's presence, and the result of a tampered body. +$ErrorActionPreference = 'Stop' +. (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'lab-install.ps1') +$sha = ('ab' * 32); $sig = ('cd' * 64) +function Mk([string] $name, [string] $s, [string] $mid, [string] $lsig) { + @{ to = 'PC1'; kind = 'run'; body = ("# lab-install`n# setup_name=" + $name + "`n# setup_sha=" + $s + "`n# machine_id=" + $mid + "`n"); flags = @{ lab_install = $true; lab_sig = $lsig; nonce = ('ef' * 16) } } +} +$fails = 0 +function Expect([string] $case, [string] $got, [string] $want) { + if ($want -eq '' -and $got -eq '') { Write-Output ('ok ' + $case); return } + if ($want -ne '' -and $got -like ('*' + $want + '*')) { Write-Output ('ok ' + $case + ' -> refused: ' + $got.Substring(0, [Math]::Min(90, $got.Length))); return } + Write-Output ('FAIL ' + $case + ': got "' + $got + '", wanted "' + $want + '"'); $script:fails++ +} +Expect 'acceptance: PC 1, the pinned lab Setup, the engine beside reads ae432dc7, lab_sig present' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3-41abccce.exe' $sha 'ae432dc7' $sig) 'PC1' 'ae432dc7') '' +Expect 'refusal 1: no lab signature on the task (the lab key did not sign it)' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' $sha 'ae432dc7' '') 'PC1' 'ae432dc7') 'no flags.lab_sig' +Expect 'refusal 2: a Setup sha that is not 64 hex' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' 'not-a-sha' 'ae432dc7' $sig) 'PC1' 'ae432dc7') 'setup_sha is not 64 hex' +Expect 'refusal 3a: a foreign machine id (not a house rig)' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' $sha 'deadbeef' $sig) 'PC1' 'deadbeef') 'not one of the two house rigs' +Expect 'refusal 3b: a house rig id pinned but the engine beside this agent is another machine' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' $sha 'ae432dc7' $sig) 'PC1' '04383a0c') 'not the pinned' +Expect 'refusal 3c: this agent is not a house rig (the canary VM)' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' $sha 'ae432dc7' $sig) 'win-canary' 'ae432dc7') 'not a house rig' +Expect 'refusal 3d: no engine answers (nothing installs blind)' (LabInstall-Refusal (Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' $sha 'ae432dc7' $sig) 'PC1' '') 'no engine answers' +Expect 'refusal: a public Setup name is not a lab Setup' (LabInstall-Refusal (Mk 'Igneum-Miner-Setup-2.0.3.exe' $sha 'ae432dc7' $sig) 'PC1' 'ae432dc7') 'not a lab Setup file name' +$t = Mk 'Igneum-Miner-Lab-Setup-2.0.3.exe' $sha 'ae432dc7' $sig; $t.body = "# lab-install`nStart-Process evil.exe`n" +Expect 'refusal: a body with a line that is not a pinned value (nothing of a body ever runs)' (LabInstall-Refusal $t 'PC1' 'ae432dc7') 'not the pinned form' +if ($fails -gt 0) { Write-Output ('RESULT lab-install agent test: ' + $fails + ' FAIL'); exit 1 } +Write-Output 'RESULT lab-install agent test: 9 of 9 as expected (1 acceptance, 8 refusals)' +exit 0 diff --git a/relay/lib/guard.mjs b/relay/lib/guard.mjs index 38d11732b..267e61469 100644 --- a/relay/lib/guard.mjs +++ b/relay/lib/guard.mjs @@ -122,3 +122,44 @@ export function machineForSecret(secret, rows) { const hit = rows.find(r => r.secret_hash && sameSecret(h, r.secret_hash)); return hit ? { name: hit.name } : { error: 'unknown machine secret' }; } + +// lab-install (main's ruling, 9 October 2026, 12:2x UK): the one task an agent accepts beside a PUBLIC engine, so the two house rigs +// come back as lab machines after a public update with no hand. A `run` whose body is comment lines only (an older agent executes +// nothing from it), pinning the lab Setup's file name and sha256 and the target's engine machine id (api/state.machine), signed by +// the run key and tagged with the machine secret like every run, AND signed by the lab OTA key over the same canon (flags.lab_sig). +// The machine id must be one of the two baked house rigs; every other machine refuses it as F14 says. relay/clients/lab-install.ps1 +// makes the same checks on the agent's side (plus the engine's own id beside it); this is the publisher's and the relay's copy. +export const LAB_INSTALL_MACHINES = ['ae432dc7', '1ccfe586']; // PC 1, PC 2 +export const LAB_INSTALL_NAMES = ['PC1', 'PC2']; +export const LAB_SETUP_NAME = /^Igneum-Miner-Lab-Setup-[0-9][0-9.]*(-[0-9a-f]{8})?\.exe$/; +export function labInstallBody({ setupName, setupSha, machineId }) { + return `# lab-install\n# setup_name=${setupName}\n# setup_sha=${setupSha}\n# machine_id=${machineId}\n`; +} +export function labInstallFields(body) { + const f = {}; + for (const raw of String(body || '').split(/\r?\n/)) { + const t = raw.trim(); if (!t) continue; + if (t === '# lab-install') { f.kind = 'lab-install'; continue; } + const m = /^#\s*(setup_name|setup_sha|machine_id)=(\S+)\s*$/.exec(t); + if (!m) return null; + f[m[1]] = m[2]; + } + return f; +} +/** '' when the task may be queued (and may run on the named rig), else why not. */ +export function checkLabInstall(o, labPubHex, allow = { names: LAB_INSTALL_NAMES, machines: LAB_INSTALL_MACHINES }) { + const f = labInstallFields(o.body); + if (!f || f.kind !== 'lab-install') return 'the body is not the pinned form (comment lines only)'; + for (const k of ['setup_name', 'setup_sha', 'machine_id']) if (!f[k]) return `no ${k} pinned in the signed body`; + if (!LAB_SETUP_NAME.test(f.setup_name)) return `setup_name ${f.setup_name} is not a lab Setup file name`; + if (!/^[0-9a-f]{64}$/.test(f.setup_sha)) return 'setup_sha is not 64 hex'; + if (!/^[0-9a-f]{8}$/.test(f.machine_id)) return 'machine_id is not 8 hex'; + if (!allow.machines.includes(f.machine_id)) return `machine id ${f.machine_id} is not one of the two house rigs; F14 stands`; + if (!allow.names.includes(String(o.to))) return `${o.to} is not a house rig; F14 stands`; + const fl = o.flags && typeof o.flags === 'object' ? o.flags : {}; + if (fl.lab_install !== true) return 'flags.lab_install is not true'; + if (!isSig(fl.lab_sig)) return 'no flags.lab_sig (the Ed25519 signature by the lab OTA key)'; + if (!isPub(labPubHex)) return 'no lab public key to verify against'; + if (!verifyRun(runCanon({ to: o.to, nonce: fl.nonce, body: o.body, flags: fl }), fl.lab_sig, labPubHex)) return 'the lab signature does not verify against the lab public key'; + return ''; +} diff --git a/relay/test/lab-install.test.mjs b/relay/test/lab-install.test.mjs new file mode 100644 index 000000000..0fed3ea84 --- /dev/null +++ b/relay/test/lab-install.test.mjs @@ -0,0 +1,44 @@ +// lab-install (main's ruling, 9 October 2026): the one task an agent accepts beside a PUBLIC engine. Three refusals and one +// acceptance on the publisher's and the relay's check (relay/lib/guard.mjs checkLabInstall), the same rules the agent makes +// on its side (relay/clients/lab-install.ps1, its own test lab-install.test.ps1 on a Windows box). +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { keygen, signRun, runCanon, machineTag, newNonce, newSecret, labInstallBody, checkLabInstall, labInstallFields } from '../lib/guard.mjs'; + +const lab = keygen(); // the lab OTA key pair (seed, pub) as hex +const other = keygen(); // another key: the public OTA key, anyone's key +const run = keygen(); +const secret = newSecret(); +const SHA = 'ab'.repeat(32); + +function task({ to = 'PC1', machineId = 'ae432dc7', setupName = 'Igneum-Miner-Lab-Setup-2.0.3-41abccce.exe', setupSha = SHA, labSeed = lab.seed, labInstall = true } = {}) { + const body = labInstallBody({ setupName, setupSha, machineId }); + const o = { to, kind: 'run', body, flags: { elevated: false, reboot_continue: false, reboot: false, shell: 'powershell', script: 'lab-install.ps1', lab_install: labInstall } }; + o.flags.nonce = newNonce(); + o.flags.mac = machineTag(secret, runCanon({ to, nonce: o.flags.nonce, body, flags: o.flags })); + o.flags.sig = signRun(runCanon({ to, nonce: o.flags.nonce, body, flags: o.flags }), run.seed); + o.flags.lab_sig = signRun(runCanon({ to, nonce: o.flags.nonce, body, flags: o.flags }), labSeed); + return o; +} + +test('acceptance: a house rig, the pinned lab Setup, signed by the lab key', () => { + assert.equal(checkLabInstall(task(), lab.pub), ''); +}); +test('refusal 1: signed by another key (the public OTA key, anyone else)', () => { + assert.match(checkLabInstall(task({ labSeed: other.seed }), lab.pub), /lab signature does not verify/); +}); +test('refusal 2: a wrong or unpinned Setup sha', () => { + assert.match(checkLabInstall(task({ setupSha: 'not-a-sha' }), lab.pub), /setup_sha is not 64 hex/); + const o = task(); o.body = labInstallBody({ setupName: 'Igneum-Miner-Lab-Setup-2.0.3.exe', setupSha: 'cd'.repeat(32), machineId: 'ae432dc7' }); // the body changed after signing + assert.match(checkLabInstall(o, lab.pub), /lab signature does not verify/); +}); +test('refusal 3: a foreign machine id or a machine that is not a house rig (F14 stands)', () => { + assert.match(checkLabInstall(task({ machineId: 'deadbeef' }), lab.pub), /not one of the two house rigs/); + assert.match(checkLabInstall(task({ to: 'win-canary' }), lab.pub), /not a house rig/); +}); +test('the body form: comment lines only; a line that is not a pinned value is not a lab-install body', () => { + assert.equal(labInstallFields('# lab-install\n# setup_name=Igneum-Miner-Lab-Setup-2.0.3.exe\n# setup_sha=' + SHA + '\n# machine_id=ae432dc7\n').machine_id, 'ae432dc7'); + assert.equal(labInstallFields('# lab-install\nStart-Process evil.exe\n'), null); + assert.match(checkLabInstall(task({ setupName: 'Igneum-Miner-Setup-2.0.3.exe' }), lab.pub), /not a lab Setup file name/); + assert.match(checkLabInstall(task({ labInstall: false }), lab.pub), /lab_install is not true/); +}); diff --git a/tools/relay.mjs b/tools/relay.mjs index 1c05e29d3..f2d91af26 100644 --- a/tools/relay.mjs +++ b/tools/relay.mjs @@ -9,6 +9,7 @@ // a script the igneum-agent runs: signed with ~/.config/igneum/relay-run-key // (Ed25519, checked by the relay) and tagged with the machine's secret // (~/.config/igneum/relay-machines/, checked by the agent); X23 +// node tools/relay.mjs lab-install the one task an agent runs beside a PUBLIC engine (the two house rigs only) // node tools/relay.mjs start-app the agent there starts the installed Igneum Miner and reports whether an // engine answered (MF-11); signed and tagged like run, body = relay/playbooks/start-app.ps1 // node tools/relay.mjs keygen make the run key pair once; prints the public key for RELAY_RUN_PUB @@ -24,7 +25,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, chmodSync import { homedir, tmpdir, hostname } from 'node:os'; import { basename, join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { keygen as edKeygen, signRun, runCanon, machineTag, newNonce, newSecret, secretHash } from '../relay/lib/guard.mjs'; +import { keygen as edKeygen, signRun, runCanon, machineTag, newNonce, newSecret, secretHash, labInstallBody, checkLabInstall, LAB_INSTALL_NAMES, LAB_INSTALL_MACHINES } from '../relay/lib/guard.mjs'; process.stdout.on('error', e => { if (e.code === 'EPIPE') process.exit(0); throw e; }); const CFG = join(homedir(), '.config', 'igneum'); @@ -147,6 +148,24 @@ try { signRunTask(o); const r = await api('task', { body: o }); console.log(`queued #${r.id} start-app for ${to}: the agent starts the installed app and reports in about a minute (node tools/relay.mjs watch)`); } + else if (cmd === 'lab-install') { + // the one kind an agent accepts beside a PUBLIC engine (main's ruling, 9 October 2026): a run whose body is comment lines only, + // pinning the lab Setup's name and sha256 and the target's engine machine id (one of the two house rigs), signed by the run + // key and tagged like every run AND signed by the lab OTA key (flags.lab_sig). relay/lib/guard.mjs checkLabInstall is the + // same check the agent makes on its side (relay/clients/lab-install.ps1), run here before anything is queued. + const [, to, setupName, setupSha, machineId] = pos; + if (!to || !setupName || !setupSha || !machineId) throw new Error('lab-install [-].exe> '); + const body = labInstallBody({ setupName, setupSha, machineId }); + const o = { from: flags.from || 'Mac', to, title: flags.title || `lab-install: ${setupName} (${setupSha.slice(0, 12)}) on ${to}/${machineId}`, kind: 'run', body, flags: { elevated: false, reboot_continue: false, reboot: false, shell: 'powershell', script: 'lab-install.ps1', lab_install: true, timeout_minutes: 30 } }; + signRunTask(o); + const labSeed = cfg('lab-signing/lab-signing-key'); + if (!/^[0-9a-f]{64}$/.test(labSeed)) throw new Error('no lab signing key at ~/.config/igneum/lab-signing/lab-signing-key'); + o.flags.lab_sig = signRun(runCanon({ to: o.to, nonce: o.flags.nonce, body: o.body, flags: o.flags }), labSeed); + const labPub = cfg('lab-signing/lab-signing-key.pub'); + const why = checkLabInstall(o, labPub, { names: LAB_INSTALL_NAMES, machines: LAB_INSTALL_MACHINES }); + if (why) throw new Error(`lab-install refused before queueing: ${why}`); + const r = await api('task', { body: o }); console.log(`queued #${r.id} lab-install for ${to}: ${o.title}`); + } else if (cmd === 'keygen') { if (existsSync(RUN_KEY)) throw new Error(`${RUN_KEY} exists; not overwriting a signing key`); const { seed, pub } = edKeygen();