New Vercel project igneum-relay from relay/: one function (api/relay.mjs) over Neon tables relay_items and relay_machines, files in Vercel Blob store igneum-relay (50 MB client uploads, 4 MB through the function), phone-first web page at /r/<token>/ with the site tokens. Mac CLI tools/relay.mjs (feed, read, drop, task, run, watch, inbox, machines, role, name). Windows clients send.bat/send.ps1 and the igneum-agent (registers hostname, role, GPUs, WSL, nvcc; runs queued PowerShell scripts, posts results, reboot-continue via scheduled task + RunOnce), bash twins send.sh and agent.sh (verified live), playbooks for WSL setup, prover setup, prove-block, miner v4, one-click placeholder. make-clients.sh bakes the secrets into a zip; the repo copies hold placeholders. Screenshots under docs/design/relay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
173 lines
11 KiB
PowerShell
173 lines
11 KiB
PowerShell
# Igneum relay agent for Windows (PowerShell 5.1 or later). Started by igneum-agent.bat, which keeps it alive.
|
|
# What it does: registers this PC on the relay (hostname, role from the relay, GPUs, WSL state, nvcc), then every 20 s
|
|
# fetches the `run` tasks queued for it on the Mac, runs each one in order (a PowerShell script per task), captures
|
|
# the output and posts a `result` item (exit code, last 64 KB inline, the full log as a file) and marks the task done.
|
|
# Flags per task: elevated (needs administrator; the agent itself runs elevated, so there is no prompt),
|
|
# reboot_continue (a task that prints RELAY-REBOOT is re-run after the restart with RELAY_PASS incremented).
|
|
# State lives in %LOCALAPPDATA%\igneum-relay (state.json, tasks\, logs\). Nothing else is written outside the task's own doing.
|
|
# The URL, key and token are written in by make-clients.sh. The repo copy holds placeholders.
|
|
$ErrorActionPreference = 'Continue'
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
$RelayUrl = '__RELAY_URL__'
|
|
$RelayKey = '__RELAY_KEY__'
|
|
$RelayToken = '__RELAY_TOKEN__'
|
|
$Base = "$RelayUrl/r/$RelayToken/api"
|
|
$Headers = @{ 'x-igneum-key' = $RelayKey }
|
|
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$StateDir = Join-Path $env:LOCALAPPDATA 'igneum-relay'
|
|
$TaskDir = Join-Path $StateDir 'tasks'
|
|
$LogDir = Join-Path $StateDir 'logs'
|
|
$StateFile = Join-Path $StateDir 'state.json'
|
|
$PollSeconds = 20
|
|
$TailBytes = 65536
|
|
New-Item -ItemType Directory -Force -Path $StateDir, $TaskDir, $LogDir | Out-Null
|
|
|
|
function Log([string] $m) { Write-Host ("[" + (Get-Date -Format 'HH:mm:ss') + "] " + $m) }
|
|
function Is-Admin { ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) }
|
|
function Api-Get([string] $Fn) { Invoke-RestMethod -Uri "$Base/$Fn" -Headers $Headers -TimeoutSec 60 }
|
|
function Api-Post([string] $Fn, $Body) {
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes((ConvertTo-Json $Body -Depth 8 -Compress))
|
|
Invoke-RestMethod -Method Post -Uri "$Base/$Fn" -Headers $Headers -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 120
|
|
}
|
|
function Read-State { if (Test-Path $StateFile) { try { return (Get-Content $StateFile -Raw | ConvertFrom-Json) } catch {} }; return $null }
|
|
function Write-State($o) { if ($null -eq $o) { Remove-Item $StateFile -ErrorAction SilentlyContinue } else { ConvertTo-Json $o -Compress | Set-Content -Path $StateFile -Encoding ascii } }
|
|
function Strip-Nulls([string] $s) { if ($null -eq $s) { return '' }; return ($s -replace "`0", '') }
|
|
|
|
# One agent per machine: a named mutex stops a second copy (the scheduled task and a double-click, for instance).
|
|
$mutex = New-Object System.Threading.Mutex($false, 'Global\IgneumRelayAgent')
|
|
if (-not $mutex.WaitOne(0)) { Log 'another igneum-agent is already running on this PC; this one exits'; Start-Sleep 5; exit 0 }
|
|
|
|
function Collect-Info {
|
|
$info = @{ os = ''; user = $env:USERNAME; admin = (Is-Admin); gpus = @(); wsl = ''; nvcc = $false; agent = 'igneum-agent.ps1 v1'; dir = $Here }
|
|
try { $info.os = (Get-CimInstance Win32_OperatingSystem).Caption + ' build ' + (Get-CimInstance Win32_OperatingSystem).BuildNumber } catch {}
|
|
try {
|
|
$nv = Get-Command nvidia-smi -ErrorAction SilentlyContinue
|
|
if ($nv) { $info.gpus = @((& nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader 2>$null) | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) }
|
|
if (-not $info.gpus -or $info.gpus.Count -eq 0) { $info.gpus = @(Get-CimInstance Win32_VideoController | ForEach-Object { $_.Name }) }
|
|
} catch {}
|
|
try {
|
|
$w = Strip-Nulls (((& wsl.exe --status 2>&1) | Out-String))
|
|
$l = Strip-Nulls (((& wsl.exe --list --verbose 2>&1) | Out-String))
|
|
$distros = @($l -split "`n" | Select-Object -Skip 1 | ForEach-Object { $_.Trim() } | Where-Object { $_ } | ForEach-Object { ($_ -replace '^\*\s*', '') -replace '\s+', ' ' })
|
|
$info.wsl = $(if ($distros.Count) { $distros -join '; ' } else { ($w -split "`n" | Select-Object -First 1).Trim() })
|
|
if (-not $info.wsl) { $info.wsl = 'none' }
|
|
} catch { $info.wsl = 'none' }
|
|
$info.nvcc = [bool](Get-Command nvcc -ErrorAction SilentlyContinue)
|
|
return $info
|
|
}
|
|
|
|
function Register-Machine {
|
|
$info = Collect-Info
|
|
$r = Api-Post 'register' @{ hostname = $env:COMPUTERNAME; info = $info }
|
|
Set-Content -Path (Join-Path $StateDir 'machine.txt') -Value $r.name -Encoding ascii
|
|
$script:Machine = $r.name; $script:Role = $r.role
|
|
Log ("registered as " + $r.name + " (role " + ($(if ($r.role) { $r.role } else { 'unset' })) + ", named " + $r.named + "); gpus: " + ($info.gpus -join ', ') + "; wsl: " + $info.wsl + "; nvcc: " + $info.nvcc)
|
|
if (-not $r.named) { Log "this PC is not named yet. On the Mac: node tools/relay.mjs name $env:COMPUTERNAME PC2" }
|
|
}
|
|
|
|
function Arm-Restart {
|
|
# Re-arm after a reboot: a logon scheduled task with highest privileges (no UAC prompt), plus RunOnce as a fallback.
|
|
$bat = Join-Path $Here 'igneum-agent.bat'
|
|
try {
|
|
& schtasks.exe /Create /F /TN 'IgneumRelayAgent' /SC ONLOGON /RL HIGHEST /TR ("cmd /c start `"igneum-agent`" `"$bat`"") 2>&1 | Out-Null
|
|
Log 'scheduled task IgneumRelayAgent set (runs at logon, highest privileges)'
|
|
} catch { Log ("schtasks failed: " + $_.Exception.Message) }
|
|
try {
|
|
New-Item -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Force | Out-Null
|
|
Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name 'IgneumRelayAgent' -Value ("cmd /c start `"igneum-agent`" `"$bat`"")
|
|
} catch { Log ("RunOnce failed: " + $_.Exception.Message) }
|
|
}
|
|
|
|
function Post-Result($task, [int] $code, [string] $logPath, [string] $note) {
|
|
$tail = ''
|
|
if (Test-Path $logPath) {
|
|
$bytes = [IO.File]::ReadAllBytes($logPath)
|
|
$n = [Math]::Min($bytes.Length, $TailBytes)
|
|
$tail = [Text.Encoding]::UTF8.GetString($bytes, $bytes.Length - $n, $n)
|
|
}
|
|
$o = @{ kind = 'result'; from = $script:Machine; to = 'all'; task_id = $task.id; body = $tail
|
|
title = ($task.title + ": exit " + $code + $(if ($note) { " (" + $note + ")" } else { "" }))
|
|
flags = @{ exit_code = $code; pass = [int]$env:RELAY_PASS; machine = $env:COMPUTERNAME } }
|
|
if ((Test-Path $logPath) -and (Get-Item $logPath).Length -gt $TailBytes) {
|
|
try { $f = & (Join-Path $Here 'send.ps1') -Machine $script:Machine -Kind 'file' -TaskId $task.id -Title ($task.title + ' full log') $logPath 2>&1 | Out-String; Log ("full log posted: " + $f.Trim()) } catch { Log ("log upload failed: " + $_.Exception.Message) }
|
|
}
|
|
try { $r = Api-Post 'drop' $o; Log ("result posted as #" + $r.id) } catch { Log ("result post failed: " + $_.Exception.Message) }
|
|
}
|
|
|
|
function Run-Task($task, [int] $pass) {
|
|
$id = $task.id
|
|
$script = Join-Path $TaskDir ("task-" + $id + ".ps1")
|
|
$wrap = Join-Path $TaskDir ("task-" + $id + ".wrap.ps1")
|
|
$log = Join-Path $LogDir ("task-" + $id + "-pass" + $pass + "-" + (Get-Date -Format 'yyyyMMdd-HHmmss') + ".log")
|
|
$elevated = [bool]$task.flags.elevated
|
|
$rebootContinue = [bool]$task.flags.reboot_continue
|
|
Log ("task #" + $id + " '" + $task.title + "' pass " + $pass + $(if ($elevated) { " elevated" } else { "" }) + $(if ($rebootContinue) { " reboot_continue" } else { "" }))
|
|
$body = "$($task.body)" -replace "`r?`n", "`r`n"
|
|
[IO.File]::WriteAllText($script, $body, (New-Object Text.UTF8Encoding $true))
|
|
$env:RELAY_PASS = "$pass"; $env:RELAY_TASK_ID = "$id"; $env:RELAY_MACHINE = $script:Machine; $env:RELAY_ROLE = $script:Role
|
|
$env:RELAY_SEND = (Join-Path $Here 'send.ps1'); $env:RELAY_HOME = $StateDir
|
|
$wrapBody = @"
|
|
`$ErrorActionPreference = 'Continue'
|
|
`$env:RELAY_PASS = '$pass'; `$env:RELAY_TASK_ID = '$id'; `$env:RELAY_MACHINE = '$($script:Machine)'; `$env:RELAY_ROLE = '$($script:Role)'
|
|
`$env:RELAY_SEND = '$(Join-Path $Here 'send.ps1')'; `$env:RELAY_HOME = '$StateDir'
|
|
Start-Transcript -Path '$log' -Append | Out-Null
|
|
`$code = 0
|
|
try { & '$script'; `$code = `$LASTEXITCODE; if (`$null -eq `$code) { `$code = 0 } } catch { Write-Host ("TASK ERROR: " + `$_.Exception.Message); `$code = 1 }
|
|
Stop-Transcript | Out-Null
|
|
Add-Content -Path '$log' -Value ("__RELAY_EXIT__=" + `$code)
|
|
exit `$code
|
|
"@
|
|
[IO.File]::WriteAllText($wrap, $wrapBody, (New-Object Text.UTF8Encoding $true))
|
|
$args = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$wrap`"")
|
|
$code = 1
|
|
try {
|
|
if ($elevated -and -not (Is-Admin)) {
|
|
Log 'task needs administrator and the agent is not elevated: asking (UAC prompt on this PC)'
|
|
$p = Start-Process powershell.exe -ArgumentList $args -Verb RunAs -Wait -PassThru
|
|
} else {
|
|
$p = Start-Process powershell.exe -ArgumentList $args -NoNewWindow -Wait -PassThru
|
|
}
|
|
$code = $p.ExitCode
|
|
} catch { Log ("could not start the task: " + $_.Exception.Message); Add-Content -Path $log -Value ("AGENT ERROR: " + $_.Exception.Message) }
|
|
$text = ''; if (Test-Path $log) { $text = Get-Content $log -Raw }
|
|
$reboot = $text -match 'RELAY-REBOOT'
|
|
if ($reboot -and $rebootContinue) {
|
|
Log ("task #" + $id + " asked for a reboot and continues after it (pass " + ($pass + 1) + ")")
|
|
Post-Result $task $code $log ("rebooting, resumes as pass " + ($pass + 1))
|
|
Write-State @{ pending = $id; pass = ($pass + 1); title = $task.title }
|
|
Arm-Restart
|
|
& shutdown.exe /r /t 10 /c "Igneum relay: task #$id continues after the restart"
|
|
Log 'restart in 10 s; the agent exits now'
|
|
exit 0
|
|
}
|
|
Post-Result $task $code $log $(if ($reboot) { 'rebooting' } else { '' })
|
|
try { Api-Post 'done' @{ id = $id; exit_code = $code } | Out-Null } catch { Log ("done failed: " + $_.Exception.Message) }
|
|
if ($reboot) {
|
|
Log ("task #" + $id + " asked for a reboot")
|
|
Arm-Restart
|
|
& shutdown.exe /r /t 10 /c "Igneum relay: task #$id asked for a restart"
|
|
exit 0
|
|
}
|
|
}
|
|
|
|
Log ("igneum relay agent on " + $env:COMPUTERNAME + " as " + $env:USERNAME + $(if (Is-Admin) { " (administrator)" } else { " (NOT administrator: elevated tasks will prompt)" }))
|
|
Arm-Restart
|
|
$registered = $false
|
|
while ($true) {
|
|
try {
|
|
if (-not $registered) { Register-Machine; $registered = $true }
|
|
$st = Read-State
|
|
if ($st -and $st.pending) {
|
|
$pending = [long]$st.pending; $pass = [int]$st.pass
|
|
Write-State $null
|
|
try { $it = (Api-Get ("item?id=" + $pending)).item; Run-Task $it $pass } catch { Log ("could not resume task #" + $pending + ": " + $_.Exception.Message) }
|
|
}
|
|
$j = Api-Get ("inbox?machine=" + [Uri]::EscapeDataString($script:Machine) + "&kind=run&ack=1")
|
|
foreach ($t in @($j.items)) { Run-Task $t 1 }
|
|
if (-not $j.items -or $j.items.Count -eq 0) { Write-Host -NoNewline ("`r[" + (Get-Date -Format 'HH:mm:ss') + "] idle as " + $script:Machine + ", next check in " + $PollSeconds + " s ") }
|
|
} catch {
|
|
Log ("loop error: " + $_.Exception.Message)
|
|
$registered = $false
|
|
}
|
|
Start-Sleep -Seconds $PollSeconds
|
|
}
|