From 5a8bad665ce439d8a95fc66375ca1fad553db9ec Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:06:10 +0000 Subject: [PATCH] 0.3.24: a job's install takes the Power Helper's unattended form through the registered helper itself (PC 2, 7 October 2026: the deferred rule left every new right to "the next interactive start", which on a PC with nobody at the desk never comes, so an update by job could never take the S4U form without a click). rights::install, in the deferred branch: when every missing right is one the registered helper can take (HELPER_TAKEABLE = power-helper-task@unattended) it starts the task, waits for the heartbeat, writes " reregister" and waits 20 s for the helper's own "reregister ok" line (the helper re-registers from the running build's script, now S4U + Highest, elevated, no prompt), then records the right in the manifest; anything else stays deferred as before. Playbooks: pc2-s4u-proof.ps1 (unelevated: an S4U probe task started from the job's session, read by stamp file, LastTaskResult and LastRunTime, then unregistered; the real task read only) and pc1-helper-reprobe.ps1 (installed version, heartbeat first, one dev line, the log line within 15 s: answers or not). Test known-failed first: a_job_install_takes_the_task_form_through_the_registered_helper_and_defers_the_rest Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/rights.rs | 80 +++++++++++++++++++++++++- relay/playbooks/pc1-helper-reprobe.ps1 | 33 +++++++++++ relay/playbooks/pc2-s4u-proof.ps1 | 35 +++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 relay/playbooks/pc1-helper-reprobe.ps1 create mode 100644 relay/playbooks/pc2-s4u-proof.ps1 diff --git a/app/igneum-app/src/rights.rs b/app/igneum-app/src/rights.rs index d1db16cf4..af4174058 100644 --- a/app/igneum-app/src/rights.rs +++ b/app/igneum-app/src/rights.rs @@ -248,13 +248,64 @@ pub fn webview2_script(install_dir: &Path) -> String { } /// The installer's step. Compares, asks once when something is missing, writes the manifest. Ok(prompted). +/// The rights the registered Igneum Power Helper can take by itself, elevated and with no prompt, through its `reregister` +/// verb (the helper re-registers its own task from the running build's script: src/powertask.rs). 0.3.24: the task's +/// unattended form is such a right, so an update by job on a PC with nobody at the desk (PC 2, 7 October 2026) takes it +/// without a click, where the deferred rule alone would have left it to "the next interactive start". +pub const HELPER_TAKEABLE: &[&str] = &["power-helper-task@unattended"]; + +pub fn helper_can_take(missing: &[String], task_registered: bool) -> bool { + task_registered && !missing.is_empty() && missing.iter().all(|m| HELPER_TAKEABLE.contains(&m.as_str())) +} + +/// The helper's answer to ` reregister` in its log: Some(true) on "ok", Some(false) on "failed", None while no line. +pub fn reregister_ack(log: &str, seq: u64) -> Option { + let ok = format!(" {seq} reregister ok"); + let failed = format!(" {seq} reregister failed"); + log.lines().rev().find_map(|l| if l.contains(&ok) { Some(true) } else if l.contains(&failed) { Some(false) } else { None }) +} + +/// Takes the helper-takeable rights through the running Power Helper task: heartbeat first, then the reregister line, then +/// the helper's own ok line within 20 s. Windows only (elsewhere the task does not exist). +fn take_through_helper(missing: &[String]) -> Result<(), String> { + if !cfg!(windows) { + return Err("the Power Helper task is Windows only".into()); + } + let dir = crate::powertask::helper_dir(); + let _ = std::fs::create_dir_all(&dir); + crate::powertask::ensure_running(&dir, std::time::Duration::from_secs(12))?; + let seq = crate::powertask::wire_seq() + 1; + std::fs::write(dir.join("cmd.txt"), format!("{seq} reregister\n")).map_err(|e| e.to_string())?; + let until = std::time::Instant::now() + std::time::Duration::from_secs(20); + loop { + std::thread::sleep(std::time::Duration::from_millis(500)); + let log = std::fs::read_to_string(dir.join("helper.log")).unwrap_or_default(); + match reregister_ack(&log, seq) { + Some(true) => return Ok(()), + Some(false) => return Err(format!("the helper's reregister failed (its log names why); {} not taken", missing.join(", "))), + None if std::time::Instant::now() >= until => return Err(format!("the helper did not answer sequence {seq} reregister within 20 s; {} not taken", missing.join(", "))), + None => {} + } + } +} + pub fn install(exe: &Path, install_dir: &Path, data_root: &Path, app_dir: &Path, version: &str) -> Result { let installed = Manifest::load(app_dir); match step(installed.as_ref(), RIGHTS, std::env::var("SESSIONNAME").ok().as_deref(), in_job_env()) { Step::Nothing => return Ok(false), Step::Deferred => { + let miss = missing(installed.as_ref(), RIGHTS); + // 0.3.24: what the registered helper can take itself, it takes now, elevated, with no prompt (PC 2 by job) + if helper_can_take(&miss, cfg!(windows) && crate::powertask::registered()) { + take_through_helper(&miss)?; + let mut rights: Vec = installed.as_ref().map(|m| m.rights.clone()).unwrap_or_default(); + rights.extend(miss.iter().cloned()); + let _ = std::fs::create_dir_all(app_dir); + Manifest { version: version.to_string(), rights, at: crate::platform::unix_now() }.save(app_dir).map_err(|e| format!("cannot write the rights manifest: {e}"))?; + return Ok(true); + } // a job's or a session-less install never raises a prompt (the project lead, 7 October 2026); the next interactive start asks once - return Err(format!("rights: deferred ({} missing: {}); no prompt in a job's or a session-less install, the next interactive start of the app asks once", missing(installed.as_ref(), RIGHTS).len(), missing(installed.as_ref(), RIGHTS).join(", "))); + return Err(format!("rights: deferred ({} missing: {}); no prompt in a job's or a session-less install, the next interactive start of the app asks once", miss.len(), miss.join(", "))); } Step::Asked => {} } @@ -347,6 +398,33 @@ mod tests { assert!(s.contains("webview2: ") && s.contains(" ok (minimum "), "one log line either way"); } + /// Known-failed first (PC 2 by job, 7 October 2026): the deferred rule left every new right to "the next interactive + /// start", which on a PC with nobody at the desk never comes. The task's unattended form is a right the registered + /// helper takes itself through `reregister`, elevated, no prompt; anything else stays deferred. + #[test] + fn a_job_install_takes_the_task_form_through_the_registered_helper_and_defers_the_rest() { + let one = vec!["power-helper-task@unattended".to_string()]; + assert!(helper_can_take(&one, true)); + assert!(!helper_can_take(&one, false), "no task registered: nothing can take it, deferred"); + assert!(!helper_can_take(&[], true), "nothing missing: nothing to take"); + assert!(!helper_can_take(&["power-helper-task@unattended".to_string(), "firewall-node".to_string()], true), "a firewall rule is not the helper's to take"); + assert!(!helper_can_take(&["boot-task".to_string()], true)); + assert_eq!(reregister_ack("1791409581 helper started\n1791409590 427400 reregister ok: the task now runs C:\\x\\igneum-app.exe --power-helper\n", 427400), Some(true)); + assert_eq!(reregister_ack("1791409590 427400 reregister failed: the task now runs \n", 427400), Some(false)); + assert_eq!(reregister_ack("1791409590 427399 reregister ok: ...\n", 427400), None, "another sequence's answer is not this one's"); + assert_eq!(reregister_ack("", 427400), None); + // the install path: the deferred branch tries the helper before it defers, and only for what the helper can take + let s = include_str!("rights.rs"); + let i = s.find("Step::Deferred => {").unwrap(); + let body = &s[i..i + 1500]; + let take = body.find("helper_can_take(&miss").expect("the helper is asked first"); + let defer = body.find("rights: deferred").expect("then the deferral"); + assert!(take < defer); + assert!(body.contains("take_through_helper(&miss)?") && body.contains("rights.extend(miss.iter().cloned())"), "the taken rights go into the manifest"); + // the helper's reregister writes the running build's script: the S4U form (src/powertask.rs register_script) + assert!(crate::powertask::register_script(Path::new("C:\\p\\igneum-app.exe")).contains("-LogonType S4U -RunLevel Highest")); + } + /// 0.3.24: an install that holds every 0.3.23 right lacks exactly the unattended task form; the update asks once. #[test] fn the_unattended_task_form_is_one_new_right_for_a_0_3_23_install() { diff --git a/relay/playbooks/pc1-helper-reprobe.ps1 b/relay/playbooks/pc1-helper-reprobe.ps1 new file mode 100644 index 000000000..17cf4f10e --- /dev/null +++ b/relay/playbooks/pc1-helper-reprobe.ps1 @@ -0,0 +1,33 @@ +# PC 1: does the Igneum Power Helper answer again after the app took the 0.3.23 kit through its own update path? +# (7 October 2026, main's order; the 0.3.20 helper read every same-count rewrite of cmd.txt as present at start, fixed in +# 0.3.21 by effective_skip.) Unelevated, by signed job. The probe is the one command that touches no card: a `dev` line, +# which the helper only logs (" dev "). Order, as 0.3.21+ engines do it: start the task, wait for a fresh +# heartbeat (helper.alive within 4 s), THEN write the line (a line present at the helper's start is never a command), +# and read helper.log for the sequence within 15 s. Reads the installed version first; nothing is quit, paused or resumed. +$ErrorActionPreference = 'Continue' +function Say([string] $s) { Write-Output ('[' + (Get-Date -Format 'HH:mm:ss') + '] ' + $s) } +$sweep = Join-Path $env:LOCALAPPDATA 'igneum\app\sweep' +$cmd = Join-Path $sweep 'cmd.txt' +$hlog = Join-Path $sweep 'helper.log' +$alive = Join-Path $sweep 'helper.alive' +$exe = Join-Path $env:LOCALAPPDATA 'Programs\Igneum Miner\igneum-app.exe' +$ver = if (Test-Path -LiteralPath $exe) { (& $exe --version 2>&1 | Select-Object -First 1) } else { 'missing' } +Say ("installed exe: " + $ver + " (mtime " + (Get-Item -LiteralPath $exe -ErrorAction SilentlyContinue).LastWriteTimeUtc.ToString('o') + ")") +$urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url' +$running = '' +if (Test-Path -LiteralPath $urlFile) { try { $u = (Get-Content -LiteralPath $urlFile -Raw).Trim(); $running = [string](Invoke-RestMethod -Uri ($u + 'api/state') -TimeoutSec 5 -UseBasicParsing).version } catch { } } +Say ("running app answers version: " + $(if ($running) { $running } else { 'nothing' })) +$t = Get-ScheduledTask -TaskName 'Igneum Power Helper' -ErrorAction SilentlyContinue +if (-not $t) { Write-Output 'RESULT helper not_registered'; exit 0 } +Say ("task: " + $t.Actions[0].Execute + " " + $t.Actions[0].Arguments + ", LogonType " + $t.Principal.LogonType + ", state " + $t.State) +function Fresh() { if (-not (Test-Path -LiteralPath $alive)) { return $false }; $v = 0; if ([long]::TryParse((Get-Content -LiteralPath $alive -Raw).Trim(), [ref]$v)) { return (([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $v) -le 4) } else { return $false } } +if (-not (Fresh)) { try { Start-ScheduledTask -TaskName 'Igneum Power Helper' } catch { Say ("Start-ScheduledTask: " + $_) } } +$deadline = (Get-Date).AddSeconds(12) +while (-not (Fresh) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 } +if (-not (Fresh)) { $i = Get-ScheduledTaskInfo -TaskName 'Igneum Power Helper'; Write-Output ('RESULT helper silent no heartbeat within 12 s of the start; LastTaskResult 0x' + ('{0:X8}' -f $i.LastTaskResult) + ' LastRunTime ' + $i.LastRunTime + ' installed ' + $ver); Get-Content -LiteralPath $hlog -Tail 6 -ErrorAction SilentlyContinue | ForEach-Object { Say ('helper.log: ' + $_) }; exit 0 } +$seq = ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() % 999990) + 7 +Set-Content -LiteralPath $cmd -Value ("$seq dev 0") -Encoding ascii +$deadline = (Get-Date).AddSeconds(15) +$hit = $null +while (-not $hit -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500; $hit = Get-Content -LiteralPath $hlog -Tail 20 -ErrorAction SilentlyContinue | Where-Object { $_ -match (" $seq dev 0") } | Select-Object -Last 1 } +if ($hit) { Write-Output ('RESULT helper answers "' + $hit.Trim() + '" installed ' + $ver) } else { Write-Output ('RESULT helper silent heartbeat fresh but no line for sequence ' + $seq + ' within 15 s; installed ' + $ver); Get-Content -LiteralPath $hlog -Tail 6 -ErrorAction SilentlyContinue | ForEach-Object { Say ('helper.log: ' + $_) } } diff --git a/relay/playbooks/pc2-s4u-proof.ps1 b/relay/playbooks/pc2-s4u-proof.ps1 new file mode 100644 index 000000000..bfed8262b --- /dev/null +++ b/relay/playbooks/pc2-s4u-proof.ps1 @@ -0,0 +1,35 @@ +# 0.3.24 Power Helper: the scheduler proof on PC 2, by job, UNELEVATED (7 October 2026, main's order after 0.3.23 take 3). +# Question: does a task registered -LogonType S4U start from a job's session (no interactive logon of its own) and run? +# A probe task in the exact S4U form (the user's own token, no stored password) at RunLevel Limited is registered (the +# only form an unelevated process may register; RunLevel Highest needs the installer's elevated rights step, so the +# Highest half is read from the real Igneum Power Helper's registration below), started with Start-ScheduledTask from +# THIS session, and its run is read three ways: the stamp file its action writes (who ran, in which session), the +# scheduler's LastTaskResult, and LastRunTime. The probe is unregistered at the end. The real task is only READ. +# Nothing of the app is quit, paused or resumed. +$ErrorActionPreference = 'Continue' +function Say([string] $s) { Write-Output ('[' + (Get-Date -Format 'HH:mm:ss') + '] ' + $s) } +$probe = 'Igneum S4U probe' +$stamp = Join-Path $env:LOCALAPPDATA 'igneum\app\sweep\s4u-probe.txt' +Remove-Item -LiteralPath $stamp -Force -ErrorAction SilentlyContinue +Unregister-ScheduledTask -TaskName $probe -Confirm:$false -ErrorAction SilentlyContinue +Say ("this session: " + (Get-Process -Id $PID).SessionId + ", user " + [System.Security.Principal.WindowsIdentity]::GetCurrent().Name) +$cmd = "`$p = (Get-Process -Id `$PID); `"`$(Get-Date -Format o) ran as `$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) session `$(`$p.SessionId) elevated `$(([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))`" | Out-File -LiteralPath '$stamp' -Encoding utf8" +$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($cmd)) +$a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ('-NoProfile -WindowStyle Hidden -EncodedCommand ' + $enc) +$pr = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType S4U -RunLevel Limited +$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 5) -MultipleInstances IgnoreNew -Hidden +try { Register-ScheduledTask -TaskName $probe -Action $a -Principal $pr -Settings $s -Force | Out-Null; Say 'probe registered (S4U, Limited)' } catch { Write-Output ('RESULT s4u register_failed ' + $_); exit 1 } +$t = Get-ScheduledTask -TaskName $probe +Say ("probe principal: LogonType " + $t.Principal.LogonType + ", RunLevel " + $t.Principal.RunLevel + ", user " + $t.Principal.UserId) +try { Start-ScheduledTask -TaskName $probe; Say 'Start-ScheduledTask returned' } catch { Write-Output ('RESULT s4u start_failed ' + $_) } +$deadline = (Get-Date).AddSeconds(30) +while (-not (Test-Path -LiteralPath $stamp) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 500 } +$info = Get-ScheduledTaskInfo -TaskName $probe +Say ("probe LastRunTime " + $info.LastRunTime + ", LastTaskResult " + $info.LastTaskResult + " (0x" + ('{0:X8}' -f $info.LastTaskResult) + "), state " + (Get-ScheduledTask -TaskName $probe).State) +if (Test-Path -LiteralPath $stamp) { Write-Output ('RESULT s4u answers ' + (Get-Content -LiteralPath $stamp -Raw).Trim()) } else { Write-Output ('RESULT s4u silent no stamp within 30 s; LastTaskResult 0x' + ('{0:X8}' -f $info.LastTaskResult)) } +Unregister-ScheduledTask -TaskName $probe -Confirm:$false -ErrorAction SilentlyContinue +Remove-Item -LiteralPath $stamp -Force -ErrorAction SilentlyContinue +# the real task, read only: its form today (0.3.23 registers Interactive + Highest; 0.3.24's rights step makes it S4U + Highest) +$h = Get-ScheduledTask -TaskName 'Igneum Power Helper' -ErrorAction SilentlyContinue +if ($h) { $hi = Get-ScheduledTaskInfo -TaskName 'Igneum Power Helper'; Write-Output ('RESULT helper_task LogonType ' + $h.Principal.LogonType + ' RunLevel ' + $h.Principal.RunLevel + ' action "' + $h.Actions[0].Execute + ' ' + $h.Actions[0].Arguments + '" state ' + $h.State + ' LastTaskResult 0x' + ('{0:X8}' -f $hi.LastTaskResult) + ' LastRunTime ' + $hi.LastRunTime) } else { Write-Output 'RESULT helper_task not_registered' } +Say 'done'