diff --git a/CLAUDE.md b/CLAUDE.md index 11006d175..171ca6a91 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ They review each other's work. A claim about another chain must cite the repo an - Transactions are public, like Ethereum. Privacy features and shielded pools were considered and REJECTED by Josh on 3 October 2026. Do not add them. ## Infrastructure (no URLs yet, by Josh's instruction) -- GitHub: https://github.com/igneum-network/igneum (organisation igneum-network, created 3 Oct 2026; owners igneum-josh (Josh, the igneum.network Google login) and joshmalone117; private, branch master). Commit as igneum-josh <337424239+igneum-josh@users.noreply.github.com> (the repo's git config; standing rule 3 Oct 2026: the organisation owner is never publicly visible, so no personal name or email in the history; the 40 commits before this rule carry Josh's name and must be rewritten before the repo goes public). Push only when Josh asks. This checkout's git credential helper returns the igneum-josh token by name (`gh auth token --user igneum-josh`, set 5 October 2026 after an agent left the other account active and a push failed with "repository not found"), so git pushes from here work whichever gh account is active; still, before any gh call: `gh auth status` must show igneum-josh as the ACTIVE account (`gh auth switch --user igneum-josh` if not); the joshmalone117 login on this Mac belongs to other projects and has no access to this repository. +- GitHub: https://github.com/igneum-network/igneum (organisation igneum-network, created 3 Oct 2026; owners igneum-josh (Josh, the igneum.network Google login) and joshmalone117; private, branch master). Commit as igneum-josh <337424239+igneum-josh@users.noreply.github.com> (the repo's git config; standing rule 3 Oct 2026: the organisation owner is never publicly visible, so no personal name or email in the history; the 40 commits before this rule carry Josh's name and must be rewritten before the repo goes public). Push only when Josh asks. RENAMED 5 October 2026 (18:00 UTC, by Josh's decision): the owner login igneum-josh is now `igneum-labs` (GitHub rename; the noreply id 337424239 is unchanged, so the commit address becomes 337424239+igneum-labs@users.noreply.github.com at the fresh-repository step, docs/plans/rotation-phase-2.md 8g; until then commits keep the igneum-josh address). gh on this Mac still stores the token under the OLD name: `gh auth token --user igneum-josh` is the only way to read it until a re-login as igneum-labs. This checkout's `.git/config` (shared by every worktree) resets `credential.helper` and then adds one helper that returns username igneum-labs with that token, so git pushes from here work whichever gh account is active (the global `gh auth git-credential` helper answered first before this fix and handed out joshmalone117: "repository not found"). Before any gh call: `gh auth status` must show igneum-josh (the stored name) as the ACTIVE account (`gh auth switch --user igneum-josh` if not); the joshmalone117 login on this Mac belongs to other projects and has no access to this repository. - Vercel project: igneum in the vivanmn team, because Vercel refuses the personal account as a scope. Move it to its own team when the venture is named. For the site, explorer and job-market API later. - Neon database: igneum, id soft-voice-31914738, London (aws-eu-west-2), Postgres 17. Connection string in ~/.config/igneum/env, never in the repo. diff --git a/app/igneum-app/src/jobrun.rs b/app/igneum-app/src/jobrun.rs index 6bbf4e735..d7a4765cd 100644 --- a/app/igneum-app/src/jobrun.rs +++ b/app/igneum-app/src/jobrun.rs @@ -1119,7 +1119,10 @@ fn run_script(shared: &Arc, job: &Job, sink: &Sink, jobs_dir: &Path, dat std::fs::write(&wrapper, [b"\xEF\xBB\xBF".as_slice(), w.as_bytes()].concat()).map_err(|e| e.to_string())?; let _ = std::fs::remove_file(&out_file); let inner = format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", wrapper.display()); - let ps = format!("$p = Start-Process -FilePath powershell.exe -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru; exit $p.ExitCode", inner.replace('\'', "''")); + // A refused or unanswered UAC prompt makes Start-Process throw (`$p` stays null) and `exit $p.ExitCode` + // would exit 0: the 5 October 2026 driver job on PC 1 was reported "done" after Windows cancelled its + // prompt at 122 s. The launch failure is exit 251 and says so on stderr. + let ps = format!("try {{ $p = Start-Process -FilePath powershell.exe -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{ Write-Error ('elevated launch failed (UAC refused, cancelled or timed out): ' + $_.Exception.Message); exit 251 }}; if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}; exit $p.ExitCode", inner.replace('\'', "''")); cmd = Command::new(crate::platform::tool("powershell")); cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]); } else if shell == "powershell" { @@ -1145,6 +1148,7 @@ fn run_script(shared: &Arc, job: &Job, sink: &Sink, jobs_dir: &Path, dat fn finish_ran(ran: Ran, what: &str) -> Result { match ran.code { Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }), + Some(251) => Ok(Done { status: "failed".into(), exit: 251, summary: format!("{what} did not start: the administrator prompt was refused, cancelled or timed out (click Yes within 2 minutes)"), extra: json!({}) }), Some(c) => Ok(Done { status: "failed".into(), exit: c as i64, summary: format!("{what} exited with code {c}"), extra: json!({}) }), None if ran.timed_out => Ok(Done { status: "timeout".into(), exit: -1, summary: format!("{what} hit the time cap and was ended"), extra: json!({}) }), None => Err(format!("{what} was ended")), @@ -1259,6 +1263,20 @@ fn collect_done(uploaded: u32, failed: u32, names: Vec, ran: Option mod tests { use super::*; + #[test] + fn a_refused_administrator_prompt_is_a_failure_not_done() { + // 5 October 2026: the elevated launcher exited 0 after Windows cancelled an unanswered UAC prompt + let d = finish_ran(Ran { code: Some(251), timed_out: false }, "script").unwrap(); + assert_eq!((d.status.as_str(), d.exit), ("failed", 251)); + assert!(d.summary.contains("administrator prompt"), "{}", d.summary); + let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap(); + assert_eq!(d.status, "done"); + // the launcher string itself: a thrown Start-Process must not fall through to `exit $p.ExitCode` + let src = include_str!("jobrun.rs"); + assert!(src.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{")); + assert!(src.contains("if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}")); + } + #[test] fn collect_outcome_follows_the_command_exit() { let d = collect_done(0, 0, vec![], None);