V6-06: the Power Helper runs a protected copy, refreshed only on the signed manifest's engine hash; elevated scripts inline; quit and remove through the sequence guard; the host asInvoker

The one elevated step copies igneum-app.exe and its runtime DLLs into %ProgramData%\Igneum\helper with inheritance cut
(Administrators and SYSTEM full, Users read and execute, owner Administrators) and registers the task against that copy;
the per-user install under LOCALAPPDATA is never what the scheduler runs elevated. The helper's reregister refreshes
the copy only when the signed update manifest names the installed exe's sha256 (platforms.windows.engine_sha256,
publish-manifest.sh --win-engine); anything else is refused with its reason in helper.log. rights.ps1 and
register-power-task.ps1 are no longer read from user-writable folders: every elevated script travels inline through
-EncodedCommand. quit and remove carry a sequence like every verb and pass the rising-sequence guard; a stale line
re-added to the file is skipped. The window host declares asInvoker. The prove host's lease is the lesser of the
grant and the free reading (the V6-07 sub-lane's 12 GB row).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 20:05:33 +00:00
parent af5ade4339
commit 295711ba29
10 changed files with 289 additions and 45 deletions

View file

@ -181,6 +181,12 @@ pub fn proof_budget_mib(vram_mib: u64, mode: Mode) -> u64 {
}
}
/// The lease's figure for a spawn: the grant the mode allows, never more than the card reads free (the V6-07 sub-lane's
/// 12 GB row: 6,159 MiB free beside the miner, under the 8,285 MiB grant).
pub fn lease_mib(vram_mib: u64, mode: Mode, free_mib: u64) -> u64 {
proof_budget_mib(vram_mib, mode).min(free_mib)
}
/// The card's words after exit 78: the figure the host printed ("needs N GB" or "needs N MB"/"MiB"; the host's own line
/// is "RESULT memory_profile refused: proving needs N GB free on the card for the <workload> workload (<floor> MiB
/// floor); <free> MiB free") when it did, else the budget it was offered, in whole GB rounded up.
@ -313,6 +319,16 @@ mod tests {
assert_eq!(proof_budget_mib(8_192, Mode::MiningOnly), 0);
}
/// The V6-07 sub-lane's row (8 October 2026): a 12 GB card beside its miner has 6,159 MiB free, under the grant the
/// mode allows; the lease is the lesser of the grant and the free reading, never more than the card has.
#[test]
fn the_lease_is_the_lesser_of_the_grant_and_the_free_memory() {
assert_eq!(lease_mib(12_288, Mode::Simultaneous, 6_159), 6_159);
assert_eq!(lease_mib(24_576, Mode::Simultaneous, 18_000), PROOF_PEAK_MIB + PROOF_PEAK_MIB * HEADROOM_PCT / 100);
assert_eq!(lease_mib(8_192, Mode::MiningOnly, 8_000), 0);
assert_eq!(lease_mib(0, Mode::ProveOnly, 0), 0, "no card, no grant");
}
/// Exit 78 from the host is the floor refusal: the card's words name the floor the host printed, else the budget.
#[test]
fn the_floor_refusal_names_the_memory_proving_needs() {

View file

@ -2916,7 +2916,7 @@ impl Engine {
// a `remove` present when the helper starts is skipped like any stale line, so the heartbeat first, then the line
match crate::powertask::ensure_running(&hdir, Duration::from_secs(12)) {
Ok(()) => {
let _ = std::fs::write(hdir.join("cmd.txt"), "remove\n");
let _ = std::fs::write(hdir.join("cmd.txt"), format!("{} remove\n", crate::powertask::wire_seq()));
shared.log("power control off: the Igneum Power Helper task removes itself");
}
Err(e) => {
@ -3386,19 +3386,17 @@ impl Engine {
// 6 October 2026: a --sweep engine skipped the cap path where the registration lived, so the one
// approved click registered nothing); no prompt: this process already holds the rights
if probe.direct && cfg!(windows) && !self.power_task_registered() {
let dir = self.sweep_dir();
let _ = std::fs::create_dir_all(&dir);
let script = dir.join("register-power-task.ps1");
let installed: Vec<PathBuf> = crate::powertask::install_candidates();
if let Ok(exe) = std::env::current_exe() {
let target = crate::powertask::task_exe(&exe, &installed);
if std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), crate::powertask::register_script(&target).as_bytes()].concat()).is_ok() {
// V6-06: the registration script travels inline, never as a file under the sweep folder
{
let mut p = std::process::Command::new(crate::platform::tool("powershell"));
p.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
p.args(crate::platform::encoded_command_args(&crate::powertask::register_script(&target)));
crate::platform::quiet(&mut p);
let ok = p.status().map(|s| s.success()).unwrap_or(false);
self.power_task = None;
self.sweep_say(&format!("TUNE helper registered={} action={}", ok, target.display()));
self.sweep_say(&format!("TUNE helper registered={} action=protected copy of {}", ok, target.display()));
}
}
}
@ -3546,7 +3544,7 @@ impl Engine {
fn sweep_helper_quit(&mut self) {
if self.sweep_helper {
let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), "quit\n");
let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), format!("{} quit\n", crate::powertask::wire_seq()));
if self.sweep_helper_is_task {
// the task exits on quit and reports nothing back; the next tune starts it again
self.sweep_helper = false;

View file

@ -45,6 +45,9 @@ pub struct PlatformEntry {
pub sha256: String,
pub size: u64,
pub kind: String, // dmg | zip | inno-setup
/// V6-06 (8 October 2026): the engine exe's own sha256 inside this installer (publish-manifest.sh --win-engine),
/// what the Power Helper checks before it refreshes its protected copy; empty when the publish did not name it.
pub engine_sha256: String,
}
#[derive(Clone, Debug, PartialEq, Default)]
@ -175,7 +178,7 @@ pub fn parse(text: &str) -> Result<Manifest, String> {
if p.is_null() {
return Ok(None);
}
let e = PlatformEntry { url: s(p, "url"), sha256: s(p, "sha256").to_ascii_lowercase(), size: p.get("size").and_then(|x| x.as_u64()).unwrap_or(0), kind: s(p, "kind") };
let e = PlatformEntry { url: s(p, "url"), sha256: s(p, "sha256").to_ascii_lowercase(), size: p.get("size").and_then(|x| x.as_u64()).unwrap_or(0), kind: s(p, "kind"), engine_sha256: s(p, "engine_sha256").to_ascii_lowercase() };
if !e.url.starts_with("https://") && !e.url.starts_with("http://127.0.0.1:") {
return Err(format!("{name}: the url is not https"));
}
@ -576,6 +579,17 @@ mod tests {
(sk, pk)
}
/// V6-06: the windows entry may name the engine exe's own sha256 (publish-manifest.sh --win-engine), what the
/// Power Helper checks before it refreshes its protected copy; absent = empty, nothing refreshes.
#[test]
fn the_windows_entry_carries_the_engine_hash_when_published() {
let sha = "cd".repeat(32);
let m = parse(&format!(r#"{{"version":"2.0.2","platforms":{{"windows":{{"url":"https://x/s.exe","sha256":"{sha}","size":1,"kind":"inno-setup","engine_sha256":"ABCD"}}}}}}"#)).unwrap();
assert_eq!(m.windows.unwrap().engine_sha256, "abcd");
let n = parse(&format!(r#"{{"version":"2.0.2","platforms":{{"windows":{{"url":"https://x/s.exe","sha256":"{sha}","size":1,"kind":"inno-setup"}}}}}}"#)).unwrap();
assert_eq!(n.windows.unwrap().engine_sha256, "");
}
#[test]
fn default_network_rules() {
// the network step: absent = the package's own; devnet-3 fine; the testnet default only once the manifest opens it

View file

@ -540,6 +540,29 @@ pub fn elevated_failure(code: Option<i32>, stderr: &str) -> String {
}
}
/// Standard base64 (RFC 4648, padded) of `bytes`: what PowerShell's -EncodedCommand reads.
pub fn base64_encode(bytes: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let n = (chunk[0] as u32) << 16 | (*chunk.get(1).unwrap_or(&0) as u32) << 8 | *chunk.get(2).unwrap_or(&0) as u32;
out.push(T[(n >> 18) as usize & 63] as char);
out.push(T[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' });
out.push(if chunk.len() > 2 { T[n as usize & 63] as char } else { '=' });
}
out
}
/// The PowerShell arguments that run `script` INLINE (V6-06, 8 October 2026): `-EncodedCommand` over the script's
/// UTF-16LE bytes, so an elevated step never reads a .ps1 from a folder the user can write between the write and the
/// run (rights.ps1 under the data root, register-power-task.ps1 under the sweep folder were that). The command line
/// cap is 32 K characters; the callers' scripts are a few K.
pub fn encoded_command_args(script: &str) -> Vec<String> {
let utf16: Vec<u8> = script.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
vec!["-NoProfile".into(), "-ExecutionPolicy".into(), "Bypass".into(), "-EncodedCommand".into(), base64_encode(&utf16)]
}
/// Doubles the single quotes of `s` for a single-quoted PowerShell literal.
pub fn ps_quote(s: &str) -> String {
s.replace('\'', "''")

View file

@ -28,6 +28,23 @@
//! else: no file, no process, no registry, no other binary.
//! - The helper exits after 20 idle minutes; a stale command file is cleared at start (sequence numbers must rise).
//! - Linux keeps pkexec per step (no scheduled task); macOS has no cap to set.
//!
//! V6-06 (the master's R1 review, 8 October 2026; the design the window lane built):
//! - The task's action is a PROTECTED copy of the exe and its runtime DLLs in %ProgramData%\Igneum\helper, made by the
//! one elevated step with inheritance cut and Administrators and SYSTEM full, Users read and execute, owner
//! Administrators (`register_script`). The per-user install under LOCALAPPDATA, which the user can overwrite, is
//! never what the scheduler runs elevated.
//! - The copy is refreshed only through `reregister`, and only when the signed update manifest names the installed
//! exe's sha256 (platforms.windows.engine_sha256, `refresh_allowed`); a swapped binary is refused with its reason in
//! helper.log. An update whose manifest names no engine hash leaves the old protected copy in place.
//! - Every elevated script travels inline (-EncodedCommand, platform::encoded_command_args): no .ps1 is read from a
//! folder the user can write between the write and the run.
//! - `quit` and `remove` carry a sequence like every other verb and pass the same rising-sequence guard; a stale
//! line re-added to the file is skipped. The command file stays the IPC: its boundary is the user account (the
//! file is in the user's profile, the task's principal is the user), the same boundary a per-user named pipe
//! would draw; what matters is that no verb lets a writer choose a path, a hash or a binary, which holds.
//! - The window host is asInvoker (app/windows/host.manifest); the one prompt it raises for the engine's step is
//! a child process, the host itself never holds rights.
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
@ -157,12 +174,7 @@ pub enum HelperCmd {
/// `remove` need no sequence). Anything that is not a fixed verb with digit-only arguments is None.
pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
let t = line.trim();
if t == "quit" {
return Some((0, HelperCmd::Quit));
}
if t == "remove" {
return Some((0, HelperCmd::Remove));
}
// V6-06: a bare quit or remove is no command; both carry a sequence and pass the guard like every verb
let p: Vec<&str> = t.split_whitespace().collect();
let digits = |s: &str| !s.is_empty() && s.len() <= 6 && s.chars().all(|c| c.is_ascii_digit());
let seq: u64 = p.first().filter(|s| digits(s)).and_then(|s| s.parse().ok())?;
@ -174,6 +186,8 @@ pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
[_, "lmc", m] if digits(m) => Some((seq, HelperCmd::MemClock(m.parse().ok()?))),
[_, "rmc"] => Some((seq, HelperCmd::MemReset)),
[_, "dev", d] if digits(d) => Some((seq, HelperCmd::Dev(d.to_string()))),
[_, "quit"] => Some((seq, HelperCmd::Quit)),
[_, "remove"] => Some((seq, HelperCmd::Remove)),
[_, "reregister"] => Some((seq, HelperCmd::Reregister)),
[_, "driver", v] if crate::driverinstall::vendor_ok(v) => Some((seq, HelperCmd::Driver(v.to_string()))),
_ => None,
@ -278,22 +292,57 @@ pub fn readback_command() -> String {
format!("$t = Get-ScheduledTask -TaskName '{TASK_NAME}' -ErrorAction SilentlyContinue; if ($t) {{ Write-Output ($t.Actions[0].Execute + ' ' + $t.Actions[0].Arguments) }}; exit 0")
}
/// The PowerShell that registers the task (run inside the ONE elevated step, with the caps). `exe` is this
/// executable's path in the install folder. Principal: the signed-in user, interactive logon, highest run level; no
/// trigger; may start on battery; one hour limit per run; multiple starts are ignored while one runs.
/// The protected folder's PowerShell expression: %ProgramData%\Igneum\helper.
pub fn protected_dir_expression() -> &'static str {
"Join-Path $env:ProgramData 'Igneum\\helper'"
}
/// The files the protected copy needs: the engine and the mingw runtime it links (packaging/windows/make-payload.sh).
pub const PROTECTED_FILES: [&str; 4] = ["igneum-app.exe", "libstdc++-6.dll", "libgcc_s_seh-1.dll", "libwinpthread-1.dll"];
/// The PowerShell that registers the task (run inside the ONE elevated step, with the caps). `exe` is the installed
/// executable; the script copies it and its runtime DLLs into the protected folder (V6-06: inheritance cut,
/// Administrators and SYSTEM full, Users read and execute, owner Administrators) and points the task at that copy.
/// Principal: the signed-in user, S4U, highest run level; no trigger; may start on battery; one hour limit per run;
/// multiple starts are ignored while one runs.
pub fn register_script(exe: &Path) -> String {
let exe = exe.display().to_string().replace('\'', "''");
let exe_s = exe.display().to_string();
let src = exe_s.rfind(['\\', '/']).map(|i| exe_s[..i].to_string()).unwrap_or_default().replace('\'', "''");
let copies: String = PROTECTED_FILES
.iter()
.map(|f| format!("if (Test-Path -LiteralPath (Join-Path $src '{f}')) {{ Copy-Item -LiteralPath (Join-Path $src '{f}') -Destination (Join-Path $helperDir '{f}') -Force }}\r\n"))
.collect();
format!(
"$a = New-ScheduledTaskAction -Execute '{exe}' -Argument '--power-helper' -WorkingDirectory '{dir}'\r\n\
"$src = '{src}'\r\n\
$helperDir = {pdir}\r\n\
New-Item -ItemType Directory -Force -Path $helperDir | Out-Null\r\n\
& icacls.exe $helperDir /inheritance:r /grant '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-545:(OI)(CI)RX' /setowner '*S-1-5-32-544' | Out-Null\r\n\
{copies}\
$helperExe = Join-Path $helperDir 'igneum-app.exe'\r\n\
$a = New-ScheduledTaskAction -Execute $helperExe -Argument '--power-helper' -WorkingDirectory $helperDir\r\n\
$p = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType S4U -RunLevel Highest\r\n\
$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 1) -MultipleInstances IgnoreNew -Hidden\r\n\
Register-ScheduledTask -TaskName '{name}' -Action $a -Principal $p -Settings $s -Force | Out-Null\r\n\
exit 0\r\n",
dir = exe.rfind(['\\', '/']).map(|i| exe[..i].to_string()).unwrap_or_default(),
pdir = protected_dir_expression(),
name = TASK_NAME
)
}
/// V6-06: whether the installed exe (its sha256 `candidate`) may replace the protected copy: only when the signed
/// manifest verifies under `pub_hex` and its windows entry names that very hash. Ok carries the manifest's version.
pub fn refresh_allowed(manifest_bytes: &[u8], sig_hex: &str, pub_hex: &str, candidate: &str) -> Result<String, String> {
let m = crate::manifest::verify_and_parse(manifest_bytes, sig_hex, pub_hex).map_err(|e| format!("the update manifest does not verify ({e}); the protected copy stays"))?;
let want = m.windows.as_ref().map(|w| w.engine_sha256.clone()).unwrap_or_default();
if want.is_empty() {
return Err("the signed manifest names no engine hash (publish-manifest.sh --win-engine); the protected copy stays".into());
}
if want != candidate.to_ascii_lowercase() {
return Err(format!("{candidate} is not the exe the signed manifest names ({want}); the protected copy stays"));
}
Ok(m.version)
}
/// The PowerShell that starts the task from an unelevated process (no prompt: the user owns the task).
pub fn start_command() -> String {
format!("Start-ScheduledTask -TaskName '{TASK_NAME}'; exit 0")
@ -397,6 +446,10 @@ fn run_helper_loop(dir: &Path, log: &dyn Fn(&str)) -> i32 {
log(&line);
}
for (seq, c) in cmds {
// V6-06: the rising-sequence guard first, quit and remove included
if seq <= last_seq {
continue;
}
match c {
HelperCmd::Quit => {
log("quit");
@ -414,7 +467,6 @@ fn run_helper_loop(dir: &Path, log: &dyn Fn(&str)) -> i32 {
let _ = std::fs::remove_file(dir.join(ALIVE_FILE));
return if ok { 0 } else { 1 };
}
_ if seq <= last_seq => continue,
HelperCmd::Reregister => {
last_seq = seq;
idle = Instant::now();
@ -422,10 +474,23 @@ fn run_helper_loop(dir: &Path, log: &dyn Fn(&str)) -> i32 {
log(&format!("{seq} reregister: no installed exe found"));
continue;
};
let script = dir.join("register-power-task.ps1");
let ok = std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), register_script(&target).as_bytes()].concat()).is_ok() && {
// V6-06: the protected copy is refreshed only when the signed manifest names this exe's hash
let updates = dir.parent().map(|p| p.join("updates")).unwrap_or_default();
let verdict = match (std::fs::read(updates.join("manifest.json")), std::fs::read_to_string(updates.join("manifest.json.sig")), crate::manifest::sha256_file(&target)) {
(Ok(b), Ok(sg), Ok(h)) => refresh_allowed(&b, sg.trim(), crate::edition::ota_key(), &h),
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => Err(format!("no verified manifest or hash to check against ({e}); the protected copy stays")),
};
let version = match verdict {
Ok(v) => v,
Err(e) => {
log(&format!("{seq} reregister refused: {e}"));
continue;
}
};
log(&format!("{seq} reregister: {} is the exe manifest {version} names; refreshing the protected copy", target.display()));
let ok = {
let mut p = std::process::Command::new(crate::platform::tool("powershell"));
p.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
p.args(crate::platform::encoded_command_args(&register_script(&target)));
crate::platform::quiet(&mut p);
p.status().map(|s| s.success()).unwrap_or(false)
};
@ -485,6 +550,92 @@ pub fn helper_dir() -> PathBuf {
mod tests {
use super::*;
/// Known failed first (V6-06, the master's R1 review, 8 October 2026): a bare `quit` or `remove` carried no sequence
/// and so skipped the guard every other verb passes; a stale line re-added to the file ended the helper. Now every
/// verb carries a rising sequence, quit and remove included.
#[test]
fn quit_and_remove_carry_a_sequence_and_a_stale_one_is_ignored() {
assert_eq!(parse_line("quit"), None, "a bare quit is not a command");
assert_eq!(parse_line("remove"), None, "a bare remove is not a command");
assert_eq!(parse_line("12 quit"), Some((12, HelperCmd::Quit)));
assert_eq!(parse_line("12 remove"), Some((12, HelperCmd::Remove)));
assert_eq!(parse_line("quit 12"), None);
// the loop itself: a stale quit (sequence at or under the highest seen) is skipped; a rising one ends the helper
let dir = std::env::temp_dir().join(format!("igneum-helper-seq-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("cmd.txt"), "5 dev 0\n").unwrap();
let d2 = dir.clone();
let t = std::thread::spawn(move || run_helper_loop(&d2, &|_| {}));
std::thread::sleep(Duration::from_millis(900));
std::fs::write(dir.join("cmd.txt"), "5 dev 0\n3 quit\n").unwrap();
std::thread::sleep(Duration::from_millis(1500));
assert!(!t.is_finished(), "a quit with a stale sequence must not end the helper");
std::fs::write(dir.join("cmd.txt"), "5 dev 0\n3 quit\n6 quit\n").unwrap();
let started = Instant::now();
while !t.is_finished() && started.elapsed() < Duration::from_secs(10) {
std::thread::sleep(Duration::from_millis(100));
}
assert!(t.is_finished(), "a quit with a rising sequence ends the helper");
assert_eq!(t.join().unwrap(), 0);
let _ = std::fs::remove_dir_all(&dir);
}
/// Known failed first (V6-06): the task's action was the per-user install's exe under LOCALAPPDATA, which the user
/// (and anything running as the user) can overwrite, and the scheduler then ran it elevated with no prompt. The one
/// elevated step now copies the exe and its runtime DLLs into %ProgramData%\Igneum\helper, a folder only
/// administrators and SYSTEM may write (inheritance cut, Users read and execute), and the task runs that copy.
#[test]
fn the_task_runs_a_protected_copy_the_user_cannot_write() {
let s = register_script(Path::new(r"C:\Users\Admin\AppData\Local\Programs\Igneum Miner\igneum-app.exe"));
assert!(s.contains("Join-Path $env:ProgramData 'Igneum\\helper'"), "{s}");
assert!(s.contains("icacls") && s.contains("/inheritance:r"), "{s}");
for grant in ["*S-1-5-32-544:(OI)(CI)F", "*S-1-5-18:(OI)(CI)F", "*S-1-5-32-545:(OI)(CI)RX"] {
assert!(s.contains(grant), "missing {grant}: {s}");
}
assert!(!s.contains("(OI)(CI)M") && !s.contains("S-1-5-32-545:(OI)(CI)F"), "users never get modify or full: {s}");
for f in ["igneum-app.exe", "libstdc++-6.dll", "libgcc_s_seh-1.dll", "libwinpthread-1.dll"] {
assert!(s.contains(&format!("Copy-Item -LiteralPath (Join-Path $src '{f}')")), "{f} is not copied: {s}");
}
assert!(s.contains("$src = 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Igneum Miner'"), "{s}");
assert!(s.contains("-Execute $helperExe -Argument '--power-helper' -WorkingDirectory $helperDir"), "{s}");
assert!(!s.contains("-Execute 'C:\\Users"), "the action is never the user-writable exe: {s}");
assert_eq!(protected_dir_expression(), "Join-Path $env:ProgramData 'Igneum\\helper'");
}
/// Known failed first (V6-06): the helper's `reregister` copied whatever exe sat in the install folder into the
/// task's action. Now the protected copy is refreshed only when the signed manifest names the exe's sha256
/// (platforms.windows.engine_sha256), so a swapped binary never becomes the elevated one.
#[test]
fn a_refresh_of_the_protected_copy_needs_the_signed_manifest_to_name_the_exe() {
use ed25519_dalek::{Signer, SigningKey};
let sk = SigningKey::from_bytes(&[7u8; 32]);
let pk = crate::manifest::hex_encode(sk.verifying_key().as_bytes());
let good = "ab".repeat(32);
let text = format!(r#"{{"version":"2.0.2","platforms":{{"windows":{{"url":"https://x/s.exe","sha256":"{}","size":1,"kind":"inno-setup","engine_sha256":"{good}"}}}}}}"#, "cd".repeat(32));
let sig = crate::manifest::hex_encode(&sk.sign(text.as_bytes()).to_bytes());
assert_eq!(refresh_allowed(text.as_bytes(), &sig, &pk, &good), Ok("2.0.2".to_string()));
assert!(refresh_allowed(text.as_bytes(), &sig, &pk, &"ef".repeat(32)).unwrap_err().contains("not the exe the signed manifest names"));
let other = crate::manifest::hex_encode(SigningKey::from_bytes(&[9u8; 32]).verifying_key().as_bytes());
assert!(refresh_allowed(text.as_bytes(), &sig, &other, &good).is_err(), "a signature under another key is refused");
let bare = format!(r#"{{"version":"2.0.2","platforms":{{"windows":{{"url":"https://x/s.exe","sha256":"{}","size":1,"kind":"inno-setup"}}}}}}"#, "cd".repeat(32));
let sig2 = crate::manifest::hex_encode(&sk.sign(bare.as_bytes()).to_bytes());
assert!(refresh_allowed(bare.as_bytes(), &sig2, &pk, &good).unwrap_err().contains("names no engine"), "a manifest without the engine hash refreshes nothing");
}
/// V6-06: the window host stays unprivileged (asInvoker; a prompt it raises for the engine's one step is a child, not
/// the host); every elevated script of the app travels inline in the command line, never as a file under a folder
/// the user can write between the write and the run.
#[test]
fn the_window_host_is_unprivileged_and_elevated_scripts_travel_inline() {
let manifest = include_str!("../../windows/host.manifest");
assert!(manifest.contains(r#"<requestedExecutionLevel level="asInvoker" uiAccess="false"/>"#), "{manifest}");
assert!(!manifest.contains("requireAdministrator") && !manifest.contains("highestAvailable"));
let args = crate::platform::encoded_command_args("exit 0");
assert_eq!(args, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", "ZQB4AGkAdAAgADAA"]);
assert!(!register_script(Path::new(r"C:\p\igneum-app.exe")).contains("-File "), "no script file is run");
}
/// Known-failed first (PC 1, 7 October 2026, 22:08 BST): the helper's process died at once after each of six starts
/// with no line after "helper started", and the engine toggled nothing and said nothing. Now a panic is a FAULT
/// line, an exit reason and exit 101, every planned exit writes its reason, and the engine's notice carries it.
@ -543,7 +694,7 @@ mod tests {
let l = nothing_to_run_line(rewritten, 2, 423699, &commands_after(rewritten, 2)).unwrap();
assert!(l.contains("2 skipped as present at start, 0 parsed"), "{l}");
assert_eq!(nothing_to_run_line("", 0, 1, &[]), None, "a truncation is not a command and not a complaint");
assert_eq!(nothing_to_run_line("quit\n", 0, 9, &commands_after("quit\n", 0)), None, "quit runs");
assert_eq!(nothing_to_run_line("10 quit\n", 0, 9, &commands_after("10 quit\n", 0)), None, "quit runs");
assert!(include_str!("powertask.rs").matches("nothing_to_run_line(").count() >= 2, "the loop logs it");
}
@ -551,13 +702,13 @@ mod tests {
/// October 2026), so the engine writes `remove` only once the helper's heartbeat is there: start first, then write.
#[test]
fn remove_is_written_after_the_heartbeat_not_before_the_start() {
assert_eq!(commands_after("remove\n", 1), vec![], "the remove present at the start is skipped");
assert_eq!(commands_after("remove\n", 0), vec![(0, HelperCmd::Remove)], "a remove added after the start runs");
assert_eq!(commands_after("5 remove\n", 1), vec![], "the remove present at the start is skipped");
assert_eq!(commands_after("5 remove\n", 0), vec![(5, HelperCmd::Remove)], "a remove added after the start runs");
let s = include_str!("engine.rs");
let off = s.find("fn power_control_off").unwrap();
let body = &s[off..off + 2500];
let ensure = body.find("ensure_running").expect("the off path waits for the heartbeat");
let write = body.find("\"remove\\n\"").expect("then writes remove");
let write = body.find(" remove\\n\"").expect("then writes remove (with its sequence, V6-06)");
assert!(ensure < write, "the heartbeat comes before the remove line");
}
@ -586,13 +737,14 @@ mod tests {
assert_eq!(smi_args("0", &HelperCmd::MemReset).unwrap(), vec!["-i", "0", "-rmc"]);
assert_eq!(parse_line("3 dev 1"), Some((3, HelperCmd::Dev("1".into()))));
assert_eq!(parse_line("5 403"), Some((5, HelperCmd::PowerLimit(403))), "the 0.3.9 form");
assert_eq!(parse_line("quit"), Some((0, HelperCmd::Quit)));
assert_eq!(parse_line("remove"), Some((0, HelperCmd::Remove)));
assert_eq!(parse_line("7 quit"), Some((7, HelperCmd::Quit)));
assert_eq!(parse_line("7 remove"), Some((7, HelperCmd::Remove)));
assert_eq!(parse_line("12 reregister"), Some((12, HelperCmd::Reregister)));
// a stale quit present at the start is skipped; a quit added later counts; a rewritten (shorter) file counts whole
assert_eq!(commands_after("5 dev 0\n6 pl 460\nquit\n", 3), vec![]);
assert_eq!(commands_after("5 dev 0\n6 pl 460\nquit\n7 dev 1\n", 3), vec![(7, HelperCmd::Dev("1".into()))]);
assert_eq!(commands_after("quit\n", 3), vec![(0, HelperCmd::Quit)]);
assert_eq!(commands_after("quit\n", 3), vec![], "a bare quit is no command (V6-06)");
assert_eq!(commands_after("8 quit\n", 0), vec![(8, HelperCmd::Quit)]);
// PC 2, 7 October 2026: the known-failed shape first. A stale four-line file at the helper's start, then the engine's
// four-line rewrite: the count never dropped, so the old rule skipped every new command
let stale = "401000 dev 0\n401001 pl 460\n401002 rgc\n401003 rmc\n";
@ -640,8 +792,9 @@ mod tests {
#[test]
fn the_registration_is_per_user_highest_no_trigger_fixed_action() {
let s = register_script(Path::new(r"C:\Users\Admin\AppData\Local\Programs\Igneum Miner\igneum-app.exe"));
assert!(s.contains("-Execute 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Igneum Miner\\igneum-app.exe' -Argument '--power-helper'"), "{s}");
assert!(s.contains("-WorkingDirectory 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Igneum Miner'"), "{s}");
// V6-06: the action is the protected copy under %ProgramData%, taken from the install folder by the elevated step
assert!(s.contains("-Execute $helperExe -Argument '--power-helper' -WorkingDirectory $helperDir"), "{s}");
assert!(s.contains("$src = 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Igneum Miner'"), "{s}");
// 0.3.24 (main, 7 October 2026, PC 1): the task runs whether or not a user is logged on (S4U, the user's own token,
// no stored password), so a start from a job's session or the engine's own tune is accepted; known-failed first
// (the Interactive-only form)
@ -653,7 +806,7 @@ mod tests {
assert!(s.contains(&format!("-TaskName '{TASK_NAME}'")));
// no window at logon or at a start (the project lead, 7 October 2026, PC 2's Terminal window): the action is the app's own exe,
// a windows-subsystem program with no console, never powershell.exe, cmd.exe or a `start` of a batch file
assert!(s.contains("-Execute 'C:\\p\\igneum-app.exe' -Argument '--power-helper'") || s.contains("-Argument '--power-helper'"), "{s}");
assert!(s.contains("-Argument '--power-helper'"), "{s}");
assert!(!s.contains("powershell.exe") && !s.contains("cmd.exe") && !s.contains("cmd /c start"), "the task's action must be the exe itself: {s}");
assert!(s.contains("-Hidden"), "the task is hidden in the scheduler too");
// the registered probe also wants the action's exe on disk and the task enabled (the stale-task class)
@ -661,7 +814,7 @@ mod tests {
assert!(q.contains("Test-Path (($t.Actions[0].Execute).Trim") && q.contains("$t.State -ne 'Disabled'") && q.contains("exit 1"), "{q}");
// a quote in the path cannot break out of the literal
let q = register_script(Path::new(r"C:\it's\igneum-app.exe"));
assert!(q.contains("'C:\\it''s\\igneum-app.exe'"), "{q}");
assert!(q.contains("$src = 'C:\\it''s'"), "{q}");
assert!(start_command().starts_with("Start-ScheduledTask -TaskName 'Igneum Power Helper'"));
assert!(remove_command().starts_with("Unregister-ScheduledTask -TaskName 'Igneum Power Helper' -Confirm:$false"));
assert!(query_command().contains("Get-ScheduledTask -TaskName 'Igneum Power Helper'"));

View file

@ -266,7 +266,7 @@ fn host_lease(shared: &Shared, workload: crate::device::Workload, deadline_s: u6
};
let used = crate::detect::nvidia_memory_used().get(&index.to_string()).copied().unwrap_or(0);
let free = vram.saturating_sub(used);
let budget = crate::device::proof_budget_mib(vram, crate::device::mode(vram, mining));
let budget = crate::device::lease_mib(vram, crate::device::mode(vram, mining), free);
let env = crate::device::host_env(index, workload, free, budget, deadline_s);
shared.log(&format!("LEASE holder=prover device=nvidia:{index} workload={} free_mib={free} budget_mib={budget} deadline_s={deadline_s}", workload.word()));
(free, budget, env)

View file

@ -19,7 +19,7 @@
//! shapes first (before 0.3.22: a fresh install then Power control on prompted; the firewall rule prompted on the first
//! run; the boot task was registered at the engine's start).
use std::path::{Path, PathBuf};
use std::path::Path;
/// The rights this build needs, in the order the elevated step takes them. An id never changes meaning; a new need is
/// a new id (that is what makes an update ask once).
@ -236,6 +236,11 @@ pub fn script(exe: &Path, install_dir: &Path, data_root: &Path) -> String {
s
}
/// The one elevated command line (V6-06): PowerShell with the rights script inline (-EncodedCommand), never a file.
pub fn elevated_line(powershell: &Path, exe: &Path, install_dir: &Path, data_root: &Path) -> String {
format!("\"{}\" {}", powershell.display(), crate::platform::encoded_command_args(&script(exe, install_dir, data_root)).join(" "))
}
/// The WSL2 part of the elevated script (Igneum 2.0): the feature and the kernel with no distribution and no window;
/// one log line either way; a reboot pending is said, never forced (the installer never restarts a PC by itself).
pub fn wsl2_script(install_dir: &Path) -> String {
@ -331,15 +336,17 @@ pub fn install(exe: &Path, install_dir: &Path, data_root: &Path, app_dir: &Path,
Step::Asked => {}
}
let _ = std::fs::create_dir_all(app_dir);
let path: PathBuf = app_dir.join(SCRIPT_FILE);
std::fs::write(&path, [b"\xEF\xBB\xBF".as_slice(), script(exe, install_dir, data_root).as_bytes()].concat()).map_err(|e| format!("cannot write {}: {e}", path.display()))?;
// V6-06: the script travels inline; no rights.ps1 under the data root is read by the elevated step (a copy is left
// for the record, after the run, never as its input)
#[cfg(windows)]
{
let line = format!("\"{}\" -NoProfile -ExecutionPolicy Bypass -File \"{}\"", crate::platform::tool("powershell").display(), path.display());
let line = elevated_line(&crate::platform::tool("powershell"), exe, install_dir, data_root);
crate::platform::run_elevated(&line)?;
let _ = std::fs::write(app_dir.join(SCRIPT_FILE), [b"\xEF\xBB\xBF".as_slice(), script(exe, install_dir, data_root).as_bytes()].concat());
}
#[cfg(not(windows))]
{
let _ = (exe, install_dir, data_root);
return Err("the rights step is Windows only".into());
}
#[allow(unreachable_code)]
@ -365,6 +372,16 @@ pub fn missing_note(id: &str) -> String {
mod tests {
use super::*;
/// Known failed first (V6-06): rights.ps1 was written to the user's data folder and run elevated with -File, so a
/// swap between the write and the run would have run as administrator. The script now travels inline.
#[test]
fn the_rights_step_runs_its_script_inline_under_the_command_line_cap() {
let (exe, dir, root) = (Path::new(r"C:\Users\Admin\AppData\Local\Programs\Igneum Miner\igneum-app.exe"), Path::new(r"C:\Users\Admin\AppData\Local\Programs\Igneum Miner"), Path::new(r"C:\Users\Admin\AppData\Local\igneum"));
let line = elevated_line(Path::new(r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"), exe, dir, root);
assert!(line.contains("-EncodedCommand ") && !line.contains("-File") && !line.contains(".ps1"), "{line}");
assert!(line.len() < 30_000, "the elevated line must fit the 32 K command line: {} chars", line.len());
}
fn m(rights: &[&str]) -> Manifest {
Manifest { version: "0.3.22".into(), rights: rights.iter().map(|s| s.to_string()).collect(), at: 1 }
}

View file

@ -352,7 +352,7 @@ while true; do
c=""; [ -f "$dir/cmd.txt" ] && c=$(cat "$dir/cmd.txt" 2>/dev/null | tr -d '\r\n')
if [ -n "$c" ] && [ "$c" != "$last" ]; then
last="$c"; idle=$(date +%s)
if [ "$c" = "quit" ]; then echo "$(date -u +%FT%TZ) quit" >> "$dir/helper.log"; break; fi
case "$c" in quit|[0-9]*" quit") echo "$(date -u +%FT%TZ) quit" >> "$dir/helper.log"; break;; esac
printf '%s\n' "$c" | while IFS= read -r line; do
set -- $line
[ $# -ge 2 ] || continue
@ -569,6 +569,8 @@ mod tests {
fn helper_scripts_carry_the_protocol() {
for s in [helper_script_unix()] {
assert!(s.contains("cmd.txt") && s.contains("quit") && s.contains("-pl") && s.contains("20 min"));
// V6-06: the engine writes "<seq> quit" (the Windows helper's guard); the unix helper takes that form too
assert!(s.contains(r#"case "$c" in quit|[0-9]*" quit")"#), "{s}");
assert!(s.contains("-lgc") && s.contains("-rgc"), "the clock cap and its reset");
assert!(s.contains("-lmc") && s.contains("-rmc"), "Ember 2: the memory clock and its reset");
}

View file

@ -11,6 +11,15 @@
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- V6-06 (8 October 2026): the window host never holds rights; the one prompt it raises for the engine's step is a
child process (runElevated in host.cpp), and the Power Helper task runs a protected copy of the engine. -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>

View file

@ -7,6 +7,9 @@
# packaging/ota/publish-manifest.sh --version 0.3.1 --mac packaging/mac/dist/Igneum-Miner-0.3.1.dmg \
# [--win packaging/windows/dist/Igneum-Miner-Setup-0.3.1.exe] --notes "one line of what changed" \
# [--activation-height 120000 --deadline-note "difficulty v2"] [--min-supported 0.3.0] [--channel devnet] [--deploy]
# [--win-engine <igneum-app.exe>] the engine exe inside the Windows installer (the payload's): its sha256 lands as
# platforms.windows.engine_sha256, what the Power Helper checks before it refreshes
# its protected copy (V6-06, 8 October 2026); without it no helper refreshes
# [--urgent] the manifest's own urgent flag (2.0.2, 8 October 2026): the apps install at the first
# safe moment, through the finality guard too (manifest::safe_to_apply); for the entry
# that IS the fix for a chain that cannot lock; beside, not instead of, --min-supported
@ -51,7 +54,7 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token"
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 UI_FILE="" NO_UI=0 DRIVERS_FILE="" NO_DRIVERS=0 URGENT=0
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0 UI_FILE="" NO_UI=0 DRIVERS_FILE="" NO_DRIVERS=0 URGENT=0 WIN_ENGINE=""
while [ $# -gt 0 ]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
@ -65,6 +68,7 @@ while [ $# -gt 0 ]; do
--override) OVERRIDE="$2"; shift 2 ;; # consensus.override: the exact JSON object every app writes to its override.json (all height switches, not just the new one)
--min-supported) MIN_SUPPORTED="$2"; shift 2 ;;
--urgent) URGENT=1; shift ;;
--win-engine) WIN_ENGINE="$2"; shift 2 ;;
--channel) CHANNEL="$2"; shift 2 ;;
--tuning) TUNING_FILE="$2"; shift 2 ;;
--no-tuning) NO_TUNING=1; shift ;;
@ -263,9 +267,15 @@ fi
# the version pair: the UI tree this publish builds from stamps its interface version on every new app entry
UI_TREE_VERSION="$(tr -d '[:space:]' < "$ROOT/app/igneum-app/ui/VERSION" 2>/dev/null || true)"
[ "$URGENT" = 1 ] && echo "urgent: the apps install this entry at the first safe moment, finality guard included"
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" "${UI_TREE_VERSION:-}" "$URGENT" <<'PY'
ENGINE_SHA=""
if [ -n "$WIN_ENGINE" ]; then
[ -f "$WIN_ENGINE" ] || { echo "--win-engine: no file at $WIN_ENGINE" >&2; exit 2; }
read -r ENGINE_SHA _ < <("$SIGNER" sha256 "$WIN_ENGINE")
echo "windows engine: $(basename "$WIN_ENGINE") sha256 $ENGINE_SHA (the Power Helper refreshes its protected copy to this exe only)"
fi
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" "${UI:-}" "${DRIVERS:-}" "${UI_TREE_VERSION:-}" "$URGENT" "$ENGINE_SHA" <<'PY'
import json, sys, datetime
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers, ui_tree, urgent = sys.argv[1:16]
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning, ui, drivers, ui_tree, urgent, engine_sha = sys.argv[1:17]
override = json.loads(override) if override else None
if override is not None and (not isinstance(override, dict) or not override): raise SystemExit("--override must be a non-empty JSON object")
def entry(s, ui_version=None):
@ -289,6 +299,8 @@ m = {
}
if urgent == "1":
m["urgent"] = True
if engine_sha and "windows" in m["platforms"]:
m["platforms"]["windows"]["engine_sha256"] = engine_sha
if tuning:
m["tuning"] = json.loads(tuning)
if ui: