diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 790e8c2d5..bd690bdfa 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -596,6 +596,10 @@ pub struct Engine { quit_source: &'static str, /// the over-the-air updater never runs: IGNEUM_APP_NO_OTA=1 or --sweep (a second engine beside the installed app) no_ota: bool, + /// the Igneum Power Helper task is registered (src/powertask.rs): once, ever; None = not asked yet + power_task: Option, + /// the running tune helper is the task (quit ends it; no SweepHelperDone comes from a thread) + sweep_helper_is_task: bool, /// the request number the vendor tool last carried out (the run's acknowledgement) tune_acked: Option, /// cards whose confirm check found a better neighbour: the full plan runs next @@ -700,6 +704,8 @@ impl Engine { sweep: None, quit_source: "unknown", no_ota, + power_task: None, + sweep_helper_is_task: false, tune_acked: None, tune_full_due: std::collections::HashSet::new(), sweep_pending: None, @@ -989,6 +995,7 @@ impl Engine { self.clock_next_https = Instant::now() + Duration::from_secs(6); } Cmd::PowerApplied(what, r, readback) => { + self.power_task = None; // the one elevated step may have registered the task: ask again next time self.power_busy = false; self.power_via_host = None; // the truth is what nvidia-smi reads back, not whether the prompt said yes @@ -1779,8 +1786,45 @@ impl Engine { self.power_busy = true; self.power_restore_pending = true; self.shared.log(&format!("power cap ({why}): {}", cmds.join(" & "))); - let line = cmds.join(" & "); let what = what.join(", "); + // once, ever (src/powertask.rs): a registered task sets the caps with no prompt; the readback judges it + if cfg!(windows) && self.power_task_registered() { + let pairs: Vec<(String, u64)> = self.st().mining.cards.iter().filter(|c| c.vendor == "nvidia" && c.enabled && c.present() && c.power_default_w > 0.0 && !c.power_applied).map(|c| (c.device.clone(), requested_watts(c).round() as u64)).collect(); + let dir = self.sweep_dir(); + let shared = self.shared.clone(); + self.shared.log("power cap: through the Igneum Power Helper task (no prompt)"); + std::thread::spawn(move || { + let r = crate::powertask::start().and_then(|_| { + let _ = std::fs::create_dir_all(&dir); + let mut seq = crate::platform::unix_now() % 1_000_000; + let mut text = String::new(); + for (dev, w) in &pairs { + seq += 1; + text.push_str(&format!("{seq} dev {dev}\n")); + seq += 1; + text.push_str(&format!("{seq} pl {w}\n")); + } + std::fs::write(dir.join("cmd.txt"), text).map_err(|e| e.to_string()) + }); + std::thread::sleep(Duration::from_secs(6)); + let back: std::collections::HashMap = crate::detect::nvidia_power_limits().into_iter().map(|(k, v)| (k, v.1)).collect(); + shared.send(Cmd::PowerApplied(what, r, back)); + }); + return; + } + // the first approval registers the task in the same elevated step as the caps (Windows), so no later step + // needs a prompt: the registration script is written next to the command file + let line = if cfg!(windows) { + let dir = self.sweep_dir(); + let _ = std::fs::create_dir_all(&dir); + let script = dir.join("register-power-task.ps1"); + match std::env::current_exe().map(|exe| std::fs::write(&script, [b"\xEF\xBB\xBF".as_slice(), crate::powertask::register_script(&exe).as_bytes()].concat())) { + Ok(Ok(())) => format!("{} & \"{}\" -NoProfile -ExecutionPolicy Bypass -File \"{}\"", cmds.join(" & "), crate::platform::tool("powershell").display(), script.display()), + _ => cmds.join(" & "), + } + } else { + cmds.join(" & ") + }; let want: std::collections::HashMap = self.st().mining.cards.iter().filter(|c| c.vendor == "nvidia" && c.enabled && c.present() && c.power_default_w > 0.0).map(|c| (c.device.clone(), requested_watts(c))).collect(); if self.wrapper && cfg!(windows) { // the window host has a UI context: it shows the administrator prompt and reports back on stdin @@ -1830,6 +1874,14 @@ impl Engine { self.shared.log(&format!("GPU power limits left as set (they reset at the next reboot; no prompt on quit): {}", cmds.join(" & "))); } + /// Is the Igneum Power Helper task registered (src/powertask.rs)? Asked once per run and after every elevated step. + fn power_task_registered(&mut self) -> bool { + if self.power_task.is_none() { + self.power_task = Some(crate::powertask::registered()); + } + self.power_task.unwrap_or(false) + } + /// Power control (config.rs power_control): may the engine ask for administrator rights for the cap or the sweep? /// The elevated PC sweep job (--sweep) sets caps directly and counts as allowed. fn elevation_allowed(&self) -> bool { @@ -1854,6 +1906,16 @@ impl Engine { if self.sweep.is_some() || self.sweep_pending.is_some() { self.sweep_abort("power control is off"); } + if cfg!(windows) && self.power_task_registered() { + // the kill switch: the task unregisters itself (elevated) and exits; nothing is left behind + let dir = self.sweep_dir(); + let _ = std::fs::write(dir.join("cmd.txt"), "remove\n"); + match crate::powertask::start() { + Ok(()) => self.shared.log("power control off: the Igneum Power Helper task removes itself"), + Err(e) => self.shared.log(&format!("power control off: the task could not be started to remove itself ({e}); remove it in Task Scheduler")), + } + self.power_task = None; + } self.shared.event(if note.starts_with("power control off:") { "error" } else { "info" }, note); } @@ -2343,8 +2405,18 @@ impl Engine { std::fs::write(&script, crate::sweep::helper_script_unix()).map_err(|e| e.to_string())?; format!("sh \"{}\" \"{}\" \"{}\" {} {}", script.display(), dir.display(), smi, c.device, restore) }; + if cfg!(windows) && self.power_task_registered() { + // once, ever: the registered task is the helper; it reads the same command file, no prompt + let _ = std::fs::write(dir.join("cmd.txt"), format!("{} dev {}\n", crate::platform::unix_now() % 1_000_000, c.device)); + crate::powertask::start()?; + self.shared.log("tune helper: the Igneum Power Helper task (no prompt)"); + self.sweep_helper = true; + self.sweep_helper_is_task = true; + return Ok(()); + } self.shared.log(&format!("tune helper (administrator prompt): {line}")); self.sweep_helper = true; + self.sweep_helper_is_task = false; let shared = self.shared.clone(); std::thread::spawn(move || { let r = crate::platform::run_elevated(&line); @@ -2356,6 +2428,11 @@ impl Engine { fn sweep_helper_quit(&mut self) { if self.sweep_helper { let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), "quit\n"); + 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; + self.sweep_helper_is_task = false; + } } } @@ -2400,7 +2477,8 @@ impl Engine { // measure only: nothing is set; the run notices the missing acknowledgement and measures return; } - let cmd = format!("{seq} pl {w}\n{seq} {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }); + let dev = device.to_string(); + let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }); let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), cmd); // the helper polls twice a second and nvidia-smi answers within a second or two std::thread::spawn(move || { diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index 5d7761c6e..bbb24485d 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -34,6 +34,7 @@ mod verifier; mod wslhost; mod sweep; mod ember; +mod powertask; mod watchdog; use std::io::{BufRead, Write}; @@ -53,6 +54,12 @@ fn main() { println!("igneum-app {}", engine::VERSION); return; } + if args.iter().any(|a| a == "--power-helper") { + // the scheduled task's action (src/powertask.rs): elevated, runs only digit-argument nvidia-smi commands + // from /app/sweep/cmd.txt, exits on quit, remove or 20 idle minutes + let dir = powertask::sweep_dir(&platform::data_root().join("app")); + std::process::exit(powertask::run_helper(&dir)); + } if args.iter().any(|a| a == "--launch") { if let Some(dir) = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())) { let host = dir.join("Igneum Miner.exe"); diff --git a/app/igneum-app/src/powertask.rs b/app/igneum-app/src/powertask.rs new file mode 100644 index 000000000..dbdf81830 --- /dev/null +++ b/app/igneum-app/src/powertask.rs @@ -0,0 +1,265 @@ +//! One administrator approval, ever (Josh, 6 October 2026, 11:50 UTC, after clicking the third prompt of the morning: +//! "can we make sure all these popups are not needed in future?"). +//! +//! What 0.3.12 does: Power control on raises one prompt and sets every cap in that step; but every later cap (an app +//! start, a reboot, a slider move) and every tune's helper is another elevated launch, so another prompt. This module +//! makes the first approval the last: the one elevated step also registers a per-user Windows scheduled task, +//! `Igneum Power Helper`, principal = the signed-in user, RunLevel Highest, no trigger, whose action is this very +//! executable with `--power-helper`. A task the user owns can be STARTED by the user's unelevated processes without a +//! prompt (`Start-ScheduledTask`), and it runs elevated; so every later cap and tune starts the task and talks to it +//! through the command file `/app/sweep/cmd.txt` (the protocol the 0.3.9 helper scripts spoke: ` pl +//! `, ` lgc `, ` rgc`, `quit`; plus `remove`, the kill switch). The task survives app restarts, +//! updates (the per-user installer replaces the exe in place; the task's action path is the install folder) and +//! reboots (a task, not a process). Power control off starts the task once and sends `remove`: the helper unregisters +//! the task (elevated) and exits; nothing is left behind. +//! +//! Threat note (what the helper will and will not run): +//! - The action is fixed at registration: the app's own exe in the install folder with `--power-helper`. The task +//! has no trigger and no arguments from outside; only `Start-ScheduledTask` by the owning user starts it. +//! - The helper reads ONE file, `/app/sweep/cmd.txt`, in the user's own profile. Every command it accepts +//! is a fixed verb with digit-only arguments: `pl ` runs `nvidia-smi -i -pl `, `lgc ` runs +//! `nvidia-smi -i -lgc 0,`, `rgc` runs `nvidia-smi -i -rgc`, `quit` ends it, `remove` unregisters +//! the task and ends it. The device index is digits only too (`dev ` sets it). No shell, no path, no string from +//! the file reaches a process: `Command::new(nvidia-smi).args([...])`, never `cmd /c`. +//! - nvidia-smi is resolved to the driver's install path (platform::tool), never from PATH. +//! - What an attacker running as the user gains: the power limit and the clock cap of the user's own NVIDIA cards, +//! within the ranges the driver allows, which the same user could set with one approved prompt anyway. Nothing +//! 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. + +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; + +/// The task name in the Windows Task Scheduler (per user). +pub const TASK_NAME: &str = "Igneum Power Helper"; +/// The helper ends after this long without a new command. +pub const IDLE_S: u64 = 20 * 60; + +/// One parsed command from cmd.txt. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HelperCmd { + Dev(String), + PowerLimit(u64), + ClockCap(u64), + ClockReset, + Quit, + Remove, +} + +/// Parses one line: ` []` (the 0.3.9 form ` ` reads as a power limit; `quit` and +/// `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)); + } + 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())?; + match p.as_slice() { + [_, w] if digits(w) => Some((seq, HelperCmd::PowerLimit(w.parse().ok()?))), + [_, "pl", w] if digits(w) => Some((seq, HelperCmd::PowerLimit(w.parse().ok()?))), + [_, "lgc", m] if digits(m) => Some((seq, HelperCmd::ClockCap(m.parse().ok()?))), + [_, "rgc"] => Some((seq, HelperCmd::ClockReset)), + [_, "dev", d] if digits(d) => Some((seq, HelperCmd::Dev(d.to_string()))), + _ => None, + } +} + +/// The nvidia-smi arguments a command becomes (None for the verbs that run nothing). +pub fn smi_args(dev: &str, c: &HelperCmd) -> Option> { + match c { + HelperCmd::PowerLimit(w) => Some(vec!["-i".into(), dev.into(), "-pl".into(), w.to_string()]), + HelperCmd::ClockCap(m) => Some(vec!["-i".into(), dev.into(), "-lgc".into(), format!("0,{m}")]), + HelperCmd::ClockReset => Some(vec!["-i".into(), dev.into(), "-rgc".into()]), + _ => None, + } +} + +/// 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. +pub fn register_script(exe: &Path) -> String { + let exe = exe.display().to_string().replace('\'', "''"); + format!( + "$a = New-ScheduledTaskAction -Execute '{exe}' -Argument '--power-helper' -WorkingDirectory '{dir}'\r\n\ + $p = New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -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(), + name = TASK_NAME + ) +} + +/// 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") +} + +/// The PowerShell that says whether the task is registered (exit 0) or not (exit 1). +pub fn query_command() -> String { + format!("if (Get-ScheduledTask -TaskName '{TASK_NAME}' -ErrorAction SilentlyContinue) {{ exit 0 }} else {{ exit 1 }}") +} + +/// The PowerShell the helper itself runs (elevated) on `remove`: the task goes, nothing is left. +pub fn remove_command() -> String { + format!("Unregister-ScheduledTask -TaskName '{TASK_NAME}' -Confirm:$false; exit 0") +} + +/// Is the task registered? Windows only; false elsewhere. +pub fn registered() -> bool { + if !cfg!(windows) { + return false; + } + let mut c = std::process::Command::new(crate::platform::tool("powershell")); + c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &query_command()]); + crate::platform::quiet(&mut c); + c.status().map(|s| s.success()).unwrap_or(false) +} + +/// Starts the task (no prompt). Ok when Start-ScheduledTask returned 0. +pub fn start() -> Result<(), String> { + let mut c = std::process::Command::new(crate::platform::tool("powershell")); + c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &start_command()]); + crate::platform::quiet(&mut c); + let out = c.output().map_err(|e| e.to_string())?; + if out.status.success() { + Ok(()) + } else { + Err(format!("Start-ScheduledTask failed: {}", String::from_utf8_lossy(&out.stderr).trim())) + } +} + +/// The helper process (`igneum-app --power-helper`): polls `/cmd.txt` twice a second, runs the parsed commands +/// through nvidia-smi, logs what it ran to `/helper.log`, ends on `quit`, on `remove` (after unregistering the +/// task) or after 20 idle minutes. `dir` is `/app/sweep`. +pub fn run_helper(dir: &Path) -> i32 { + let _ = std::fs::create_dir_all(dir); + let cmd_file = dir.join("cmd.txt"); + let log_file = dir.join("helper.log"); + let log = |line: &str| { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new().append(true).create(true).open(&log_file) { + let _ = writeln!(f, "{} {line}", crate::platform::unix_now()); + } + }; + log("helper started (scheduled task, elevated)"); + // a stale file from an earlier run is not a command: only lines after the start count + let mut last_seq: u64 = std::fs::read_to_string(&cmd_file).ok().and_then(|t| t.lines().filter_map(parse_line).map(|(s, _)| s).max()).unwrap_or(0); + let mut last_text = String::new(); + let mut dev = "0".to_string(); + let mut idle = Instant::now(); + let smi = crate::platform::tool("nvidia-smi"); + loop { + let text = std::fs::read_to_string(&cmd_file).unwrap_or_default(); + if text != last_text { + last_text = text.clone(); + for (seq, c) in text.lines().filter_map(parse_line) { + match c { + HelperCmd::Quit => { + log("quit"); + return 0; + } + HelperCmd::Remove => { + let mut p = std::process::Command::new(crate::platform::tool("powershell")); + p.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &remove_command()]); + crate::platform::quiet(&mut p); + let ok = p.status().map(|s| s.success()).unwrap_or(false); + log(&format!("remove: the task is {}", if ok { "unregistered" } else { "still registered (Unregister-ScheduledTask failed)" })); + return if ok { 0 } else { 1 }; + } + _ if seq <= last_seq => continue, + HelperCmd::Dev(d) => { + last_seq = seq; + idle = Instant::now(); + dev = d; + log(&format!("{seq} dev {dev}")); + } + other => { + last_seq = seq; + idle = Instant::now(); + let args = smi_args(&dev, &other).unwrap_or_default(); + let mut p = std::process::Command::new(&smi); + p.args(&args); + crate::platform::quiet(&mut p); + let out = p.output().map(|o| format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))).unwrap_or_else(|e| e.to_string()); + log(&format!("{seq} nvidia-smi {} : {}", args.join(" "), out.replace('\n', " ").trim())); + } + } + } + } + if idle.elapsed() >= Duration::from_secs(IDLE_S) { + log("idle 20 min: exit (the engine starts the task again when it needs it)"); + return 0; + } + std::thread::sleep(Duration::from_millis(500)); + } +} + +/// Where the command file lives for a data root. +pub fn sweep_dir(app_dir: &Path) -> PathBuf { + app_dir.join("sweep") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_fixed_verbs_with_digit_arguments_parse() { + assert_eq!(parse_line("7 pl 460"), Some((7, HelperCmd::PowerLimit(460)))); + assert_eq!(parse_line("8 lgc 2472"), Some((8, HelperCmd::ClockCap(2472)))); + assert_eq!(parse_line("9 rgc"), Some((9, HelperCmd::ClockReset))); + 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))); + // nothing else: no shell, no path, no string argument, no oversized number + for bad in ["7 pl 460; calc", "7 pl -460", "7 pl 4.60", "7 lgc 0,2472", "7 rm C:\\x", "x pl 460", "7 pl", "7 lgc 12345678", "7 dev ../1", "", "7 pl 460 extra"] { + assert_eq!(parse_line(bad), None, "{bad:?}"); + } + } + + #[test] + fn the_arguments_reach_nvidia_smi_as_a_list_never_a_shell() { + assert_eq!(smi_args("0", &HelperCmd::PowerLimit(460)).unwrap(), vec!["-i", "0", "-pl", "460"]); + assert_eq!(smi_args("1", &HelperCmd::ClockCap(2472)).unwrap(), vec!["-i", "1", "-lgc", "0,2472"]); + assert_eq!(smi_args("1", &HelperCmd::ClockReset).unwrap(), vec!["-i", "1", "-rgc"]); + assert_eq!(smi_args("0", &HelperCmd::Quit), None); + assert_eq!(smi_args("0", &HelperCmd::Remove), None); + assert_eq!(smi_args("0", &HelperCmd::Dev("1".into())), None); + } + + #[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}"); + assert!(s.contains("-RunLevel Highest") && s.contains("-LogonType Interactive"), "{s}"); + assert!(s.contains("[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"), "the signed-in user, never a literal"); + assert!(!s.contains("-Trigger"), "no trigger: only the app starts it"); + assert!(s.contains("-MultipleInstances IgnoreNew") && s.contains("-ExecutionTimeLimit"), "{s}"); + assert!(s.contains(&format!("-TaskName '{TASK_NAME}'"))); + // 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!(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'")); + } + + #[test] + fn a_stale_command_file_does_not_run_at_start() { + // the helper's start reads the highest sequence already in the file and runs nothing below or at it + let text = "3 pl 460\n4 lgc 2472\n"; + let last = text.lines().filter_map(parse_line).map(|(s, _)| s).max().unwrap_or(0); + assert_eq!(last, 4); + let newer: Vec<_> = "3 pl 460\n4 lgc 2472\n5 rgc\n".lines().filter_map(parse_line).filter(|(s, _)| *s > last).collect(); + assert_eq!(newer, vec![(5, HelperCmd::ClockReset)]); + } +} diff --git a/docs/plans/ember-tune.md b/docs/plans/ember-tune.md index c55865315..4f990112f 100644 --- a/docs/plans/ember-tune.md +++ b/docs/plans/ember-tune.md @@ -161,13 +161,36 @@ maximum, 14,001 MHz memory; 9070 XT present on bus 98 with OFFSET ranges `gmax_r 10`). The offset finding changed the AMD mapping (054e041): an offset clock range closes the clock knob and the power ladder runs on a percent scale bounded by `plimit_range`. The re-run follows the 0.3.11 rollout. +## 7a. One administrator approval, ever (0.3.13; Josh, 6 October 2026, 11:50 UTC) + +What 0.3.12 does: Power control on raises one prompt and sets every cap in that step; every later cap (an app start, a +reboot, a slider move) and every tune's helper is another elevated launch, so another prompt. Not "once, ever". + +What `src/powertask.rs` does: the first approval's elevated step also registers a per-user Windows scheduled task, +`Igneum Power Helper` (principal = the signed-in user, interactive logon, RunLevel Highest, no trigger, hidden, one +hour limit, new starts ignored while one runs), whose action is the app's own exe in the install folder with +`--power-helper`. A task the user owns is started by the user's unelevated engine with `Start-ScheduledTask`, no +prompt, and runs elevated. Every later cap and every tune's helper starts the task and writes the command file +`/app/sweep/cmd.txt` (` dev `, ` pl `, ` lgc `, ` rgc`, `quit`). The task +survives app restarts, updates (the per-user installer replaces the exe in place; the task's action path is the +install folder) and reboots. Power control off starts the task once and sends `remove`: the helper unregisters the +task (elevated) and exits; nothing is left behind. Linux keeps pkexec per step; macOS has no cap. + +Threat note: the helper runs only fixed verbs with digit-only arguments through `Command::new(nvidia-smi).args` +(the driver's own path, never PATH, never a shell); a line that is anything else is ignored; the sequence must rise +(a stale file runs nothing); an attacker running as the user gains the power limit and clock cap of the user's own +NVIDIA cards inside the driver's ranges, which the same user could set with one approved prompt anyway; no file, +process, registry key or other binary is reachable through it. Tests: `powertask::tests` (the parser refuses every +non-digit or extra argument, the arguments reach nvidia-smi as a list, the registration is per-user, highest, +trigger-less and quote-safe, a stale command file runs nothing). + ## 8a. Next-cut notes (for the 0.3.12 shipper) | Commit | What | Where | |---|---|---| | b671c8b | every `quit:` names its source; Power control alone decides; no cap at start under `--sweep` | main.rs, server.rs, engine.rs (separable) | | e600e63 | a second engine never runs the updater (`IGNEUM_APP_NO_OTA`, implied by `--sweep`) | engine.rs (6 lines, separable) | -| 1e9550e (this commit, amended) | the elevated job path's output file is followed while the script runs, so the 5-minute progress reports carry its lines (a 35-minute run that never mined showed only "script running" on 6 October 2026); the tune playbook's watchdog fails a run that mines nothing within 120 s of its first status line, with the engine's last log line in the RESULT | jobrun.rs `follow_file`, relay/playbooks/ember-tune-pc1.ps1 | +| 1e9550e | the elevated job path's output file is followed while the script runs, so the 5-minute progress reports carry its lines (a 35-minute run that never mined showed only "script running" on 6 October 2026); the tune playbook's watchdog fails a run that mines nothing within 120 s of its first status line, with the engine's last log line in the RESULT | jobrun.rs `follow_file`, relay/playbooks/ember-tune-pc1.ps1 | ## 9. Open