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:
parent
af5ade4339
commit
295711ba29
10 changed files with 289 additions and 45 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('\'', "''")
|
||||
|
|
|
|||
|
|
@ -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(®ister_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'"));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="*"/>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue