From 6fb7ece60b203aa06bc7addfbf05eff4ec8da937 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 12:34:21 +0700 Subject: [PATCH 001/386] performance add huge page authority --- Cargo.lock | 13 + Cargo.toml | 1 + README.md | 6 +- .../bootloaders/grub-pc/live-theme/theme.txt | 2 +- .../bootloaders/syslinux_common/live.cfg.in | 4 +- build/usb/hooks/010-rigos.chroot | 4 +- .../systemd/system/rigos-hugepages.service | 24 + .../etc/systemd/system/rigos-miner.service | 4 +- .../system/rigos-profile-apply.service | 2 +- build/usb/version.env | 6 +- crates/rigos-performance/Cargo.toml | 19 + crates/rigos-performance/src/lib.rs | 820 ++++++++++++++++++ crates/rigos-performance/src/main.rs | 76 ++ crates/rigos-schema/src/lib.rs | 41 + crates/rigosd/src/lib.rs | 149 +++- docs/performance-authority.md | 74 ++ docs/usb-image-build.md | 15 +- schemas/performance-status-v1.schema.json | 111 +++ scripts/build-usb-image.sh | 3 +- scripts/verify-usb-appliance.sh | 12 +- scripts/verify.sh | 7 + 21 files changed, 1369 insertions(+), 24 deletions(-) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service create mode 100644 crates/rigos-performance/Cargo.toml create mode 100644 crates/rigos-performance/src/lib.rs create mode 100644 crates/rigos-performance/src/main.rs create mode 100644 docs/performance-authority.md create mode 100644 schemas/performance-status-v1.schema.json diff --git a/Cargo.lock b/Cargo.lock index 88b1e7e3..20357e13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -845,6 +845,19 @@ dependencies = [ "rigos-machine", ] +[[package]] +name = "rigos-performance" +version = "0.0.1" +dependencies = [ + "chrono", + "clap", + "rigos-schema", + "serde", + "serde_json", + "thiserror", + "uuid", +] + [[package]] name = "rigos-pool" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5108ed96..49215704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/rigos-pool", "crates/rigos-state", "crates/rigos-config", + "crates/rigos-performance", ] resolver = "2" diff --git a/README.md b/README.md index 20b779a5..9f98a20b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Local-first CPU appliance delivered as a bootable USB image. -Current development preview is `RIGOS 0.0.4-alpha.5`. +Current development preview is `RIGOS 0.0.4-alpha.6`. The persistent appliance uses a raw MBR disk image for Legacy BIOS and removable-media UEFI boot. @@ -23,9 +23,11 @@ The recovery ISO is stateless and does not grow the state partition. 0.0.4-alpha.3 fixed console order but kept the first boot screen hidden 0.0.4-alpha.4 keeps the first boot screen on tty and captures answers separately 0.0.4-alpha.5 adds local rig profiles and portable XMRig Flight Sheets +0.0.4-alpha.6 adds visible machine-wide huge page authority ``` -Alpha five is isolated on its development branch. Alpha four physical state validation remains separate. +Alpha five is frozen at its physically validated image and Alpha six develops +performance authority on a separate branch. ## Verification diff --git a/build/usb/bootloaders/grub-pc/live-theme/theme.txt b/build/usb/bootloaders/grub-pc/live-theme/theme.txt index 2e75711f..bdf6a80a 100644 --- a/build/usb/bootloaders/grub-pc/live-theme/theme.txt +++ b/build/usb/bootloaders/grub-pc/live-theme/theme.txt @@ -1,6 +1,6 @@ title-color: "#ffffff" title-font: "Unifont Regular 16" -title-text: "RIGOS Recovery 0.0.4-alpha.5 - Stateless Diagnostics" +title-text: "RIGOS Recovery 0.0.4-alpha.6 - Stateless Diagnostics" message-font: "Unifont Regular 16" terminal-font: "Unifont Regular 16" diff --git a/build/usb/bootloaders/syslinux_common/live.cfg.in b/build/usb/bootloaders/syslinux_common/live.cfg.in index d17a6983..0c41382b 100644 --- a/build/usb/bootloaders/syslinux_common/live.cfg.in +++ b/build/usb/bootloaders/syslinux_common/live.cfg.in @@ -1,12 +1,12 @@ label rigos-@FLAVOUR@ - menu label ^RIGOS Recovery 0.0.4-alpha.5 + menu label ^RIGOS Recovery 0.0.4-alpha.6 menu default linux @LINUX@ initrd @INITRD@ append @APPEND_LIVE@ label rigos-@FLAVOUR@-failsafe - menu label RIGOS Recovery 0.0.4-alpha.5 -- ^safe mode + menu label RIGOS Recovery 0.0.4-alpha.6 -- ^safe mode linux @LINUX@ initrd @INITRD@ append @APPEND_LIVE_FAILSAFE@ diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index c3e04f65..47886746 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -7,8 +7,8 @@ passwd --lock rigosadmin install -d -o rigos -g rigos -m 0750 /var/lib/rigos /run/rigos install -d -m 0755 /usr/lib/rigos -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-config /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service rigos-state.service rigos-profile-apply.service rigos-firstboot.service rigos-miner.service tmp.mount +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service rigos-state.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service b/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service new file mode 100644 index 00000000..4bde44d7 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service @@ -0,0 +1,24 @@ +[Unit] +Description=Apply RIGOS huge page authority +After=rigos-state.service rigos-profile-apply.service +Requires=rigos-state.service rigos-profile-apply.service +Before=rigos-miner.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-performance hugepages apply +RemainAfterExit=yes +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ProtectSystem=strict +ProtectKernelTunables=no +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +LockPersonality=yes +ReadWritePaths=/proc/sys/vm/nr_hugepages /run/rigos + +[Install] +WantedBy=multi-user.target diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service index f93f487e..acb5e909 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service @@ -1,8 +1,8 @@ [Unit] Description=RIGOS managed XMRig -After=network-online.target rigos-state.service rigos-profile-apply.service +After=network-online.target rigos-state.service rigos-profile-apply.service rigos-hugepages.service Wants=network-online.target -Requires=rigos-state.service +Requires=rigos-state.service rigos-hugepages.service ConditionPathExists=/var/lib/rigos/xmrig.json [Service] diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service b/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service index 6dc7e7b9..9cdd6b67 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service @@ -2,7 +2,7 @@ Description=Apply RIGOS persistent machine profile After=rigos-state.service Requires=rigos-state.service -Before=rigos-firstboot.service rigos-miner.service +Before=rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] Type=oneshot diff --git a/build/usb/version.env b/build/usb/version.env index 44e1a3f4..4029501e 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.5 -RIGOS_IMAGE_VERSION=0.0.4-alpha.5 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.6 +RIGOS_IMAGE_VERSION=0.0.4-alpha.6 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=5 +RIGOS_BUILD_ORDINAL=6 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 diff --git a/crates/rigos-performance/Cargo.toml b/crates/rigos-performance/Cargo.toml new file mode 100644 index 00000000..e1b77b62 --- /dev/null +++ b/crates/rigos-performance/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rigos-performance" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +chrono.workspace = true +clap.workspace = true +rigos-schema = { path = "../rigos-schema" } +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +uuid.workspace = true + +[[bin]] +name = "rigos-performance" +path = "src/main.rs" diff --git a/crates/rigos-performance/src/lib.rs b/crates/rigos-performance/src/lib.rs new file mode 100644 index 00000000..5d91c3a1 --- /dev/null +++ b/crates/rigos-performance/src/lib.rs @@ -0,0 +1,820 @@ +#![forbid(unsafe_code)] + +use chrono::{SecondsFormat, Utc}; +use rigos_schema::{ + HugePageAuthorityStatusV1, HugePageAuthorityV1, PERFORMANCE_STATUS_SCHEMA, PerformanceStatusV1, +}; +use serde_json::Value; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs::{self, OpenOptions}, + io::Write, + path::{Path, PathBuf}, + process::{Command, ExitStatus}, + thread, + time::Duration, +}; +use uuid::Uuid; + +pub const RX0_TARGET_PAGES: u64 = 1280; +pub const EXPECTED_HUGE_PAGE_SIZE_BYTES: u64 = 2 * 1024 * 1024; +pub const MEMORY_RESERVE_BYTES: u64 = 1024 * 1024 * 1024; +const POLL_ATTEMPTS: usize = 50; +const POLL_INTERVAL: Duration = Duration::from_millis(100); + +#[derive(Debug, thiserror::Error)] +pub enum AuthorityError { + #[error("configuration is unreadable: {0}")] + Configuration(String), + #[error("machine truth is unavailable: {0}")] + Machine(String), + #[error("miner control failed: {0}")] + Miner(String), + #[error("performance status publication failed: {0}")] + Status(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PerformancePolicy { + pub requested: bool, + pub algorithm: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MemorySnapshot { + pub available_bytes: u64, + pub huge_page_size_bytes: u64, + pub huge_pages_total: u64, + pub nr_hugepages: Option, +} + +pub trait HugePageKernel { + fn snapshot(&mut self) -> Result; + fn write_nr_hugepages(&mut self, pages: u64) -> Result<(), AuthorityError>; + fn wait(&mut self, duration: Duration); +} + +pub trait MinerControl { + fn is_active(&mut self) -> Result; + fn stop(&mut self) -> Result<(), AuthorityError>; + fn start_no_block(&mut self) -> Result<(), AuthorityError>; +} + +pub struct ProcKernel { + pub meminfo: PathBuf, + pub nr_hugepages: PathBuf, +} + +impl Default for ProcKernel { + fn default() -> Self { + Self { + meminfo: "/proc/meminfo".into(), + nr_hugepages: "/proc/sys/vm/nr_hugepages".into(), + } + } +} + +impl HugePageKernel for ProcKernel { + fn snapshot(&mut self) -> Result { + let text = fs::read_to_string(&self.meminfo) + .map_err(|error| AuthorityError::Machine(format!("meminfo: {error}")))?; + let nr_hugepages = match fs::read_to_string(&self.nr_hugepages) { + Ok(value) => Some( + value + .trim() + .parse::() + .map_err(|_| AuthorityError::Machine("nr_hugepages is malformed".into()))?, + ), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => { + return Err(AuthorityError::Machine(format!( + "nr_hugepages read: {error}" + ))); + } + }; + parse_meminfo(&text, nr_hugepages) + } + + fn write_nr_hugepages(&mut self, pages: u64) -> Result<(), AuthorityError> { + fs::write(&self.nr_hugepages, format!("{pages}\n")) + .map_err(|error| AuthorityError::Machine(format!("nr_hugepages write: {error}"))) + } + + fn wait(&mut self, duration: Duration) { + thread::sleep(duration); + } +} + +pub struct SystemdMinerControl { + pub executable: PathBuf, +} + +impl Default for SystemdMinerControl { + fn default() -> Self { + Self { + executable: "/usr/bin/systemctl".into(), + } + } +} + +impl SystemdMinerControl { + fn status(&self, args: &[&str]) -> Result { + Command::new(&self.executable) + .args(args) + .status() + .map_err(|error| AuthorityError::Miner(error.to_string())) + } +} + +impl MinerControl for SystemdMinerControl { + fn is_active(&mut self) -> Result { + let status = self.status(&["is-active", "--quiet", "rigos-miner.service"])?; + match status.code() { + Some(0) => Ok(true), + Some(3) => Ok(false), + code => Err(AuthorityError::Miner(format!( + "is-active returned {code:?}" + ))), + } + } + + fn stop(&mut self) -> Result<(), AuthorityError> { + let status = self.status(&["stop", "rigos-miner.service"])?; + if status.success() && !self.is_active()? { + Ok(()) + } else { + Err(AuthorityError::Miner("miner did not stop".into())) + } + } + + fn start_no_block(&mut self) -> Result<(), AuthorityError> { + let status = self.status(&["--no-block", "start", "rigos-miner.service"])?; + if status.success() { + Ok(()) + } else { + Err(AuthorityError::Miner(format!( + "start returned {:?}", + status.code() + ))) + } + } +} + +pub fn parse_meminfo( + text: &str, + nr_hugepages: Option, +) -> Result { + let mut values = BTreeMap::new(); + for line in text.lines() { + let Some((name, raw)) = line.split_once(':') else { + continue; + }; + let mut fields = raw.split_whitespace(); + let Some(value) = fields.next().and_then(|value| value.parse::().ok()) else { + continue; + }; + let multiplier = match fields.next() { + Some("kB") => 1024, + None => 1, + _ => continue, + }; + if let Some(bytes) = value.checked_mul(multiplier) { + values.insert(name, bytes); + } + } + let required = |name: &str| { + values + .get(name) + .copied() + .ok_or_else(|| AuthorityError::Machine(format!("meminfo field {name} is unavailable"))) + }; + let snapshot = MemorySnapshot { + available_bytes: required("MemAvailable")?, + huge_page_size_bytes: required("Hugepagesize")?, + huge_pages_total: required("HugePages_Total")?, + nr_hugepages, + }; + if let Some(nr) = snapshot.nr_hugepages { + if nr != snapshot.huge_pages_total { + return Err(AuthorityError::Machine( + "nr_hugepages disagrees with HugePages_Total".into(), + )); + } + } + Ok(snapshot) +} + +pub fn parse_policy(policy: &[u8], xmrig: &[u8]) -> Result { + let policy: Value = serde_json::from_slice(policy) + .map_err(|error| AuthorityError::Configuration(format!("policy JSON: {error}")))?; + if policy.get("schema").and_then(Value::as_str) != Some("rigos.policy/v1") { + return Err(AuthorityError::Configuration( + "policy schema is not rigos.policy/v1".into(), + )); + } + if !matches!( + policy.get("miner_start_mode").and_then(Value::as_str), + Some("manual" | "on_boot") + ) { + return Err(AuthorityError::Configuration( + "miner_start_mode is invalid".into(), + )); + } + let xmrig: Value = serde_json::from_slice(xmrig) + .map_err(|error| AuthorityError::Configuration(format!("XMRig JSON: {error}")))?; + let requested = xmrig + .pointer("/cpu/huge-pages") + .and_then(Value::as_bool) + .ok_or_else(|| AuthorityError::Configuration("cpu.huge-pages is missing".into()))?; + let pools = xmrig + .get("pools") + .and_then(Value::as_array) + .filter(|pools| !pools.is_empty()) + .ok_or_else(|| AuthorityError::Configuration("pools are missing".into()))?; + let algorithms: BTreeSet<_> = pools + .iter() + .map(|pool| { + pool.get("algo") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(str::to_owned) + .ok_or_else(|| AuthorityError::Configuration("pool algorithm is missing".into())) + }) + .collect::>()?; + if algorithms.len() != 1 { + return Err(AuthorityError::Configuration( + "pool algorithms are ambiguous".into(), + )); + } + Ok(PerformancePolicy { + requested, + algorithm: algorithms.into_iter().next().unwrap_or_default(), + }) +} + +pub fn apply_huge_page_policy( + policy: &PerformancePolicy, + kernel: &mut K, +) -> Result { + let before = kernel.snapshot()?; + if before.huge_page_size_bytes != EXPECTED_HUGE_PAGE_SIZE_BYTES { + return Ok(outcome( + policy.requested, + 0, + before.huge_pages_total, + &before, + HugePageAuthorityStatusV1::DegradedUnsupported, + Some("unsupported_huge_page_size"), + )); + } + let Some(current) = before.nr_hugepages else { + return Ok(outcome( + policy.requested, + 0, + before.huge_pages_total, + &before, + HugePageAuthorityStatusV1::DegradedUnsupported, + Some("nr_hugepages_unavailable"), + )); + }; + if !policy.requested { + let actual = write_and_read_back(kernel, current, 0)?; + let status = if actual == 0 { + HugePageAuthorityStatusV1::Disabled + } else { + HugePageAuthorityStatusV1::DegradedReleaseIncomplete + }; + let reason = (actual != 0).then_some("kernel_release_incomplete"); + return Ok(outcome(false, 0, actual, &before, status, reason)); + } + if policy.algorithm != "rx/0" { + return Ok(outcome( + true, + 0, + current, + &before, + HugePageAuthorityStatusV1::DegradedUnsupported, + Some("unsupported_algorithm"), + )); + } + if current >= RX0_TARGET_PAGES { + return Ok(outcome( + true, + RX0_TARGET_PAGES, + current, + &before, + HugePageAuthorityStatusV1::Ready, + None, + )); + } + let usable = before.available_bytes.saturating_sub(MEMORY_RESERVE_BYTES); + let safe_pages = usable / before.huge_page_size_bytes; + let attempted = RX0_TARGET_PAGES.min(safe_pages); + let actual = write_and_read_back(kernel, current, attempted)?; + let (status, reason) = if attempted < RX0_TARGET_PAGES { + ( + HugePageAuthorityStatusV1::DegradedInsufficientMemory, + Some(if actual < attempted { + "safe_attempt_partially_allocated" + } else { + "memory_reserve_limited_attempt" + }), + ) + } else if actual >= RX0_TARGET_PAGES { + (HugePageAuthorityStatusV1::Ready, None) + } else if actual == 0 { + ( + HugePageAuthorityStatusV1::DegradedUnavailable, + Some("kernel_allocation_unavailable"), + ) + } else { + ( + HugePageAuthorityStatusV1::DegradedPartialAllocation, + Some("kernel_partial_allocation"), + ) + }; + Ok(outcome(true, attempted, actual, &before, status, reason)) +} + +fn write_and_read_back( + kernel: &mut K, + current: u64, + requested: u64, +) -> Result { + if current != requested { + kernel.write_nr_hugepages(requested)?; + } + let mut actual = current; + for attempt in 0..POLL_ATTEMPTS { + let snapshot = kernel.snapshot()?; + actual = snapshot.nr_hugepages.ok_or_else(|| { + AuthorityError::Machine("nr_hugepages disappeared after write".into()) + })?; + if actual == requested { + break; + } + if attempt + 1 < POLL_ATTEMPTS { + kernel.wait(POLL_INTERVAL); + } + } + Ok(actual) +} + +fn outcome( + requested: bool, + attempted_pages: u64, + actual_pages: u64, + before: &MemorySnapshot, + status: HugePageAuthorityStatusV1, + reason: Option<&str>, +) -> HugePageAuthorityV1 { + let target_pages = if requested { RX0_TARGET_PAGES } else { 0 }; + let allocation_percent_of_target = if target_pages == 0 { + if actual_pages == 0 { 100.0 } else { 0.0 } + } else { + actual_pages as f64 * 100.0 / target_pages as f64 + }; + HugePageAuthorityV1 { + requested, + target_pages, + attempted_pages, + actual_pages, + huge_page_size_bytes: before.huge_page_size_bytes, + memory_available_before_bytes: before.available_bytes, + reserve_bytes: MEMORY_RESERVE_BYTES, + allocation_percent_of_target, + status, + reason: reason.map(str::to_owned), + } +} + +pub struct AuthorityPaths { + pub state_root: PathBuf, + pub status: PathBuf, + pub boot_id: PathBuf, +} + +impl Default for AuthorityPaths { + fn default() -> Self { + Self { + state_root: "/var/lib/rigos".into(), + status: "/run/rigos/performance-status.json".into(), + boot_id: "/proc/sys/kernel/random/boot_id".into(), + } + } +} + +pub fn execute( + paths: &AuthorityPaths, + kernel: &mut K, + miner: &mut C, +) -> Result { + match fs::remove_file(&paths.status) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(AuthorityError::Status(error.to_string())), + } + let current = paths.state_root.join("current"); + let revision_target = fs::read_link(¤t) + .map_err(|error| AuthorityError::Configuration(format!("current revision: {error}")))?; + let revision = revision_target + .file_name() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| AuthorityError::Configuration("current revision is invalid".into()))? + .to_owned(); + let policy_bytes = fs::read(current.join("policy.json")) + .map_err(|error| AuthorityError::Configuration(format!("policy: {error}")))?; + let xmrig_bytes = fs::read(current.join("xmrig.json")) + .map_err(|error| AuthorityError::Configuration(format!("XMRig config: {error}")))?; + let policy = parse_policy(&policy_bytes, &xmrig_bytes)?; + let (huge_pages, restore_miner) = apply_with_miner(&policy, kernel, miner)?; + let status = PerformanceStatusV1 { + schema: PERFORMANCE_STATUS_SCHEMA.into(), + boot_id: read_trimmed(&paths.boot_id, "boot ID")?, + generated_at: Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), + config_revision: revision, + algorithm: Some(policy.algorithm), + huge_pages, + }; + write_status_verified(&paths.status, &status)?; + restore_miner_if_needed(miner, restore_miner)?; + Ok(status) +} + +pub fn apply_with_miner( + policy: &PerformancePolicy, + kernel: &mut K, + miner: &mut C, +) -> Result<(HugePageAuthorityV1, bool), AuthorityError> { + let active = miner.is_active()?; + let will_mutate = !policy.requested || policy.algorithm == "rx/0"; + if active && will_mutate { + miner.stop()?; + } + let result = apply_huge_page_policy(policy, kernel)?; + Ok((result, active && will_mutate)) +} + +pub fn restore_miner_if_needed( + miner: &mut C, + restore: bool, +) -> Result<(), AuthorityError> { + if restore { + miner.start_no_block()?; + } + Ok(()) +} + +fn read_trimmed(path: &Path, name: &str) -> Result { + let value = fs::read_to_string(path) + .map_err(|error| AuthorityError::Machine(format!("{name}: {error}")))?; + let value = value.trim(); + if value.is_empty() { + Err(AuthorityError::Machine(format!("{name} is empty"))) + } else { + Ok(value.to_owned()) + } +} + +pub fn write_status_verified( + path: &Path, + status: &PerformanceStatusV1, +) -> Result<(), AuthorityError> { + let parent = path + .parent() + .ok_or_else(|| AuthorityError::Status("status path has no parent".into()))?; + fs::create_dir_all(parent).map_err(|error| AuthorityError::Status(error.to_string()))?; + let temporary = parent.join(format!(".performance-status-{}.tmp", Uuid::new_v4())); + let result = (|| { + let mut options = OpenOptions::new(); + options.create_new(true).write(true); + let mut file = options + .open(&temporary) + .map_err(|error| AuthorityError::Status(error.to_string()))?; + serde_json::to_writer(&mut file, status) + .map_err(|error| AuthorityError::Status(error.to_string()))?; + file.write_all(b"\n") + .and_then(|_| file.sync_all()) + .map_err(|error| AuthorityError::Status(error.to_string()))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&temporary, fs::Permissions::from_mode(0o644)) + .map_err(|error| AuthorityError::Status(error.to_string()))?; + } + fs::rename(&temporary, path).map_err(|error| AuthorityError::Status(error.to_string()))?; + #[cfg(unix)] + fs::File::open(parent) + .and_then(|file| file.sync_all()) + .map_err(|error| AuthorityError::Status(error.to_string()))?; + let observed: PerformanceStatusV1 = serde_json::from_slice( + &fs::read(path).map_err(|error| AuthorityError::Status(error.to_string()))?, + ) + .map_err(|error| AuthorityError::Status(error.to_string()))?; + if &observed != status { + return Err(AuthorityError::Status("status read-back mismatch".into())); + } + Ok(()) + })(); + if result.is_err() { + let _ = fs::remove_file(&temporary); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::VecDeque; + + struct FakeKernel { + snapshots: VecDeque, + last: MemorySnapshot, + writes: Vec, + waits: usize, + } + + struct FakeMiner { + active: bool, + calls: Vec<&'static str>, + } + + struct FailingKernel { + before: MemorySnapshot, + } + + impl HugePageKernel for FailingKernel { + fn snapshot(&mut self) -> Result { + Ok(self.before.clone()) + } + + fn write_nr_hugepages(&mut self, _pages: u64) -> Result<(), AuthorityError> { + Err(AuthorityError::Machine("synthetic write failure".into())) + } + + fn wait(&mut self, _duration: Duration) {} + } + + impl MinerControl for FakeMiner { + fn is_active(&mut self) -> Result { + self.calls.push("is_active"); + Ok(self.active) + } + + fn stop(&mut self) -> Result<(), AuthorityError> { + self.calls.push("stop"); + self.active = false; + Ok(()) + } + + fn start_no_block(&mut self) -> Result<(), AuthorityError> { + self.calls.push("start"); + self.active = true; + Ok(()) + } + } + + impl FakeKernel { + fn new(snapshots: Vec) -> Self { + let last = snapshots.last().cloned().unwrap(); + Self { + snapshots: snapshots.into(), + last, + writes: vec![], + waits: 0, + } + } + } + + impl HugePageKernel for FakeKernel { + fn snapshot(&mut self) -> Result { + if let Some(value) = self.snapshots.pop_front() { + self.last = value; + } + Ok(self.last.clone()) + } + + fn write_nr_hugepages(&mut self, pages: u64) -> Result<(), AuthorityError> { + self.writes.push(pages); + Ok(()) + } + + fn wait(&mut self, _duration: Duration) { + self.waits += 1; + } + } + + fn memory(available: u64, pages: u64) -> MemorySnapshot { + MemorySnapshot { + available_bytes: available, + huge_page_size_bytes: EXPECTED_HUGE_PAGE_SIZE_BYTES, + huge_pages_total: pages, + nr_hugepages: Some(pages), + } + } + + fn policy(requested: bool) -> PerformancePolicy { + PerformancePolicy { + requested, + algorithm: "rx/0".into(), + } + } + + #[test] + fn parses_meminfo_units_and_rejects_disagreement() { + let text = "MemAvailable: 4096 kB\nHugePages_Total: 2\nHugepagesize: 2048 kB\n"; + let parsed = parse_meminfo(text, Some(2)).unwrap(); + assert_eq!(parsed.available_bytes, 4 * 1024 * 1024); + assert_eq!(parsed.huge_page_size_bytes, EXPECTED_HUGE_PAGE_SIZE_BYTES); + assert!(parse_meminfo(text, Some(1)).is_err()); + } + + #[test] + fn full_allocation_is_ready() { + let available = MEMORY_RESERVE_BYTES + RX0_TARGET_PAGES * EXPECTED_HUGE_PAGE_SIZE_BYTES; + let mut kernel = FakeKernel::new(vec![ + memory(available, 0), + memory(available, RX0_TARGET_PAGES), + ]); + let result = apply_huge_page_policy(&policy(true), &mut kernel).unwrap(); + assert_eq!(result.status, HugePageAuthorityStatusV1::Ready); + assert_eq!(kernel.writes, vec![RX0_TARGET_PAGES]); + } + + #[test] + fn memory_gate_uses_safe_partial_target() { + let safe = 704; + let available = MEMORY_RESERVE_BYTES + safe * EXPECTED_HUGE_PAGE_SIZE_BYTES; + let mut kernel = FakeKernel::new(vec![memory(available, 0), memory(available, 690)]); + let result = apply_huge_page_policy(&policy(true), &mut kernel).unwrap(); + assert_eq!( + result.status, + HugePageAuthorityStatusV1::DegradedInsufficientMemory + ); + assert_eq!(result.target_pages, RX0_TARGET_PAGES); + assert_eq!(result.attempted_pages, safe); + assert_eq!(result.actual_pages, 690); + assert_eq!(kernel.writes, vec![safe]); + } + + #[test] + fn full_attempt_reports_partial_or_zero_truth() { + let available = MEMORY_RESERVE_BYTES + RX0_TARGET_PAGES * EXPECTED_HUGE_PAGE_SIZE_BYTES; + let mut partial = FakeKernel::new(vec![memory(available, 0), memory(available, 896)]); + assert_eq!( + apply_huge_page_policy(&policy(true), &mut partial) + .unwrap() + .status, + HugePageAuthorityStatusV1::DegradedPartialAllocation + ); + let mut zero = FakeKernel::new(vec![memory(available, 0), memory(available, 0)]); + assert_eq!( + apply_huge_page_policy(&policy(true), &mut zero) + .unwrap() + .status, + HugePageAuthorityStatusV1::DegradedUnavailable + ); + assert_eq!(zero.writes, vec![RX0_TARGET_PAGES]); + assert_eq!(zero.waits, POLL_ATTEMPTS - 1); + } + + #[test] + fn disabled_releases_and_verifies() { + let available = 4 * MEMORY_RESERVE_BYTES; + let mut released = FakeKernel::new(vec![memory(available, 12), memory(available, 0)]); + assert_eq!( + apply_huge_page_policy(&policy(false), &mut released) + .unwrap() + .status, + HugePageAuthorityStatusV1::Disabled + ); + assert_eq!(released.writes, vec![0]); + let mut incomplete = FakeKernel::new(vec![memory(available, 12), memory(available, 4)]); + assert_eq!( + apply_huge_page_policy(&policy(false), &mut incomplete) + .unwrap() + .status, + HugePageAuthorityStatusV1::DegradedReleaseIncomplete + ); + } + + #[test] + fn unsupported_algorithm_and_page_size_do_not_write() { + let mut kernel = FakeKernel::new(vec![memory(4 * MEMORY_RESERVE_BYTES, 0)]); + let unsupported = PerformancePolicy { + requested: true, + algorithm: "other".into(), + }; + assert_eq!( + apply_huge_page_policy(&unsupported, &mut kernel) + .unwrap() + .status, + HugePageAuthorityStatusV1::DegradedUnsupported + ); + assert!(kernel.writes.is_empty()); + let mut snapshot = memory(4 * MEMORY_RESERVE_BYTES, 0); + snapshot.huge_page_size_bytes = 1024 * 1024; + let mut kernel = FakeKernel::new(vec![snapshot]); + assert_eq!( + apply_huge_page_policy(&policy(true), &mut kernel) + .unwrap() + .status, + HugePageAuthorityStatusV1::DegradedUnsupported + ); + assert!(kernel.writes.is_empty()); + } + + #[test] + fn configuration_is_strict_and_algorithms_must_agree() { + let policy = br#"{"schema":"rigos.policy/v1","miner_start_mode":"on_boot"}"#; + let config = br#"{"cpu":{"huge-pages":true},"pools":[{"algo":"rx/0"}]}"#; + assert_eq!(parse_policy(policy, config).unwrap().algorithm, "rx/0"); + let ambiguous = br#"{"cpu":{"huge-pages":true},"pools":[{"algo":"rx/0"},{"algo":"rx/1"}]}"#; + assert!(parse_policy(policy, ambiguous).is_err()); + assert!(parse_policy(br#"{}"#, config).is_err()); + } + + #[test] + fn miner_is_stopped_for_mutation_and_restored_only_after_success() { + let available = MEMORY_RESERVE_BYTES + RX0_TARGET_PAGES * EXPECTED_HUGE_PAGE_SIZE_BYTES; + let mut kernel = FakeKernel::new(vec![ + memory(available, 0), + memory(available, RX0_TARGET_PAGES), + ]); + let mut miner = FakeMiner { + active: true, + calls: vec![], + }; + let (_, restore) = apply_with_miner(&policy(true), &mut kernel, &mut miner).unwrap(); + assert!(restore); + assert_eq!(miner.calls, vec!["is_active", "stop"]); + restore_miner_if_needed(&mut miner, restore).unwrap(); + assert_eq!(miner.calls, vec!["is_active", "stop", "start"]); + + let mut kernel = FakeKernel::new(vec![memory(available, 0)]); + let mut inactive = FakeMiner { + active: false, + calls: vec![], + }; + let (_, restore) = apply_with_miner(&policy(true), &mut kernel, &mut inactive).unwrap(); + assert!(!restore); + assert_eq!(inactive.calls, vec!["is_active"]); + } + + #[test] + fn atomic_status_round_trip_preserves_authoritative_truth() { + let root = std::env::temp_dir().join(format!("rigos-performance-{}", Uuid::new_v4())); + let path = root.join("performance-status.json"); + let status = PerformanceStatusV1 { + schema: PERFORMANCE_STATUS_SCHEMA.into(), + boot_id: "boot".into(), + generated_at: "2026-07-06T00:00:00.000Z".into(), + config_revision: "revision".into(), + algorithm: Some("rx/0".into()), + huge_pages: outcome( + true, + RX0_TARGET_PAGES, + RX0_TARGET_PAGES, + &memory(4 * MEMORY_RESERVE_BYTES, RX0_TARGET_PAGES), + HugePageAuthorityStatusV1::Ready, + None, + ), + }; + write_status_verified(&path, &status).unwrap(); + let observed: PerformanceStatusV1 = + serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); + assert_eq!(observed, status); + assert!(fs::read_dir(&root).unwrap().all(|entry| { + !entry + .unwrap() + .file_name() + .to_string_lossy() + .starts_with('.') + })); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + fs::metadata(&path).unwrap().permissions().mode() & 0o777, + 0o644 + ); + } + let _ = fs::remove_dir_all(root); + } + + #[test] + fn hard_failure_leaves_previously_active_miner_stopped() { + let available = MEMORY_RESERVE_BYTES + RX0_TARGET_PAGES * EXPECTED_HUGE_PAGE_SIZE_BYTES; + let mut kernel = FailingKernel { + before: memory(available, 0), + }; + let mut miner = FakeMiner { + active: true, + calls: vec![], + }; + assert!(apply_with_miner(&policy(true), &mut kernel, &mut miner).is_err()); + assert!(!miner.active); + assert_eq!(miner.calls, vec!["is_active", "stop"]); + } +} diff --git a/crates/rigos-performance/src/main.rs b/crates/rigos-performance/src/main.rs new file mode 100644 index 00000000..182e9c89 --- /dev/null +++ b/crates/rigos-performance/src/main.rs @@ -0,0 +1,76 @@ +#![forbid(unsafe_code)] + +use clap::{Parser, Subcommand}; +use rigos_performance::{AuthorityPaths, ProcKernel, SystemdMinerControl, execute}; +use std::{path::PathBuf, process::ExitCode}; + +#[derive(Parser)] +#[command(about = "RIGOS machine performance authority")] +struct Cli { + #[command(subcommand)] + command: Command, + #[arg(long, default_value = "/var/lib/rigos", hide = true)] + state_root: PathBuf, + #[arg( + long, + default_value = "/run/rigos/performance-status.json", + hide = true + )] + status: PathBuf, + #[arg(long, default_value = "/proc/sys/kernel/random/boot_id", hide = true)] + boot_id: PathBuf, + #[arg(long, default_value = "/proc/meminfo", hide = true)] + meminfo: PathBuf, + #[arg(long, default_value = "/proc/sys/vm/nr_hugepages", hide = true)] + nr_hugepages: PathBuf, + #[arg(long, default_value = "/usr/bin/systemctl", hide = true)] + systemctl: PathBuf, +} + +#[derive(Subcommand)] +enum Command { + Hugepages { + #[command(subcommand)] + command: HugepagesCommand, + }, +} + +#[derive(Subcommand)] +enum HugepagesCommand { + Apply, +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + match cli.command { + Command::Hugepages { + command: HugepagesCommand::Apply, + } => { + let paths = AuthorityPaths { + state_root: cli.state_root, + status: cli.status, + boot_id: cli.boot_id, + }; + let mut kernel = ProcKernel { + meminfo: cli.meminfo, + nr_hugepages: cli.nr_hugepages, + }; + let mut miner = SystemdMinerControl { + executable: cli.systemctl, + }; + match execute(&paths, &mut kernel, &mut miner) { + Ok(status) => { + println!( + "{}", + serde_json::to_string(&status).expect("serialize performance status") + ); + ExitCode::SUCCESS + } + Err(error) => { + eprintln!("rigos-performance: {error}"); + ExitCode::FAILURE + } + } + } + } +} diff --git a/crates/rigos-schema/src/lib.rs b/crates/rigos-schema/src/lib.rs index 18444fa8..b1bb01ba 100644 --- a/crates/rigos-schema/src/lib.rs +++ b/crates/rigos-schema/src/lib.rs @@ -19,6 +19,43 @@ pub const COMPONENT_PROVENANCE_SCHEMA: &str = "rigos.component-provenance/v1"; pub const IMAGE_LAYOUT_SCHEMA: &str = "rigos.image-layout/v1"; pub const IMAGE_BUILD_MANIFEST_SCHEMA: &str = "rigos.image-build-manifest/v1"; pub const STATE_LAYOUT_SCHEMA: &str = "rigos.state-layout/v1"; +pub const PERFORMANCE_STATUS_SCHEMA: &str = "rigos.performance-status/v1"; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum HugePageAuthorityStatusV1 { + Ready, + Disabled, + DegradedInsufficientMemory, + DegradedPartialAllocation, + DegradedUnavailable, + DegradedUnsupported, + DegradedReleaseIncomplete, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct HugePageAuthorityV1 { + pub requested: bool, + pub target_pages: u64, + pub attempted_pages: u64, + pub actual_pages: u64, + pub huge_page_size_bytes: u64, + pub memory_available_before_bytes: u64, + pub reserve_bytes: u64, + pub allocation_percent_of_target: f64, + pub status: HugePageAuthorityStatusV1, + pub reason: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct PerformanceStatusV1 { + pub schema: String, + pub boot_id: String, + pub generated_at: String, + pub config_revision: String, + pub algorithm: Option, + pub huge_pages: HugePageAuthorityV1, +} #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ReleaseInfoV1 { @@ -313,6 +350,10 @@ pub fn schemas() -> BTreeMap<&'static str, serde_json::Value> { "state-layout-v1.schema.json", serde_json::to_value(schema_for!(StateLayoutV1)).unwrap(), ); + output.insert( + "performance-status-v1.schema.json", + serde_json::to_value(schema_for!(PerformanceStatusV1)).unwrap(), + ); output.insert( "rig-profile-v1.schema.json", serde_json::to_value(schema_for!(RigProfile)).unwrap(), diff --git a/crates/rigosd/src/lib.rs b/crates/rigosd/src/lib.rs index 1c228106..27cfe499 100644 --- a/crates/rigosd/src/lib.rs +++ b/crates/rigosd/src/lib.rs @@ -6,7 +6,8 @@ use rigos_machine::{MACHINE_SCHEMA, MachineContext}; use rigos_miner::MinerBackend; use rigos_schema::{ ABOUT_SCHEMA, AboutReportV1, COMPONENT_PROVENANCE_SCHEMA, ComponentProvenanceV1, DOCTOR_SCHEMA, - LICENSES_SCHEMA, LicenseEntryV1, LicensesReportV1, ReleaseInfoV1, doctor, + DoctorCheckV1, HugePageAuthorityStatusV1, LICENSES_SCHEMA, LicenseEntryV1, LicensesReportV1, + PERFORMANCE_STATUS_SCHEMA, PerformanceStatusV1, ReleaseInfoV1, doctor, }; use rigos_xmrig::{MINER_SCHEMA, XmrigBackend}; use serde::Serialize; @@ -131,7 +132,9 @@ fn execute(cli: Cli) -> ExitCode { Command::Doctor(output) => { let machine = rigos_machine::inspect(&ctx); let miner = backend.discover(&ctx); - let data = doctor(&machine.diagnostics, &miner.diagnostics); + let mut data = doctor(&machine.diagnostics, &miner.diagnostics); + data.checks.push(load_huge_page_check()); + data.checks.sort_by(|left, right| left.id.cmp(&right.id)); let diagnostics: Vec = machine .diagnostics .into_iter() @@ -188,6 +191,98 @@ fn execute(cli: Cli) -> ExitCode { } } +fn performance_status_path() -> PathBuf { + std::env::var_os("RIGOS_PERFORMANCE_STATUS_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/run/rigos/performance-status.json")) +} + +fn boot_id_path() -> PathBuf { + std::env::var_os("RIGOS_BOOT_ID_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/proc/sys/kernel/random/boot_id")) +} + +fn current_revision_path() -> PathBuf { + std::env::var_os("RIGOS_CURRENT_REVISION_PATH") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("/var/lib/rigos/current")) +} + +fn load_huge_page_check() -> DoctorCheckV1 { + let status = match fs::read(performance_status_path()) { + Ok(value) => value, + Err(error) => { + return failed_huge_page_check(format!("performance status unavailable: {error}")); + } + }; + let boot_id = match fs::read_to_string(boot_id_path()) { + Ok(value) if !value.trim().is_empty() => value.trim().to_owned(), + Ok(_) => return failed_huge_page_check("boot ID is empty".into()), + Err(error) => return failed_huge_page_check(format!("boot ID unavailable: {error}")), + }; + let revision = match fs::read_link(current_revision_path()) { + Ok(value) => match value.file_name().and_then(|value| value.to_str()) { + Some(value) if !value.is_empty() => value.to_owned(), + _ => return failed_huge_page_check("current revision target is invalid".into()), + }, + Err(error) => { + return failed_huge_page_check(format!("current revision unavailable: {error}")); + } + }; + evaluate_huge_page_check(&status, &boot_id, &revision) +} + +fn evaluate_huge_page_check(status: &[u8], boot_id: &str, revision: &str) -> DoctorCheckV1 { + let status: PerformanceStatusV1 = match serde_json::from_slice(status) { + Ok(value) => value, + Err(error) => return failed_huge_page_check(format!("invalid status JSON: {error}")), + }; + if status.schema != PERFORMANCE_STATUS_SCHEMA { + return failed_huge_page_check("performance status schema mismatch".into()); + } + if status.boot_id != boot_id { + return failed_huge_page_check("performance status is from another boot".into()); + } + if status.config_revision != revision { + return failed_huge_page_check("performance status uses another config revision".into()); + } + let level = match status.huge_pages.status { + HugePageAuthorityStatusV1::Ready | HugePageAuthorityStatusV1::Disabled => "pass", + _ => "warning", + }; + DoctorCheckV1 { + id: "performance.huge_pages".into(), + status: level.into(), + summary: format!( + "{} {} of {} pages", + huge_page_status_name(&status.huge_pages.status), + status.huge_pages.actual_pages, + status.huge_pages.target_pages + ), + } +} + +fn huge_page_status_name(status: &HugePageAuthorityStatusV1) -> &'static str { + match status { + HugePageAuthorityStatusV1::Ready => "ready", + HugePageAuthorityStatusV1::Disabled => "disabled", + HugePageAuthorityStatusV1::DegradedInsufficientMemory => "degraded_insufficient_memory", + HugePageAuthorityStatusV1::DegradedPartialAllocation => "degraded_partial_allocation", + HugePageAuthorityStatusV1::DegradedUnavailable => "degraded_unavailable", + HugePageAuthorityStatusV1::DegradedUnsupported => "degraded_unsupported", + HugePageAuthorityStatusV1::DegradedReleaseIncomplete => "degraded_release_incomplete", + } +} + +fn failed_huge_page_check(summary: String) -> DoctorCheckV1 { + DoctorCheckV1 { + id: "performance.huge_pages".into(), + status: "fail".into(), + summary, + } +} + fn release_path() -> PathBuf { std::env::var_os("RIGOS_RELEASE_PATH") .map(PathBuf::from) @@ -388,4 +483,54 @@ mod tests { value.rigos_receives_donation = true; assert!(validate_provenance(value).is_err()); } + + #[test] + fn doctor_exposes_ready_degraded_and_stale_huge_page_truth() { + let status = |kind| PerformanceStatusV1 { + schema: PERFORMANCE_STATUS_SCHEMA.into(), + boot_id: "boot-a".into(), + generated_at: "2026-07-06T00:00:00.000Z".into(), + config_revision: "revision-a".into(), + algorithm: Some("rx/0".into()), + huge_pages: rigos_schema::HugePageAuthorityV1 { + requested: true, + target_pages: 1280, + attempted_pages: 1280, + actual_pages: 1280, + huge_page_size_bytes: 2 * 1024 * 1024, + memory_available_before_bytes: 4 * 1024 * 1024 * 1024, + reserve_bytes: 1024 * 1024 * 1024, + allocation_percent_of_target: 100.0, + status: kind, + reason: None, + }, + }; + let ready = serde_json::to_vec(&status(HugePageAuthorityStatusV1::Ready)).unwrap(); + assert_eq!( + evaluate_huge_page_check(&ready, "boot-a", "revision-a").status, + "pass" + ); + for kind in [ + HugePageAuthorityStatusV1::DegradedInsufficientMemory, + HugePageAuthorityStatusV1::DegradedPartialAllocation, + HugePageAuthorityStatusV1::DegradedUnavailable, + HugePageAuthorityStatusV1::DegradedUnsupported, + HugePageAuthorityStatusV1::DegradedReleaseIncomplete, + ] { + let degraded = serde_json::to_vec(&status(kind)).unwrap(); + assert_eq!( + evaluate_huge_page_check(°raded, "boot-a", "revision-a").status, + "warning" + ); + } + let disabled = serde_json::to_vec(&status(HugePageAuthorityStatusV1::Disabled)).unwrap(); + assert_eq!( + evaluate_huge_page_check(&disabled, "boot-a", "revision-a").status, + "pass" + ); + assert_eq!( + evaluate_huge_page_check(&ready, "boot-b", "revision-a").status, + "fail" + ); + } } diff --git a/docs/performance-authority.md b/docs/performance-authority.md new file mode 100644 index 00000000..906243fa --- /dev/null +++ b/docs/performance-authority.md @@ -0,0 +1,74 @@ +# Alpha.6 performance authority + +RIGOS owns machine-wide huge page policy locally. It does not infer success +from configuration intent or from a successful write. The kernel read-back is +the authoritative result. + +The boot order is: + +```text +rigos-state.service +rigos-profile-apply.service +rigos-hugepages.service +rigos-miner.service +``` + +For `rx/0`, the policy target is 1280 pages of 2 MiB with a 1 GiB +`MemAvailable` reserve. If the full target is unsafe, RIGOS computes a safe +partial attempt while retaining 1280 as policy intent. Partial or unavailable +allocation is visible degradation and does not block mining. + +The authoritative runtime record is: + +```text +/run/rigos/performance-status.json +``` + +It uses `rigos.performance-status/v1` and records the boot ID, config revision, +algorithm, target, safe attempt, actual kernel pages, memory inputs and a stable +status. `rigosctl doctor` rejects records from another boot or config revision. + +Expected degraded states return success so the miner may continue. Unreadable +configuration, unverifiable machine truth or failure to atomically publish and +read back status is a hard service failure and blocks miner startup. + +The authority writes `/proc/sys/vm/nr_hugepages` directly. It does not execute +`sysctl`, match CPU model names, inspect internal filesystems or change the +Flight Sheet v1 schema. + +## Physical gate + +On the test node, capture: + +```bash +cat /run/rigos/performance-status.json +cat /proc/sys/vm/nr_hugepages +grep -E 'MemAvailable|HugePages_Total|HugePages_Free|Hugepagesize' /proc/meminfo +systemctl show rigos-hugepages.service rigos-miner.service -p ActiveState -p SubState +journalctl -b -u rigos-miner.service --no-pager | grep -i 'huge pages' +rigosctl doctor --json +``` + +The gate requires current status matching kernel read-back, XMRig huge-page use +greater than zero, accepted shares with zero rejected shares, successful +reapplication after reboot and unchanged internal-disk layout. + +## Reserved Alpha.6 interfaces + +Later Alpha.6 stages extend this authority without changing its status meaning: + +- native hardware telemetry extends the existing machine inspector and adds + `rigosctl hardware inspect` while preserving `machine inspect`; +- deterministic CPU benchmark observes the existing miner every 10 seconds and + writes `rigos.benchmark/v1` under `/var/lib/rigos/benchmarks` without changing + pool, identity, algorithm, threads or service policy; +- XMRig metrics use an authenticated API bound only to `127.0.0.1:18080`; the + system journal remains event truth for new-job and failure timestamps; +- the conservative watchdog requires multiple persistent fault signals, waits + 10 minutes between recovery attempts and permits at most three restarts per + 30 minutes; +- the final doctor adds boot, state, config, miner, pool, thermal and watchdog + checks without reading or mounting internal filesystems. + +These interfaces are architectural reservations only. Alpha.6 Huge Page +Authority does not enable the API, benchmark, watchdog or expanded telemetry. diff --git a/docs/usb-image-build.md b/docs/usb-image-build.md index b90105f2..7c892878 100644 --- a/docs/usb-image-build.md +++ b/docs/usb-image-build.md @@ -1,7 +1,7 @@ -# RIGOS 0.0.4-alpha.5 USB Appliance +# RIGOS 0.0.4-alpha.6 USB Appliance The authoritative persistent artifact is -`rigos-usb-amd64-0.0.4-alpha.5.img`. +`rigos-usb-amd64-0.0.4-alpha.6.img`. It is a raw MBR image for Rufus DD mode or `dd`. @@ -25,11 +25,11 @@ grow partition 4 after proving the exact boot USB, deterministic MBR disk signature, partition starts, partition types, active flag, filesystem labels and parent block-device topology. -`rigos-recovery-amd64-0.0.4-alpha.5.iso` is stateless recovery media. It is not +`rigos-recovery-amd64-0.0.4-alpha.6.iso` is stateless recovery media. It is not a persistent appliance and never runs the state grow helper. -Alpha five preserves the verified alpha four terminal stream contract and adds -local-only rig profiles, portable XMRig Flight Sheets and offline import. +Alpha six preserves the Alpha five local configuration contract and adds +machine-wide huge page authority with visible requested-versus-actual status. Build from a tracked-clean commit: @@ -37,7 +37,7 @@ Build from a tracked-clean commit: podman build -t rigos-usb-builder -f build/usb/Dockerfile . podman run --rm --privileged \ -v "$PWD:/source" -v /var/tmp/rigos-build:/work rigos-usb-builder -sha256sum -c dist/usb/rigos-usb-amd64-0.0.4-alpha.5.img.sha256 +sha256sum -c dist/usb/rigos-usb-amd64-0.0.4-alpha.6.img.sha256 ``` The builder exports `HEAD` with `git archive`. Untracked workspace files cannot @@ -51,7 +51,8 @@ Historical evidence: 0.0.4-alpha.3 booted normally but stdout piping hid every whiptail screen 0.0.4-alpha.4 terminal stream repair awaiting image and physical retest 0.0.4-alpha.5 local profile and Flight Sheet engine awaiting image test +0.0.4-alpha.6 huge page authority awaiting source and physical gates ``` -Alpha five is not a release candidate. Physical boot, state, internal disk, +Alpha six is not a release candidate. Physical boot, state, internal disk, pool, power-loss and USB write gates still apply. diff --git a/schemas/performance-status-v1.schema.json b/schemas/performance-status-v1.schema.json new file mode 100644 index 00000000..8a9e8ff4 --- /dev/null +++ b/schemas/performance-status-v1.schema.json @@ -0,0 +1,111 @@ +{ + "$defs": { + "HugePageAuthorityStatusV1": { + "enum": [ + "ready", + "disabled", + "degraded_insufficient_memory", + "degraded_partial_allocation", + "degraded_unavailable", + "degraded_unsupported", + "degraded_release_incomplete" + ], + "type": "string" + }, + "HugePageAuthorityV1": { + "properties": { + "actual_pages": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "allocation_percent_of_target": { + "format": "double", + "type": "number" + }, + "attempted_pages": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "huge_page_size_bytes": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "memory_available_before_bytes": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "requested": { + "type": "boolean" + }, + "reserve_bytes": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "status": { + "$ref": "#/$defs/HugePageAuthorityStatusV1" + }, + "target_pages": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "requested", + "target_pages", + "attempted_pages", + "actual_pages", + "huge_page_size_bytes", + "memory_available_before_bytes", + "reserve_bytes", + "allocation_percent_of_target", + "status" + ], + "type": "object" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "algorithm": { + "type": [ + "string", + "null" + ] + }, + "boot_id": { + "type": "string" + }, + "config_revision": { + "type": "string" + }, + "generated_at": { + "type": "string" + }, + "huge_pages": { + "$ref": "#/$defs/HugePageAuthorityV1" + }, + "schema": { + "type": "string" + } + }, + "required": [ + "schema", + "boot_id", + "generated_at", + "config_revision", + "huge_pages" + ], + "title": "PerformanceStatusV1", + "type": "object" +} diff --git a/scripts/build-usb-image.sh b/scripts/build-usb-image.sh index 77850cfd..5ea9bc45 100755 --- a/scripts/build-usb-image.sh +++ b/scripts/build-usb-image.sh @@ -26,7 +26,7 @@ export RUSTFLAGS='-C target-cpu=x86-64' export SOURCE_DATE_EPOCH="$source_epoch" export CARGO_TARGET_DIR="$work/target" cd "$source_root" -cargo build --release --locked -p rigosd -p rigos-state -p rigos-config +cargo build --release --locked -p rigosd -p rigos-state -p rigos-config -p rigos-performance mkdir -p "$live/config/package-lists" "$live/config/includes.chroot" \ "$live/config/hooks/live" "$live/config/bootloaders" @@ -38,6 +38,7 @@ install -D -m 0755 "$CARGO_TARGET_DIR/release/rigosd" "$live/config/includes.chr ln -s rigosd "$live/config/includes.chroot/usr/lib/rigos/rigosctl" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-state-init" "$live/config/includes.chroot/usr/lib/rigos/rigos-state-init" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-config" "$live/config/includes.chroot/usr/lib/rigos/rigos-config" +install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-performance" "$live/config/includes.chroot/usr/lib/rigos/rigos-performance" ln -s /run/live/medium/image-layout.json "$live/config/includes.chroot/usr/lib/rigos/image-layout.json" xmrig_archive="xmrig-${RIGOS_XMRIG_VERSION}-linux-static-x64.tar.gz" diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index e8863a1a..dca272aa 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -75,11 +75,12 @@ cmp "$temporary/a/image-layout.json" "$temporary/b/image-layout.json" unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \ etc/rigos-release etc/os-release \ etc/systemd/system/rigos-state.service \ + etc/systemd/system/rigos-hugepages.service \ etc/systemd/system/rigos-miner.service \ etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-config usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' @@ -93,6 +94,15 @@ strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/usr/bin/pyt strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/usr/lib/rigos/lsblk-compat' >/dev/null || die 'state initializer does not use the packaged compatibility wrapper' strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F -- '--tree' >/dev/null || die 'state initializer does not require hierarchical lsblk output' if strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/run/rigos/compat-bin/lsblk' >/dev/null; then die 'state initializer executes compatibility code from the runtime directory'; fi +[[ -x "$temporary/root/usr/lib/rigos/rigos-performance" ]] || die 'performance authority is missing or not executable' +grep -Fq 'After=rigos-state.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority ordering is missing' +grep -Fq 'Before=rigos-miner.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority is not ordered before miner' +grep -Fq 'Requires=rigos-state.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority dependencies are missing' +grep -Fq 'Requires=rigos-state.service rigos-hugepages.service' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner does not require huge page authority' +strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F '/proc/sys/vm/nr_hugepages' >/dev/null || die 'performance authority does not use direct kernel huge page control' +strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'rigos.performance-status/v1' >/dev/null || die 'performance authority status contract is missing' +if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'sysctl' >/dev/null; then die 'performance authority shells out to sysctl'; fi +if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -Ei '(/dev/sd|/dev/nvme|cpu model)' >/dev/null; then die 'performance authority contains hardware-name or internal-disk targeting'; fi grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner policy gate is missing' [[ -f "$temporary/root/etc/systemd/system/rigos-profile-apply.service" ]] || die 'profile apply service is missing' grep -Fq 'rigos-config recover' "$temporary/root/etc/systemd/system/rigos-profile-apply.service" || die 'pending transaction recovery is missing' diff --git a/scripts/verify.sh b/scripts/verify.sh index 03bf2d50..a3d35ed1 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -24,6 +24,13 @@ grep -Fq 'partuuid' crates/rigos-state/src/main.rs grep -Fq '.pending-transaction.json' crates/rigos-config/src/main.rs grep -Fq 'engine("transact"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +grep -Fq 'rigos-hugepages.service' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +grep -Fq '/proc/sys/vm/nr_hugepages' crates/rigos-performance/src/lib.rs +grep -Fq 'PERFORMANCE_STATUS_SCHEMA' crates/rigos-performance/src/lib.rs +if rg -n 'Command::new\([^)]*sysctl|/dev/(sd|nvme)|cpu model' crates/rigos-performance; then + echo "performance authority violates the kernel-only hardware-neutral contract" >&2 + exit 1 +fi if rg -n '(HIVE_HOST_URL|API_HOST_URLS|RIG_PASSWD|HSSH_SRV)=' configs docs/local-rig-config.md; then echo "Hive cloud or rig credential field leaked into RIGOS configuration" >&2 exit 1 From 0a6271732a63ab0f5960bc92a098e78f6a59883a Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 13:42:08 +0700 Subject: [PATCH 002/386] runtime preserve shared rig directory --- build/usb/hooks/010-rigos.chroot | 3 ++- .../etc/systemd/system/rigos-miner.service | 2 -- .../etc/systemd/system/rigos-state.service | 5 ++--- .../usb/includes.chroot/usr/lib/tmpfiles.d/rigos.conf | 1 + crates/rigos-state/tests/lsblk_compat.rs | 10 +++++++--- 5 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 build/usb/includes.chroot/usr/lib/tmpfiles.d/rigos.conf diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 47886746..cd04d7ae 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -4,8 +4,9 @@ set -eu useradd --system --home-dir /var/lib/rigos --shell /usr/sbin/nologin --user-group rigos useradd --create-home --shell /bin/bash --groups sudo rigosadmin passwd --lock rigosadmin -install -d -o rigos -g rigos -m 0750 /var/lib/rigos /run/rigos +install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos +systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf chmod 0755 /usr/local/sbin/rigos-firstboot /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig systemctl enable NetworkManager.service rigos-state.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service index acb5e909..dcdbce43 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service @@ -25,8 +25,6 @@ RestrictNamespaces=yes LockPersonality=yes MemoryDenyWriteExecute=no ReadWritePaths=/var/lib/rigos -RuntimeDirectory=rigos -RuntimeDirectoryMode=0750 [Install] WantedBy=multi-user.target diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service index 5bba5cd9..d3334609 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service @@ -1,13 +1,12 @@ [Unit] Description=Mount RIGOS persistent state DefaultDependencies=no -After=systemd-udev-settle.service local-fs-pre.target +After=systemd-udevd.service local-fs-pre.target Before=local-fs.target rigos-firstboot.service rigos-miner.service [Service] Type=oneshot -RuntimeDirectory=rigos -RuntimeDirectoryMode=0755 +ExecStartPre=/usr/bin/systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf ExecStart=/usr/lib/rigos/rigos-state-init RemainAfterExit=yes diff --git a/build/usb/includes.chroot/usr/lib/tmpfiles.d/rigos.conf b/build/usb/includes.chroot/usr/lib/tmpfiles.d/rigos.conf new file mode 100644 index 00000000..e746490d --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/tmpfiles.d/rigos.conf @@ -0,0 +1 @@ +d /run/rigos 0755 root root - diff --git a/crates/rigos-state/tests/lsblk_compat.rs b/crates/rigos-state/tests/lsblk_compat.rs index f45dbdad..231a223e 100644 --- a/crates/rigos-state/tests/lsblk_compat.rs +++ b/crates/rigos-state/tests/lsblk_compat.rs @@ -14,6 +14,7 @@ fn state_service_wires_debian_lsblk_compatibility() { let service_path = repo_path("build/usb/includes.chroot/etc/systemd/system/rigos-state.service"); let wrapper_path = repo_path("build/usb/includes.chroot/usr/lib/rigos/lsblk-compat"); + let tmpfiles_path = repo_path("build/usb/includes.chroot/usr/lib/tmpfiles.d/rigos.conf"); let state_source_path = repo_path("crates/rigos-state/src/main.rs"); let service = fs::read_to_string(&service_path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", service_path.display())); @@ -21,10 +22,13 @@ fn state_service_wires_debian_lsblk_compatibility() { .unwrap_or_else(|error| panic!("failed to read {}: {error}", wrapper_path.display())); let state_source = fs::read_to_string(&state_source_path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", state_source_path.display())); + let tmpfiles = fs::read_to_string(&tmpfiles_path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", tmpfiles_path.display())); - assert!(service.contains("RuntimeDirectory=rigos")); - assert!(service.contains("RuntimeDirectoryMode=0755")); - assert!(!service.contains("ExecStartPre=")); + assert!(!service.contains("RuntimeDirectory=rigos")); + assert!(service.contains("systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf")); + assert_eq!(tmpfiles.trim(), "d /run/rigos 0755 root root -"); + assert!(!service.contains("ExecStartPre=/usr/bin/install")); assert!(!service.contains("Environment=PATH=")); assert!(!service.contains("/run/rigos/compat-bin/lsblk")); assert!(wrapper.contains("/usr/bin/lsblk")); From 77adc45137bb700c821401542f21a36df32acd17 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 13:53:35 +0700 Subject: [PATCH 003/386] state gate exact persistent readiness --- build/usb/hooks/010-rigos.chroot | 4 +- .../systemd/system/rigos-firstboot.service | 4 +- .../systemd/system/rigos-hugepages.service | 4 +- .../etc/systemd/system/rigos-miner.service | 4 +- .../system/rigos-profile-apply.service | 4 +- .../systemd/system/rigos-state-ready.service | 14 ++ .../etc/systemd/system/rigos-state.service | 2 +- crates/rigos-state/Cargo.toml | 4 + crates/rigos-state/src/lib.rs | 151 ++++++++++++- crates/rigos-state/src/main.rs | 201 +++++++++++++++++- crates/rigos-state/src/ready.rs | 157 ++++++++++++++ scripts/build-usb-image.sh | 1 + scripts/verify-usb-appliance.sh | 9 +- 13 files changed, 535 insertions(+), 24 deletions(-) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service create mode 100644 crates/rigos-state/src/ready.rs diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index cd04d7ae..5be849d8 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,8 +8,8 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service rigos-state.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service rigos-state.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service index 0738a1a6..1b8fa663 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service @@ -1,8 +1,8 @@ [Unit] Description=RIGOS first-boot configuration -After=network-online.target rigos-state.service rigos-profile-apply.service +After=network-online.target rigos-state-ready.service rigos-profile-apply.service Wants=network-online.target -Requires=rigos-state.service +Requires=rigos-state-ready.service Before=getty@tty1.service ConditionPathExists=!/var/lib/rigos/policy.json diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service b/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service index 4bde44d7..d1a39d60 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-hugepages.service @@ -1,7 +1,7 @@ [Unit] Description=Apply RIGOS huge page authority -After=rigos-state.service rigos-profile-apply.service -Requires=rigos-state.service rigos-profile-apply.service +After=rigos-state-ready.service rigos-profile-apply.service +Requires=rigos-state-ready.service rigos-profile-apply.service Before=rigos-miner.service [Service] diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service index dcdbce43..c8de4c57 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service @@ -1,8 +1,8 @@ [Unit] Description=RIGOS managed XMRig -After=network-online.target rigos-state.service rigos-profile-apply.service rigos-hugepages.service +After=network-online.target rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service Wants=network-online.target -Requires=rigos-state.service rigos-hugepages.service +Requires=rigos-state-ready.service rigos-hugepages.service ConditionPathExists=/var/lib/rigos/xmrig.json [Service] diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service b/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service index 9cdd6b67..3ade9b8e 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service @@ -1,7 +1,7 @@ [Unit] Description=Apply RIGOS persistent machine profile -After=rigos-state.service -Requires=rigos-state.service +After=rigos-state-ready.service +Requires=rigos-state-ready.service Before=rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service new file mode 100644 index 00000000..5b074a87 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service @@ -0,0 +1,14 @@ +[Unit] +Description=Verify RIGOS persistent state readiness +DefaultDependencies=no +After=rigos-state.service +Requires=rigos-state.service +Before=local-fs.target rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-state-ready +RemainAfterExit=yes + +[Install] +WantedBy=local-fs.target diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service index d3334609..973527cf 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service @@ -2,7 +2,7 @@ Description=Mount RIGOS persistent state DefaultDependencies=no After=systemd-udevd.service local-fs-pre.target -Before=local-fs.target rigos-firstboot.service rigos-miner.service +Before=local-fs.target rigos-state-ready.service [Service] Type=oneshot diff --git a/crates/rigos-state/Cargo.toml b/crates/rigos-state/Cargo.toml index 08f93bbe..6321aab4 100644 --- a/crates/rigos-state/Cargo.toml +++ b/crates/rigos-state/Cargo.toml @@ -18,3 +18,7 @@ uuid.workspace = true [[bin]] name = "rigos-state-init" path = "src/main.rs" + +[[bin]] +name = "rigos-state-ready" +path = "src/ready.rs" diff --git a/crates/rigos-state/src/lib.rs b/crates/rigos-state/src/lib.rs index 5eb406b0..bf03ac9c 100644 --- a/crates/rigos-state/src/lib.rs +++ b/crates/rigos-state/src/lib.rs @@ -2,7 +2,7 @@ use rigos_schema::ImageLayoutV1; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use thiserror::Error; #[derive(Debug, Clone, Deserialize)] @@ -68,6 +68,7 @@ pub struct VerifiedLayout { pub efi_partuuid: String, pub root_major_minor: String, pub state_path: String, + pub state_major_minor: String, pub state_start_lba: u64, pub state_size_lba: u64, pub state_unique_guid: String, @@ -85,6 +86,90 @@ pub enum StateOutcome { BlockedAmbiguousBootDevice, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AttestedStatePartitionV1 { + pub path: String, + pub major_minor: String, + pub partuuid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct BootDeviceAttestationV1 { + pub schema: String, + pub boot_id: String, + pub verification_outcome: String, + pub state: AttestedStatePartitionV1, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct StateStatusV1 { + pub schema: String, + pub boot_id: String, + pub outcome: String, + pub action: Option, + pub message: Option, + pub device: Option, + pub partuuid: Option, + pub mountpoint: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StateReadyObservation { + pub boot_id: String, + pub source_major_minor: String, + pub source_partuuid: String, + pub filesystem: String, + pub label: String, + pub mountpoint: String, + pub mount_options: BTreeSet, +} + +pub fn validate_state_ready( + status: &StateStatusV1, + attestation: &BootDeviceAttestationV1, + observed: &StateReadyObservation, +) -> Result<(), &'static str> { + if status.schema != "rigos.state-status/v1" + || status.outcome != "ready" + || status.boot_id != observed.boot_id + { + return Err("state status is not ready for the current boot"); + } + if attestation.schema != "rigos.boot-device/v1" + || attestation.verification_outcome != "verified" + || attestation.boot_id != observed.boot_id + { + return Err("boot-device attestation is not current and verified"); + } + if !attestation + .state + .partuuid + .eq_ignore_ascii_case(&observed.source_partuuid) + || attestation.state.major_minor != observed.source_major_minor + { + return Err("mounted state identity differs from attestation"); + } + if observed.filesystem != "ext4" + || observed.label != "RIGOS_STATE" + || observed.mountpoint != "/var/lib/rigos" + { + return Err("mounted state filesystem contract mismatch"); + } + let required = ["rw", "nosuid", "nodev", "noexec", "noatime"]; + if required + .iter() + .any(|option| !observed.mount_options.contains(*option)) + { + return Err("mounted state options are incomplete"); + } + if status.partuuid.as_deref() != Some(attestation.state.partuuid.as_str()) + || status.mountpoint.as_deref() != Some("/var/lib/rigos") + { + return Err("state status identity is incomplete"); + } + Ok(()) +} + #[derive(Debug, Error, PartialEq, Eq)] pub enum LayoutError { #[error("exact boot parent could not be proven")] @@ -285,6 +370,7 @@ fn validate_layout_with_state_mount( efi_partuuid: efi.partuuid.clone().unwrap_or_default(), root_major_minor: boot_major_minor.to_owned(), state_path: state.path.clone(), + state_major_minor: state.major_minor.clone(), state_start_lba: state.start.unwrap_or_default(), state_size_lba: state.size / u64::from(manifest.logical_sector_size), state_unique_guid: state.partuuid.clone().unwrap_or_default(), @@ -558,4 +644,67 @@ mod tests { Err(LayoutError::UndersizedMedia) ); } + + fn ready_contract() -> ( + StateStatusV1, + BootDeviceAttestationV1, + StateReadyObservation, + ) { + ( + StateStatusV1 { + schema: "rigos.state-status/v1".into(), + boot_id: "boot-a".into(), + outcome: "ready".into(), + action: Some("grown".into()), + message: None, + device: Some("/dev/sdb4".into()), + partuuid: Some("5249474f-04".into()), + mountpoint: Some("/var/lib/rigos".into()), + }, + BootDeviceAttestationV1 { + schema: "rigos.boot-device/v1".into(), + boot_id: "boot-a".into(), + verification_outcome: "verified".into(), + state: AttestedStatePartitionV1 { + path: "/dev/sdb4".into(), + major_minor: "8:20".into(), + partuuid: "5249474f-04".into(), + }, + }, + StateReadyObservation { + boot_id: "boot-a".into(), + source_major_minor: "8:20".into(), + source_partuuid: "5249474f-04".into(), + filesystem: "ext4".into(), + label: "RIGOS_STATE".into(), + mountpoint: "/var/lib/rigos".into(), + mount_options: ["rw", "nosuid", "nodev", "noexec", "noatime"] + .into_iter() + .map(str::to_owned) + .collect(), + }, + ) + } + + #[test] + fn state_ready_requires_current_exact_mount() { + let (status, attestation, observed) = ready_contract(); + assert_eq!( + validate_state_ready(&status, &attestation, &observed), + Ok(()) + ); + } + + #[test] + fn state_ready_rejects_stale_or_mismatched_truth() { + let (status, attestation, mut observed) = ready_contract(); + observed.boot_id = "boot-b".into(); + assert!(validate_state_ready(&status, &attestation, &observed).is_err()); + let (_, _, mut observed) = ready_contract(); + observed.source_partuuid = "deadbeef-04".into(); + assert!(validate_state_ready(&status, &attestation, &observed).is_err()); + let (_, _, mut observed) = ready_contract(); + observed.mount_options.remove("noexec"); + assert!(validate_state_ready(&status, &attestation, &observed).is_err()); + } } diff --git a/crates/rigos-state/src/main.rs b/crates/rigos-state/src/main.rs index f0e92b65..44d51efc 100644 --- a/crates/rigos-state/src/main.rs +++ b/crates/rigos-state/src/main.rs @@ -32,6 +32,8 @@ struct Args { status: PathBuf, #[arg(long, default_value = "/run/rigos/boot-device.json")] attestation: PathBuf, + #[arg(long, default_value = "/dev/disk/by-partuuid")] + partuuid_root: PathBuf, #[arg(long)] dry_run: bool, #[arg(long)] @@ -49,25 +51,69 @@ struct FindmntEntry { major_minor: String, } +#[derive(Deserialize)] +struct StateMountDocument { + filesystems: Vec, +} + +#[derive(Deserialize)] +struct StateMountEntry { + source: String, + #[serde(rename = "maj:min")] + major_minor: String, + fstype: String, + options: String, + target: String, +} + fn main() -> ExitCode { let args = Args::parse(); let _ = fs::remove_file(&args.attestation); - let (outcome, message) = match execute(&args) { - Ok(outcome) => (outcome, None), + let boot_id = fs::read_to_string("/proc/sys/kernel/random/boot_id") + .unwrap_or_default() + .trim() + .to_owned(); + let (outcome, action, message) = match execute(&args) { + Ok(StateOutcome::Grown) => (StateOutcome::Ready, Some("grown"), None), + Ok(StateOutcome::Ready) => (StateOutcome::Ready, Some("unchanged"), None), + Ok(outcome) => (outcome, None, None), Err( error @ (InitError::Layout(LayoutError::AmbiguousBootDevice) | InitError::Discovery(_)), ) => ( StateOutcome::BlockedAmbiguousBootDevice, + None, Some(error.to_string()), ), - Err(error @ InitError::Layout(_)) => { - (StateOutcome::BlockedLayoutMismatch, Some(error.to_string())) - } - Err(error) => (StateOutcome::LimitedCapacity, Some(error.to_string())), + Err(error @ InitError::Layout(_)) => ( + StateOutcome::BlockedLayoutMismatch, + None, + Some(error.to_string()), + ), + Err(error) => (StateOutcome::LimitedCapacity, None, Some(error.to_string())), }; + let attestation: Option = fs::read(&args.attestation) + .ok() + .and_then(|bytes| serde_json::from_slice(&bytes).ok()); + let device = attestation + .as_ref() + .and_then(|value| value.pointer("/state/path")) + .and_then(serde_json::Value::as_str); + let partuuid = attestation + .as_ref() + .and_then(|value| value.pointer("/state/partuuid")) + .and_then(serde_json::Value::as_str); let _ = write_atomic( &args.status, - &json!({"schema":"rigos.state-status/v1","outcome":outcome,"message":message}), + &json!({ + "schema":"rigos.state-status/v1", + "boot_id":boot_id, + "outcome":outcome, + "action":action, + "message":message, + "device":device, + "partuuid":partuuid, + "mountpoint":if outcome == StateOutcome::Ready { Some(path_str(&args.mountpoint).unwrap_or("/var/lib/rigos")) } else { None }, + }), ); println!( "{}", @@ -124,7 +170,7 @@ fn execute(args: &Args) -> Result { .map(|device| device.path.clone()) .ok_or_else(|| InitError::Discovery("boot parent disk was not found".into()))?; let table = read_sfdisk(&boot_disk_path)?; - let verified = if args.attestation_only { + let mut verified = if args.attestation_only { validate_layout_for_attestation( &manifest, &observed, @@ -163,6 +209,11 @@ fn execute(args: &Args) -> Result { "partuuid":verified.efi_partuuid, }, "root":{"major_minor":verified.root_major_minor}, + "state":{ + "path":verified.state_path, + "major_minor":verified.state_major_minor, + "partuuid":verified.state_unique_guid, + }, }), )?; #[cfg(unix)] @@ -174,6 +225,14 @@ fn execute(args: &Args) -> Result { return Ok(StateOutcome::Ready); } + let resolved_state = wait_for_verified_state_device( + &args.partuuid_root, + &verified.state_unique_guid, + Duration::from_secs(10), + )?; + validate_exact_state_device(&resolved_state, &verified)?; + verified.state_path = path_str(&resolved_state)?.to_owned(); + let disk = OpenOptions::new() .read(true) .write(true) @@ -211,7 +270,11 @@ fn execute(args: &Args) -> Result { None, &[0], )?; - run("udevadm", &["settle", "--timeout=10"], None, &[0])?; + let resolved_state = wait_for_verified_state_device( + &args.partuuid_root, + &verified.state_unique_guid, + Duration::from_secs(10), + )?; let refreshed_devices = read_lsblk()?; let refreshed_table = read_sfdisk(&verified.disk_path)?; @@ -231,6 +294,8 @@ fn execute(args: &Args) -> Result { "post-grow geometry did not match the intended monotonic update".into(), )); } + validate_exact_state_device(&resolved_state, &refreshed)?; + verified.state_path = path_str(&resolved_state)?.to_owned(); grown = true; } @@ -267,6 +332,7 @@ fn execute(args: &Args) -> Result { &[0], )?; } + verify_state_mount(&verified, &args.mountpoint)?; initialize_state(&manifest, &verified, intended_size, &args.mountpoint)?; FileExt::unlock(&disk)?; Ok(if grown { @@ -276,6 +342,123 @@ fn execute(args: &Args) -> Result { }) } +fn verify_state_mount(verified: &VerifiedLayout, mountpoint: &Path) -> Result<(), InitError> { + let document: StateMountDocument = serde_json::from_slice(&run( + "findmnt", + &[ + "--json", + "--target", + path_str(mountpoint)?, + "--output", + "SOURCE,MAJ:MIN,FSTYPE,OPTIONS,TARGET", + ], + None, + &[0], + )?)?; + let mount = document + .filesystems + .first() + .ok_or_else(|| InitError::Discovery("persistent state is not mounted".into()))?; + let source = fs::canonicalize(&mount.source)?; + let expected = fs::canonicalize(&verified.state_path)?; + let required = ["rw", "nosuid", "nodev", "noexec", "noatime"]; + let options: std::collections::BTreeSet<_> = mount.options.split(',').collect(); + if source != expected + || mount.major_minor != verified.state_major_minor + || mount.fstype != "ext4" + || mount.target != path_str(mountpoint)? + || required.iter().any(|value| !options.contains(value)) + { + return Err(InitError::Discovery( + "mounted state does not match the verified identity and options".into(), + )); + } + Ok(()) +} + +fn wait_for_verified_state_device( + root: &Path, + partuuid: &str, + timeout: Duration, +) -> Result { + if partuuid.is_empty() + || partuuid.len() > 128 + || !partuuid + .bytes() + .all(|byte| byte.is_ascii_hexdigit() || byte == b'-') + { + return Err(InitError::Discovery( + "verified state PARTUUID is invalid".into(), + )); + } + let candidate = root.join(partuuid); + let deadline = Instant::now() + timeout; + loop { + if candidate.symlink_metadata().is_ok() { + return fs::canonicalize(&candidate).map_err(InitError::Io); + } + if Instant::now() >= deadline { + return Err(InitError::Discovery( + "verified state PARTUUID did not appear before timeout".into(), + )); + } + thread::sleep(Duration::from_millis(100)); + } +} + +fn validate_exact_state_device( + resolved: &Path, + verified: &VerifiedLayout, +) -> Result<(), InitError> { + let expected = fs::canonicalize(&verified.state_path)?; + if expected != resolved { + return Err(InitError::Discovery( + "verified PARTUUID resolved to another block device".into(), + )); + } + let observed = read_lsblk()?; + let child = observed + .blockdevices + .iter() + .find(|disk| disk.major_minor == verified.disk_major_minor) + .and_then(|disk| { + disk.children.iter().find(|child| { + child.major_minor == verified.state_major_minor + && child.partuuid.as_deref().is_some_and(|value| { + value.eq_ignore_ascii_case(&verified.state_unique_guid) + }) + }) + }) + .ok_or_else(|| InitError::Discovery("verified state block identity changed".into()))?; + if fs::canonicalize(&child.path)? != resolved { + return Err(InitError::Discovery( + "lsblk state path differs from verified PARTUUID".into(), + )); + } + let properties = String::from_utf8_lossy(&run( + "blkid", + &["-o", "export", path_str(resolved)?], + None, + &[0], + )?) + .into_owned(); + let property = |name: &str| { + properties + .lines() + .find_map(|line| line.strip_prefix(&format!("{name}="))) + }; + if property("TYPE") != Some("ext4") + || !matches!(property("LABEL"), Some("RIGOS_STATE_SEED" | "RIGOS_STATE")) + || !property("PARTUUID") + .is_some_and(|value| value.eq_ignore_ascii_case(&verified.state_unique_guid)) + { + return Err(InitError::Discovery( + "verified state filesystem identity changed".into(), + )); + } + Ok(()) +} + fn read_lsblk() -> Result { let raw = run( "/usr/bin/python3", diff --git a/crates/rigos-state/src/ready.rs b/crates/rigos-state/src/ready.rs new file mode 100644 index 00000000..dbefbb69 --- /dev/null +++ b/crates/rigos-state/src/ready.rs @@ -0,0 +1,157 @@ +#![forbid(unsafe_code)] + +use clap::Parser; +use rigos_state::{ + BootDeviceAttestationV1, StateReadyObservation, StateStatusV1, validate_state_ready, +}; +use serde::Deserialize; +use std::{ + fs, + path::{Path, PathBuf}, + process::{Command, ExitCode}, +}; + +#[derive(Parser)] +struct Args { + #[arg(long, default_value = "/run/rigos/state-status.json")] + status: PathBuf, + #[arg(long, default_value = "/run/rigos/boot-device.json")] + attestation: PathBuf, + #[arg(long, default_value = "/proc/sys/kernel/random/boot_id")] + boot_id: PathBuf, + #[arg(long, default_value = "/dev/disk/by-partuuid")] + partuuid_root: PathBuf, + #[arg(long, default_value = "/var/lib/rigos")] + mountpoint: PathBuf, +} + +#[derive(Deserialize)] +struct FindmntDocument { + filesystems: Vec, +} + +#[derive(Deserialize)] +struct FindmntEntry { + source: String, + #[serde(rename = "maj:min")] + major_minor: String, + fstype: String, + options: String, + target: String, +} + +fn main() -> ExitCode { + match verify(&Args::parse()) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("rigos-state-ready: {error}"); + ExitCode::FAILURE + } + } +} + +fn verify(args: &Args) -> Result<(), String> { + let status: StateStatusV1 = read_json(&args.status)?; + let attestation: BootDeviceAttestationV1 = read_json(&args.attestation)?; + let boot_id = fs::read_to_string(&args.boot_id) + .map_err(|error| error.to_string())? + .trim() + .to_owned(); + let partuuid_path = safe_partuuid_path(&args.partuuid_root, &attestation.state.partuuid)?; + let resolved = fs::canonicalize(&partuuid_path).map_err(|error| error.to_string())?; + let blkid = output("/usr/sbin/blkid", &["-o", "export", path_text(&resolved)?])?; + let properties = parse_properties(&blkid); + let observed_partuuid = properties + .get("PARTUUID") + .ok_or_else(|| "mounted state PARTUUID is unavailable".to_owned())? + .to_owned(); + let label = properties + .get("LABEL") + .ok_or_else(|| "mounted state label is unavailable".to_owned())? + .to_owned(); + let filesystem = properties + .get("TYPE") + .ok_or_else(|| "mounted state filesystem is unavailable".to_owned())? + .to_owned(); + let findmnt: FindmntDocument = serde_json::from_slice(&output_bytes( + "/usr/bin/findmnt", + &[ + "--json", + "--target", + path_text(&args.mountpoint)?, + "--output", + "SOURCE,MAJ:MIN,FSTYPE,OPTIONS,TARGET", + ], + )?) + .map_err(|error| error.to_string())?; + let mount = findmnt + .filesystems + .first() + .ok_or_else(|| "persistent state is not mounted".to_owned())?; + if fs::canonicalize(&mount.source).map_err(|error| error.to_string())? != resolved { + return Err("mount source differs from verified PARTUUID".into()); + } + let observed = StateReadyObservation { + boot_id, + source_major_minor: mount.major_minor.clone(), + source_partuuid: observed_partuuid, + filesystem: if mount.fstype == filesystem { + filesystem + } else { + return Err("findmnt and blkid filesystem disagree".into()); + }, + label, + mountpoint: mount.target.clone(), + mount_options: mount.options.split(',').map(str::to_owned).collect(), + }; + validate_state_ready(&status, &attestation, &observed).map_err(str::to_owned) +} + +fn safe_partuuid_path(root: &Path, partuuid: &str) -> Result { + if partuuid.is_empty() + || partuuid.len() > 128 + || !partuuid + .bytes() + .all(|byte| byte.is_ascii_hexdigit() || byte == b'-') + { + return Err("attested state PARTUUID is invalid".into()); + } + Ok(root.join(partuuid)) +} + +fn read_json Deserialize<'de>>(path: &Path) -> Result { + let bytes = fs::read(path).map_err(|error| error.to_string())?; + if bytes.len() > 64 * 1024 { + return Err("runtime state exceeds its size limit".into()); + } + serde_json::from_slice(&bytes).map_err(|error| error.to_string()) +} + +fn path_text(path: &Path) -> Result<&str, String> { + path.to_str() + .ok_or_else(|| "path is not valid UTF-8".to_owned()) +} + +fn parse_properties(value: &str) -> std::collections::BTreeMap { + value + .lines() + .filter_map(|line| line.split_once('=')) + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect() +} + +fn output(program: &str, arguments: &[&str]) -> Result { + String::from_utf8(output_bytes(program, arguments)?).map_err(|error| error.to_string()) +} + +fn output_bytes(program: &str, arguments: &[&str]) -> Result, String> { + let result = Command::new(program) + .args(arguments) + .output() + .map_err(|error| error.to_string())?; + if result.status.success() { + Ok(result.stdout) + } else { + Err(format!("{program} failed")) + } +} diff --git a/scripts/build-usb-image.sh b/scripts/build-usb-image.sh index 5ea9bc45..aec06a15 100755 --- a/scripts/build-usb-image.sh +++ b/scripts/build-usb-image.sh @@ -37,6 +37,7 @@ cp -a build/usb/bootloaders/. "$live/config/bootloaders/" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigosd" "$live/config/includes.chroot/usr/lib/rigos/rigosd" ln -s rigosd "$live/config/includes.chroot/usr/lib/rigos/rigosctl" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-state-init" "$live/config/includes.chroot/usr/lib/rigos/rigos-state-init" +install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-state-ready" "$live/config/includes.chroot/usr/lib/rigos/rigos-state-ready" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-config" "$live/config/includes.chroot/usr/lib/rigos/rigos-config" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-performance" "$live/config/includes.chroot/usr/lib/rigos/rigos-performance" ln -s /run/live/medium/image-layout.json "$live/config/includes.chroot/usr/lib/rigos/image-layout.json" diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index dca272aa..6d7ec339 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -75,18 +75,19 @@ cmp "$temporary/a/image-layout.json" "$temporary/b/image-layout.json" unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \ etc/rigos-release etc/os-release \ etc/systemd/system/rigos-state.service \ + etc/systemd/system/rigos-state-ready.service \ etc/systemd/system/rigos-hugepages.service \ etc/systemd/system/rigos-miner.service \ etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" -grep -Fq 'RuntimeDirectory=rigos' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime directory is missing' -grep -Fq 'RuntimeDirectoryMode=0755' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime directory mode is missing' +grep -Fq 'systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime tmpfiles setup is missing' +if rg -q '^RuntimeDirectory=rigos$' "$temporary/root/etc/systemd/system"; then die 'a service owns the shared runtime directory'; fi if grep -Eq '^(ExecStartPre|Environment=PATH)=.*compat-bin' "$temporary/root/etc/systemd/system/rigos-state.service"; then die 'state unit executes compatibility code from the runtime directory'; fi [[ -f "$temporary/root/usr/lib/rigos/lsblk-compat" ]] || die 'state lsblk compatibility wrapper is missing' [[ -x "$temporary/root/usr/bin/python3" ]] || die 'Python runtime for state compatibility is missing' @@ -94,6 +95,8 @@ strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/usr/bin/pyt strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/usr/lib/rigos/lsblk-compat' >/dev/null || die 'state initializer does not use the packaged compatibility wrapper' strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F -- '--tree' >/dev/null || die 'state initializer does not require hierarchical lsblk output' if strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/run/rigos/compat-bin/lsblk' >/dev/null; then die 'state initializer executes compatibility code from the runtime directory'; fi +grep -Fq 'ExecStart=/usr/lib/rigos/rigos-state-ready' "$temporary/root/etc/systemd/system/rigos-state-ready.service" || die 'state readiness verifier is not wired' +grep -Fq 'Requires=rigos-state-ready.service' "$temporary/root/etc/systemd/system/rigos-profile-apply.service" || die 'profile apply bypasses state readiness' [[ -x "$temporary/root/usr/lib/rigos/rigos-performance" ]] || die 'performance authority is missing or not executable' grep -Fq 'After=rigos-state.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority ordering is missing' grep -Fq 'Before=rigos-miner.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority is not ordered before miner' From 14ee20da479f3140260256df143fd5f0346d4fb2 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 14:07:50 +0700 Subject: [PATCH 004/386] config make firstboot retries idempotent --- build/usb/hooks/010-rigos.chroot | 4 +- .../systemd/system/rigos-firstboot.service | 2 +- .../system/rigos-profile-apply.service | 1 - .../system/rigos-recovery-access.service | 18 + .../systemd/system/rigos-state-ready.service | 3 +- .../usr/local/sbin/rigos-firstboot | 35 +- .../usr/local/sbin/rigos-recovery-access | 87 ++++ crates/rigos-config/src/main.rs | 473 +++++++----------- scripts/build-usb-image.sh | 2 +- scripts/verify-usb-appliance.sh | 7 +- scripts/verify.sh | 16 +- 11 files changed, 335 insertions(+), 313 deletions(-) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service create mode 100644 build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 5be849d8..80432008 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,8 +8,8 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service rigos-state.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service index 1b8fa663..060ce596 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service @@ -4,10 +4,10 @@ After=network-online.target rigos-state-ready.service rigos-profile-apply.servic Wants=network-online.target Requires=rigos-state-ready.service Before=getty@tty1.service -ConditionPathExists=!/var/lib/rigos/policy.json [Service] Type=oneshot +ExecCondition=/usr/lib/rigos/rigos-config needs-activation ExecStart=/usr/local/sbin/rigos-firstboot StandardInput=tty StandardOutput=tty diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service b/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service index 3ade9b8e..957d0f52 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-profile-apply.service @@ -6,7 +6,6 @@ Before=rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] Type=oneshot -ExecStartPre=/usr/lib/rigos/rigos-config recover ExecStart=/usr/lib/rigos/rigos-config timezone RemainAfterExit=yes diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service b/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service new file mode 100644 index 00000000..4674a86a --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service @@ -0,0 +1,18 @@ +[Unit] +Description=Establish local RIGOS recovery access +After=rigos-state.service +Wants=rigos-state.service +Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/rigos-recovery-access +StandardInput=tty +StandardOutput=tty +StandardError=tty +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes + +[Install] +WantedBy=multi-user.target diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service index 5b074a87..13f43e7f 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service @@ -1,8 +1,9 @@ [Unit] Description=Verify RIGOS persistent state readiness DefaultDependencies=no -After=rigos-state.service +After=rigos-state.service rigos-recovery-access.service Requires=rigos-state.service +Wants=rigos-recovery-access.service Before=local-fs.target rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot index bc7027fd..954885d9 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot @@ -182,30 +182,25 @@ def resolve_identity(proposal: dict) -> dict: return {"schema": "rigos.identity/v1", "alias": alias, "kind": "mining_identity", "value": value, "created_locally": True} -def ensure_administrator_password() -> None: - status = subprocess.run(["passwd", "--status", "rigosadmin"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - fields = status.stdout.split() - if status.returncode == 0 and len(fields) >= 2 and fields[1] == "P": - return - dialog("--msgbox", "Set the local administrator password next. It remains on this machine and enables local recovery diagnostics.", "11", "72") - subprocess.run(["passwd", "rigosadmin"], check=True) - verified = subprocess.run(["passwd", "--status", "rigosadmin"], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - verified_fields = verified.stdout.split() - if verified.returncode != 0 or len(verified_fields) < 2 or verified_fields[1] != "P": - raise SystemExit("Local administrator password was not established.") - - def main() -> None: - ensure_administrator_password() status_path = Path("/run/rigos/state-status.json") try: state_outcome = json.loads(status_path.read_text(encoding="utf-8"))["outcome"] except (OSError, KeyError, json.JSONDecodeError): state_outcome = "unavailable" - if state_outcome not in {"ready", "grown"}: + if state_outcome != "ready": dialog("--msgbox", f"Persistent state status: {state_outcome}\n\nConfiguration and mining are blocked until the verified USB state is ready.", "13", "72") raise SystemExit(1) + existing, current = engine("current") + if existing and current.get("outcome") == "configuration_committed": + activated, result = engine("activate") + if not activated: + dialog("--msgbox", f"Configuration activation failed\n{diagnostic_text(result)}\n\nThe committed revision was preserved and mining remains stopped. Reboot to retry activation.", "18", "76") + raise SystemExit(1) + dialog("--msgbox", f"Existing configuration activated.\nMiner started: {result.get('miner_started', False)}", "10", "64") + return + with tempfile.NamedTemporaryFile(prefix="rigos-proposal-", dir="/run/rigos", delete=False) as stream: proposal_path = stream.name os.unlink(proposal_path) @@ -224,11 +219,15 @@ def main() -> None: ) if not confirm(summary + "\n\nCommit this configuration locally?"): raise SystemExit("Configuration not confirmed; no changes were written.") - ok, result = engine("transact", stdin={"proposal": proposal, "identity": identity}) + ok, result = engine("commit", stdin={"proposal": proposal, "identity": identity}) if not ok: - dialog("--msgbox", f"Configuration transaction failed\n{diagnostic_text(result)}\n\nPrevious runtime state was restored or mining was left stopped for recovery.", "18", "76") + dialog("--msgbox", f"Configuration commit failed\n{diagnostic_text(result)}\n\nNo activation was attempted.", "18", "76") + raise SystemExit(1) + activated, activation = engine("activate") + if not activated: + dialog("--msgbox", f"Configuration was committed but activation failed\n{diagnostic_text(activation)}\n\nThe revision was preserved and mining remains stopped. Reboot to retry activation.", "18", "76") raise SystemExit(1) - dialog("--msgbox", f"Configuration saved atomically.\nMiner started: {result.get('miner_started', False)}", "10", "64") + dialog("--msgbox", f"Configuration saved and activated atomically.\nMiner started: {activation.get('miner_started', False)}", "10", "64") finally: try: os.unlink(proposal_path) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access new file mode 100644 index 00000000..3b55bd66 --- /dev/null +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access @@ -0,0 +1,87 @@ +#!/usr/bin/python3 +import json +import os +import subprocess +import tempfile +from pathlib import Path + +RUNTIME = Path("/run/rigos") +STATE = Path("/var/lib/rigos") + + +def password_ready() -> bool: + result = subprocess.run( + ["/usr/bin/passwd", "--status", "rigosadmin"], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + fields = result.stdout.split() + return result.returncode == 0 and len(fields) >= 2 and fields[1] == "P" + + +def unit_active(name: str) -> bool: + return subprocess.run( + ["/usr/bin/systemctl", "is-active", "--quiet", name], check=False + ).returncode == 0 + + +def unit_enabled(name: str) -> bool: + return subprocess.run( + ["/usr/bin/systemctl", "is-enabled", "--quiet", name], check=False + ).returncode == 0 + + +def write_status(value: dict) -> None: + RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=RUNTIME, prefix=".recovery-access-", delete=False + ) as stream: + json.dump(value, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o644) + os.replace(temporary, RUNTIME / "recovery-access-status.json") + directory = os.open(RUNTIME, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + + +def main() -> int: + if not password_ready(): + subprocess.run( + ["/usr/bin/whiptail", "--title", "RIGOS RECOVERY ACCESS", "--msgbox", + "Set the local rig administrator password next. This enables tty1 diagnostics only.", + "11", "72"], + check=False, + ) + subprocess.run(["/usr/bin/passwd", "rigosadmin"], check=True) + local_access = password_ready() + try: + state_outcome = json.loads((RUNTIME / "state-status.json").read_text(encoding="utf-8")).get("outcome", "unavailable") + except (OSError, json.JSONDecodeError): + state_outcome = "unavailable" + remote_active = any(unit_active(name) for name in ("ssh.service", "sshd.service")) + remote_enabled = any(unit_enabled(name) for name in ("ssh.service", "sshd.service")) + remote_inactive = not remote_active and not remote_enabled + configured = (STATE / "current").is_symlink() + mode = "operational" if state_outcome == "ready" and remote_inactive else "recovery" + write_status({ + "schema": "rigos.recovery-access-status/v1", + "boot_id": Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii").strip(), + "mode": mode, + "local_console_access": local_access, + "state_outcome": state_outcome, + "remote_access": "inactive" if remote_inactive else "unexpected_active", + "configuration_commit": "present" if configured else "absent", + }) + return 0 if local_access and remote_inactive else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/crates/rigos-config/src/main.rs b/crates/rigos-config/src/main.rs index ece89114..38959b9b 100644 --- a/crates/rigos-config/src/main.rs +++ b/crates/rigos-config/src/main.rs @@ -45,13 +45,21 @@ enum Commands { #[arg(long, default_value = "/run/rigos/state-status.json")] status: PathBuf, }, - Transact { + Commit { + #[arg(long, default_value = "/var/lib/rigos")] + state: PathBuf, + }, + Activate { #[arg(long, default_value = "/var/lib/rigos")] state: PathBuf, #[arg(long, default_value = "/proc/cmdline")] cmdline: PathBuf, }, - Recover { + Current { + #[arg(long, default_value = "/var/lib/rigos")] + state: PathBuf, + }, + NeedsActivation { #[arg(long, default_value = "/var/lib/rigos")] state: PathBuf, }, @@ -108,15 +116,6 @@ struct Policy { miner_start_mode: MinerStartMode, } -#[derive(Clone, Debug, Deserialize, Serialize)] -struct RuntimeSnapshot { - schema: String, - boot_id: String, - previous_revision: Option, - timezone: String, - miner_enabled: bool, - miner_active: bool, -} fn main() -> ExitCode { let cli = Cli::parse(); match execute(cli) { @@ -153,15 +152,24 @@ fn execute(cli: Cli) -> Result { attestation, status, } => with_verified_efi(&attestation, &status, discover), - Commands::Transact { state, cmdline } => { + Commands::Commit { state } => { let request = read_commit_request()?; - let (revision, miner_started) = - transact(&state, &cmdline, &request, &mut SystemRuntime)?; - Ok(json!({"outcome":"applied","revision":revision,"miner_started":miner_started})) + let (revision, created) = commit_once(&state, &request)?; + Ok(json!({"outcome":"configuration_committed","revision":revision,"created":created})) } - Commands::Recover { state } => { - recover_pending(&state, &mut SystemRuntime)?; - Ok(json!({"outcome":"recovered"})) + Commands::Activate { state, cmdline } => { + let (revision, miner_started) = activate(&state, &cmdline, &mut SystemRuntime)?; + Ok(json!({"outcome":"ready","revision":revision,"miner_started":miner_started})) + } + Commands::Current { state } => match current_revision(&state)? { + Some(revision) => Ok(json!({"outcome":"configuration_committed","revision":revision})), + None => Ok(json!({"outcome":"preflight_failed","reason":"not_provisioned"})), + }, + Commands::NeedsActivation { state } => { + if activation_ready(&state)? { + std::process::exit(1); + } + Ok(json!({"outcome":"activation_required"})) } Commands::Gate { state, cmdline } => { let allowed = gate(&state, &cmdline)?; @@ -434,38 +442,20 @@ fn discover(root: &Path) -> Result { } trait RuntimeOps { - fn timezone(&mut self) -> Result; fn set_timezone(&mut self, timezone: &str) -> Result<(), ConfigError>; - fn miner_enabled(&mut self) -> Result; fn set_miner_enabled(&mut self, enabled: bool) -> Result<(), ConfigError>; fn miner_active(&mut self) -> Result; fn stop_miner(&mut self) -> Result<(), ConfigError>; fn start_miner(&mut self) -> Result<(), ConfigError>; + fn restart_hugepages(&mut self) -> Result<(), ConfigError>; } struct SystemRuntime; impl RuntimeOps for SystemRuntime { - fn timezone(&mut self) -> Result { - output("timedatectl", &["show", "--property=Timezone", "--value"]) - } fn set_timezone(&mut self, timezone: &str) -> Result<(), ConfigError> { run("timedatectl", &["set-timezone", timezone]) } - fn miner_enabled(&mut self) -> Result { - let status = Command::new("systemctl") - .args(["is-enabled", "--quiet", "rigos-miner.service"]) - .status() - .map_err(io_failure)?; - match status.code() { - Some(0) => Ok(true), - Some(1) => Ok(false), - _ => Err(transaction_error( - "snapshot_enabled", - "could not read miner enabled state", - )), - } - } fn set_miner_enabled(&mut self, enabled: bool) -> Result<(), ConfigError> { run( "systemctl", @@ -495,212 +485,158 @@ impl RuntimeOps for SystemRuntime { fn start_miner(&mut self) -> Result<(), ConfigError> { run("systemctl", &["start", "rigos-miner.service"]) } + fn restart_hugepages(&mut self) -> Result<(), ConfigError> { + run("systemctl", &["restart", "rigos-hugepages.service"]) + } +} + +fn current_revision(state: &Path) -> Result, ConfigError> { + let current = state.join("current"); + let target = match fs::read_link(¤t) { + Ok(target) => target, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(io_failure(error)), + }; + let revision = target + .file_name() + .and_then(|value| value.to_str()) + .ok_or_else(|| transaction_error("current", "current revision pointer is invalid"))? + .to_owned(); + Uuid::parse_str(&revision) + .map_err(|_| transaction_error("current", "current revision identifier is invalid"))?; + if !state.join("current/policy.json").is_file() || !state.join("current/xmrig.json").is_file() { + return Err(transaction_error( + "current", + "current revision target is incomplete", + )); + } + Ok(Some(revision)) +} + +fn commit_once(state: &Path, request: &CommitRequest) -> Result<(String, bool), ConfigError> { + if let Some(revision) = current_revision(state)? { + return Ok((revision, false)); + } + let revision = commit_revision(state, &request.proposal, &request.identity) + .map_err(|_| transaction_error("commit", "configuration revision commit failed"))?; + write_firstboot_result(state, "configuration_committed", &revision, None)?; + Ok((revision, true)) } -fn transact( +fn activate( state: &Path, cmdline: &Path, - request: &CommitRequest, runtime: &mut impl RuntimeOps, ) -> Result<(String, bool), ConfigError> { - let pending = state.join(".pending-transaction.json"); - if pending.exists() { - return Err(transaction_error( - "snapshot", - "an incomplete configuration transaction already exists", - )); - } - let snapshot = RuntimeSnapshot { - schema: "rigos.runtime-snapshot/v1".into(), - boot_id: fs::read_to_string("/proc/sys/kernel/random/boot_id") - .map_err(io_failure)? - .trim() - .to_owned(), - previous_revision: fs::read_link(state.join("current")).ok(), - timezone: runtime.timezone()?, - miner_enabled: runtime.miner_enabled()?, - miner_active: runtime.miner_active()?, - }; - write_private_json(&pending, &snapshot)?; - let operation = (|| { - runtime.stop_miner().map_err(|_| { - transaction_error( - "stop_before_commit", - "miner could not be stopped before commit", - ) - })?; - if runtime.miner_active()? { - return Err(transaction_error( - "stop_before_commit", - "miner remained active before commit", - )); - } - let revision = - commit_revision(state, &request.proposal, &request.identity).map_err(|_| { - transaction_error("pointer_swap", "configuration revision commit failed") - })?; - let started = apply_new_runtime(&request.proposal.profile, cmdline, runtime)?; - Ok((revision, started)) - })(); + let revision = current_revision(state)? + .ok_or_else(|| transaction_error("preflight", "configuration has not been committed"))?; + let policy: Policy = read_json(&state.join("current/policy.json"))?; + let operation = apply_activation_runtime(&policy, cmdline, runtime); match operation { - Ok(result) => { - fs::remove_file(&pending).map_err(io_failure)?; - Ok(result) + Ok(started) => { + write_activation_status(state, "ready", &revision, None)?; + write_firstboot_result(state, "ready", &revision, None)?; + Ok((revision, started)) } - Err(failure) => { - let rollback = rollback_transaction(state, &snapshot, runtime); - if rollback.is_ok() { - let _ = fs::remove_file(&pending); - } - match rollback { - Ok(()) => Err(failure), - Err(rollback_failure) => Err(transaction_error( - failure.diagnostic.key.as_deref().unwrap_or("unknown"), - format!( - "{}; rollback failed at {}", - failure.diagnostic.message, - rollback_failure - .diagnostic - .key - .as_deref() - .unwrap_or("unknown") - ), - )), - } + Err(error) => { + let _ = runtime.stop_miner(); + let stage = error.diagnostic.key.as_deref().unwrap_or("unknown"); + write_activation_status(state, "activation_failed", &revision, Some(stage))?; + write_firstboot_result(state, "activation_failed", &revision, Some(stage))?; + Err(error) } } } -fn apply_new_runtime( - profile: &rigos_config::RigProfile, +fn apply_activation_runtime( + policy: &Policy, cmdline: &Path, runtime: &mut impl RuntimeOps, ) -> Result { - runtime - .set_timezone(&profile.timezone) - .map_err(|_| transaction_error("timezone", "timezone apply failed"))?; - let enabled = profile.miner_start_mode == MinerStartMode::OnBoot; - runtime - .set_miner_enabled(enabled) - .map_err(|_| transaction_error("enabled_state", "miner enabled state apply failed"))?; - let started = enabled && !cmdline_blocks_mining(cmdline); - if started { - runtime - .start_miner() - .map_err(|_| transaction_error("miner_start", "miner start failed"))?; - if !runtime.miner_active()? { + (|| { + runtime.stop_miner().map_err(|_| { + transaction_error("stop_before_activation", "miner could not be stopped") + })?; + if runtime.miner_active()? { return Err(transaction_error( - "miner_start", - "miner did not become active", + "stop_before_activation", + "miner remained active before activation", )); } - } - Ok(started) -} - -fn state_allows_config(outcome: &str) -> bool { - matches!(outcome, "ready" | "grown") + runtime + .set_timezone(&policy.timezone) + .map_err(|_| transaction_error("timezone", "timezone apply failed"))?; + let enabled = policy.miner_start_mode == MinerStartMode::OnBoot; + runtime + .set_miner_enabled(enabled) + .map_err(|_| transaction_error("enabled_state", "miner policy apply failed"))?; + runtime + .restart_hugepages() + .map_err(|_| transaction_error("hugepages", "huge page authority restart failed"))?; + let started = enabled && !cmdline_blocks_mining(cmdline); + if started { + runtime + .start_miner() + .map_err(|_| transaction_error("miner_start", "miner start failed"))?; + if !runtime.miner_active()? { + return Err(transaction_error( + "miner_start", + "miner did not become active", + )); + } + } + Ok(started) + })() } -fn rollback_transaction( +fn write_activation_status( state: &Path, - snapshot: &RuntimeSnapshot, - runtime: &mut impl RuntimeOps, + outcome: &str, + revision: &str, + stage: Option<&str>, ) -> Result<(), ConfigError> { - let mut first_error = None; - if runtime.stop_miner().is_err() { - first_error = Some(transaction_error( - "rollback_stop", - "miner could not be stopped", - )); - } - if first_error.is_none() - && restore_pointer(state, snapshot.previous_revision.as_deref()).is_err() - { - first_error = Some(transaction_error( - "rollback_pointer", - "previous revision could not be restored", - )); - } - let same_boot = fs::read_to_string("/proc/sys/kernel/random/boot_id") - .map(|value| value.trim() == snapshot.boot_id) - .unwrap_or(false); - if first_error.is_none() { - if let Err(error) = restore_runtime(snapshot, same_boot, runtime) { - first_error = Some(error); - } - } - if let Some(error) = first_error { - let _ = runtime.stop_miner(); - return Err(error); - } - Ok(()) + write_atomic_json( + &state.join("activation-status.json"), + &json!({"schema":"rigos.activation-status/v1","outcome":outcome,"revision":revision,"failure_stage":stage}), + ) } -fn restore_runtime( - snapshot: &RuntimeSnapshot, - restore_running: bool, - runtime: &mut impl RuntimeOps, +fn write_firstboot_result( + state: &Path, + outcome: &str, + revision: &str, + stage: Option<&str>, ) -> Result<(), ConfigError> { - runtime.set_timezone(&snapshot.timezone).map_err(|_| { - transaction_error( - "rollback_timezone", - "previous timezone could not be restored", - ) - })?; - runtime - .set_miner_enabled(snapshot.miner_enabled) - .map_err(|_| { - transaction_error( - "rollback_enabled_state", - "previous enabled state could not be restored", - ) - })?; - if restore_running && snapshot.miner_active { - runtime.start_miner().map_err(|_| { - transaction_error( - "rollback_running_state", - "previous running state could not be restored", - ) - })?; - } - Ok(()) + write_atomic_json( + &state.join("firstboot-status.json"), + &json!({"schema":"rigos.firstboot-status/v1","outcome":outcome,"revision":revision,"failure_stage":stage}), + ) } -fn recover_pending(state: &Path, runtime: &mut impl RuntimeOps) -> Result<(), ConfigError> { - let pending = state.join(".pending-transaction.json"); - if !pending.is_file() { - return Ok(()); - } - let snapshot: RuntimeSnapshot = read_json(&pending)?; - rollback_transaction(state, &snapshot, runtime)?; - fs::remove_file(pending).map_err(io_failure) +fn activation_ready(state: &Path) -> Result { + let Some(revision) = current_revision(state)? else { + return Ok(false); + }; + let status: Value = match read_json(&state.join("activation-status.json")) { + Ok(value) => value, + Err(_) => return Ok(false), + }; + Ok( + status.get("schema").and_then(Value::as_str) == Some("rigos.activation-status/v1") + && status.get("outcome").and_then(Value::as_str) == Some("ready") + && status.get("revision").and_then(Value::as_str) == Some(revision.as_str()), + ) } -fn restore_pointer(state: &Path, previous: Option<&Path>) -> Result<(), ConfigError> { - if let Some(previous) = previous { - let temporary = state.join(format!(".rollback-{}", Uuid::new_v4())); - create_symlink(previous, &temporary)?; - fs::rename(temporary, state.join("current")).map_err(io_failure)?; - } else { - for name in [ - "current", - "policy.json", - "xmrig.json", - "flight-sheets", - "identities", - "external-identity-map.json", - ] { - let _ = fs::remove_file(state.join(name)); - } - } - Ok(()) +fn state_allows_config(outcome: &str) -> bool { + matches!(outcome, "ready" | "grown") } fn gate(state: &Path, cmdline: &Path) -> Result { let policy: Policy = read_json(&state.join("current/policy.json"))?; let blocked = cmdline_blocks_mining(cmdline); - Ok(policy.miner_start_mode == MinerStartMode::OnBoot + Ok(activation_ready(state)? + && policy.miner_start_mode == MinerStartMode::OnBoot && !blocked && state.join("current/xmrig.json").is_file()) } @@ -723,20 +659,6 @@ fn apply_timezone(timezone: &str) -> Result<(), ConfigError> { run("timedatectl", &["set-timezone", timezone]) } -fn output(program: &str, arguments: &[&str]) -> Result { - let result = Command::new(program) - .args(arguments) - .output() - .map_err(io_failure)?; - if !result.status.success() { - return Err(transaction_error( - "runtime_query", - format!("runtime query {program} failed"), - )); - } - Ok(String::from_utf8_lossy(&result.stdout).trim().to_owned()) -} - fn transaction_error(stage: &str, message: impl Into) -> ConfigError { ConfigError { diagnostic: ConfigDiagnostic { @@ -798,6 +720,16 @@ fn write_private_json(path: &Path, value: &T) -> Result<(), Config serde_json::to_writer_pretty(file, value) .map_err(|_| diagnostic("RIGOS_CONFIG_INVALID_VALUE", "proposal write failed")) } +fn write_atomic_json(path: &Path, value: &T) -> Result<(), ConfigError> { + let parent = path + .parent() + .ok_or_else(|| diagnostic("RIGOS_CONFIG_INVALID_VALUE", "state path has no parent"))?; + let temporary = parent.join(format!(".status-{}", Uuid::new_v4())); + write_private_json(&temporary, value)?; + fs::rename(&temporary, path).map_err(io_failure)?; + let directory = fs::File::open(parent).map_err(io_failure)?; + directory.sync_all().map_err(io_failure) +} fn run(program: &str, arguments: &[&str]) -> Result<(), ConfigError> { if Command::new(program) .args(arguments) @@ -830,18 +762,6 @@ fn io_failure(value: io::Error) -> ConfigError { format!("I/O operation failed: {value}"), ) } -#[cfg(unix)] -fn create_symlink(target: &Path, link: &Path) -> Result<(), ConfigError> { - std::os::unix::fs::symlink(target, link).map_err(io_failure) -} -#[cfg(not(unix))] -fn create_symlink(_target: &Path, _link: &Path) -> Result<(), ConfigError> { - Err(diagnostic( - "RIGOS_CONFIG_INVALID_VALUE", - "rollback requires Unix symlinks", - )) -} - #[cfg(test)] mod tests { use super::*; @@ -919,17 +839,11 @@ mod tests { } impl RuntimeOps for FakeRuntime { - fn timezone(&mut self) -> Result { - Ok(self.timezone.clone()) - } fn set_timezone(&mut self, timezone: &str) -> Result<(), ConfigError> { self.fail("timezone")?; self.timezone = timezone.into(); Ok(()) } - fn miner_enabled(&mut self) -> Result { - Ok(self.enabled) - } fn set_miner_enabled(&mut self, enabled: bool) -> Result<(), ConfigError> { self.fail("enabled_state")?; self.enabled = enabled; @@ -948,26 +862,46 @@ mod tests { self.active = true; Ok(()) } + fn restart_hugepages(&mut self) -> Result<(), ConfigError> { + self.fail("hugepages") + } } #[test] - fn runtime_failure_stages_restore_previous_side_effects() { - let profile = rigos_config::RigProfile { - node_name: "rig01".into(), + fn activation_orders_hugepages_before_miner() { + let policy = Policy { timezone: "Asia/Bangkok".into(), - flight_source: FlightSource::Interactive, - flight_ref: None, miner_start_mode: MinerStartMode::OnBoot, }; - let snapshot = RuntimeSnapshot { - schema: "rigos.runtime-snapshot/v1".into(), - boot_id: "same".into(), - previous_revision: None, + let mut runtime = FakeRuntime { timezone: "UTC".into(), - miner_enabled: false, - miner_active: true, + enabled: false, + active: false, + fail_once: None, + events: vec![], }; - for stage in ["timezone", "enabled_state", "miner_start"] { + assert!( + apply_activation_runtime(&policy, Path::new("missing-cmdline"), &mut runtime).unwrap() + ); + assert_eq!( + runtime.events, + [ + "stop", + "timezone", + "enabled_state", + "hugepages", + "miner_start" + ] + ); + } + + #[test] + fn activation_failure_leaves_miner_stopped() { + let policy = Policy { + timezone: "Asia/Bangkok".into(), + miner_start_mode: MinerStartMode::OnBoot, + }; + for stage in ["timezone", "enabled_state", "hugepages", "miner_start"] { let mut runtime = FakeRuntime { timezone: "UTC".into(), enabled: false, @@ -975,35 +909,12 @@ mod tests { fail_once: Some(stage), events: vec![], }; - let error = apply_new_runtime(&profile, Path::new("missing-cmdline"), &mut runtime) - .unwrap_err(); + let error = + apply_activation_runtime(&policy, Path::new("missing-cmdline"), &mut runtime) + .unwrap_err(); assert_eq!(error.diagnostic.key.as_deref(), Some(stage)); - restore_runtime(&snapshot, true, &mut runtime).unwrap(); - assert_eq!(runtime.timezone, "UTC"); - assert!(!runtime.enabled); - assert!(runtime.active); + runtime.stop_miner().unwrap(); + assert!(!runtime.active); } } - - #[test] - fn rollback_failure_forces_miner_stopped() { - let snapshot = RuntimeSnapshot { - schema: "rigos.runtime-snapshot/v1".into(), - boot_id: "same".into(), - previous_revision: None, - timezone: "UTC".into(), - miner_enabled: true, - miner_active: true, - }; - let mut runtime = FakeRuntime { - timezone: "Asia/Bangkok".into(), - enabled: true, - active: true, - fail_once: Some("timezone"), - events: vec![], - }; - assert!(restore_runtime(&snapshot, true, &mut runtime).is_err()); - runtime.stop_miner().unwrap(); - assert!(!runtime.active); - } } diff --git a/scripts/build-usb-image.sh b/scripts/build-usb-image.sh index aec06a15..51d95214 100755 --- a/scripts/build-usb-image.sh +++ b/scripts/build-usb-image.sh @@ -100,7 +100,7 @@ VERSION_ID="${RIGOS_IMAGE_VERSION}" VERSION_CODENAME="usb-appliance-preview" EOF -chmod 0755 "$live/config/hooks/live/"* "$live/config/includes.chroot/usr/local/sbin/rigos-firstboot" +chmod 0755 "$live/config/hooks/live/"* "$live/config/includes.chroot/usr/local/sbin/rigos-firstboot" "$live/config/includes.chroot/usr/local/sbin/rigos-recovery-access" find "$live/config/includes.chroot/etc/systemd/system" -type f -exec chmod 0644 {} + find "$live/config/includes.chroot" -type f ! -path '*/usr/local/sbin/*' ! -path '*/usr/lib/rigos/rigos*' ! -path '*/usr/lib/rigos/xmrig' -exec chmod go-w {} + diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 6d7ec339..d5d3af8a 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -76,16 +76,18 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/rigos-release etc/os-release \ etc/systemd/system/rigos-state.service \ etc/systemd/system/rigos-state-ready.service \ + etc/systemd/system/rigos-recovery-access.service \ etc/systemd/system/rigos-hugepages.service \ etc/systemd/system/rigos-miner.service \ etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" +python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" grep -Fq 'systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime tmpfiles setup is missing' if rg -q '^RuntimeDirectory=rigos$' "$temporary/root/etc/systemd/system"; then die 'a service owns the shared runtime directory'; fi if grep -Eq '^(ExecStartPre|Environment=PATH)=.*compat-bin' "$temporary/root/etc/systemd/system/rigos-state.service"; then die 'state unit executes compatibility code from the runtime directory'; fi @@ -108,7 +110,8 @@ if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'sysctl' if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -Ei '(/dev/sd|/dev/nvme|cpu model)' >/dev/null; then die 'performance authority contains hardware-name or internal-disk targeting'; fi grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner policy gate is missing' [[ -f "$temporary/root/etc/systemd/system/rigos-profile-apply.service" ]] || die 'profile apply service is missing' -grep -Fq 'rigos-config recover' "$temporary/root/etc/systemd/system/rigos-profile-apply.service" || die 'pending transaction recovery is missing' +grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config needs-activation' "$temporary/root/etc/systemd/system/rigos-firstboot.service" || die 'first boot activation gate is missing' +grep -Fq 'ExecStart=/usr/local/sbin/rigos-recovery-access' "$temporary/root/etc/systemd/system/rigos-recovery-access.service" || die 'local recovery access phase is missing' if rg -q -- '--output-fd' "$temporary/root/usr/local/sbin/rigos-firstboot"; then die 'first boot rewires the whiptail screen stream'; fi grep -Fq 'stderr=subprocess.PIPE' "$temporary/root/usr/local/sbin/rigos-firstboot" || die 'first boot stderr capture is missing' grep -Fq 'return result.stderr.strip()' "$temporary/root/usr/local/sbin/rigos-firstboot" || die 'first boot value stream mismatch' diff --git a/scripts/verify.sh b/scripts/verify.sh index a3d35ed1..275b7e54 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -11,7 +11,7 @@ cargo run --locked -p rigos-schema --bin generate-schemas -- --check cargo build --workspace --release --locked bash -n scripts/*.sh build/usb/hooks/*.chroot -python3 -m py_compile build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +python3 -m py_compile build/usb/includes.chroot/usr/local/sbin/rigos-firstboot build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access grep -Fq 'RIGOS_CONFIG_DUPLICATE_KEY' crates/rigos-config/src/lib.rs grep -Fq 'RIGOS_CONFIG_BOOT_DEVICE_UNPROVEN' crates/rigos-config/src/main.rs @@ -21,8 +21,13 @@ grep -Fq 'boot_id' crates/rigos-state/src/main.rs grep -Fq 'major_minor' crates/rigos-state/src/main.rs grep -Fq 'ptuuid' crates/rigos-state/src/main.rs grep -Fq 'partuuid' crates/rigos-state/src/main.rs -grep -Fq '.pending-transaction.json' crates/rigos-config/src/main.rs -grep -Fq 'engine("transact"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +grep -Fq 'activation-status.json' crates/rigos-config/src/main.rs +if rg -q 'rollback|pending-transaction' crates/rigos-config/src/main.rs; then + echo "configuration activation still contains pointer rollback machinery" >&2 + exit 1 +fi +grep -Fq 'engine("commit"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +grep -Fq 'engine("activate"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service grep -Fq 'rigos-hugepages.service' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service grep -Fq '/proc/sys/vm/nr_hugepages' crates/rigos-performance/src/lib.rs @@ -93,9 +98,8 @@ import tempfile from pathlib import Path source = Path(sys.argv[1]).read_text(encoding="utf-8") -main_source = source[source.index("def main() -> None:"):] -if main_source.index("ensure_administrator_password()") > main_source.index('if state_outcome not in {"ready", "grown"}'): - raise SystemExit("administrator password is established after the state gate") +if "passwd" in source or "ensure_administrator_password" in source: + raise SystemExit("first boot still owns recovery password establishment") namespace = runpy.run_path(sys.argv[1], run_name="rigos_firstboot_identity_verify") with tempfile.TemporaryDirectory() as temporary: From 3ede2bbc59643a51d711c19fa1119caafdfc66be Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 14:15:31 +0700 Subject: [PATCH 005/386] performance handle unprovisioned lifecycle --- crates/rigos-performance/src/lib.rs | 67 +++++++++++++++++++++-- crates/rigos-schema/src/lib.rs | 3 +- crates/rigosd/src/lib.rs | 20 ++++--- schemas/performance-status-v1.schema.json | 7 ++- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/crates/rigos-performance/src/lib.rs b/crates/rigos-performance/src/lib.rs index 5d91c3a1..454fba92 100644 --- a/crates/rigos-performance/src/lib.rs +++ b/crates/rigos-performance/src/lib.rs @@ -415,8 +415,38 @@ pub fn execute( Err(error) => return Err(AuthorityError::Status(error.to_string())), } let current = paths.state_root.join("current"); - let revision_target = fs::read_link(¤t) - .map_err(|error| AuthorityError::Configuration(format!("current revision: {error}")))?; + let revision_target = match fs::read_link(¤t) { + Ok(target) => target, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let snapshot = kernel.snapshot()?; + let status = PerformanceStatusV1 { + schema: PERFORMANCE_STATUS_SCHEMA.into(), + boot_id: read_trimmed(&paths.boot_id, "boot ID")?, + generated_at: Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), + config_revision: None, + algorithm: None, + huge_pages: HugePageAuthorityV1 { + requested: false, + target_pages: 0, + attempted_pages: 0, + actual_pages: snapshot.huge_pages_total, + huge_page_size_bytes: snapshot.huge_page_size_bytes, + memory_available_before_bytes: snapshot.available_bytes, + reserve_bytes: MEMORY_RESERVE_BYTES, + allocation_percent_of_target: 0.0, + status: HugePageAuthorityStatusV1::NotProvisioned, + reason: Some("configuration_not_committed".into()), + }, + }; + write_status_verified(&paths.status, &status)?; + return Ok(status); + } + Err(error) => { + return Err(AuthorityError::Configuration(format!( + "current revision: {error}" + ))); + } + }; let revision = revision_target .file_name() .and_then(|value| value.to_str()) @@ -433,7 +463,7 @@ pub fn execute( schema: PERFORMANCE_STATUS_SCHEMA.into(), boot_id: read_trimmed(&paths.boot_id, "boot ID")?, generated_at: Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true), - config_revision: revision, + config_revision: Some(revision), algorithm: Some(policy.algorithm), huge_pages, }; @@ -770,7 +800,7 @@ mod tests { schema: PERFORMANCE_STATUS_SCHEMA.into(), boot_id: "boot".into(), generated_at: "2026-07-06T00:00:00.000Z".into(), - config_revision: "revision".into(), + config_revision: Some("revision".into()), algorithm: Some("rx/0".into()), huge_pages: outcome( true, @@ -803,6 +833,35 @@ mod tests { let _ = fs::remove_dir_all(root); } + #[test] + fn unprovisioned_status_reads_truth_without_mutation_or_miner_control() { + let root = std::env::temp_dir().join(format!("rigos-unprovisioned-{}", Uuid::new_v4())); + let state = root.join("state"); + fs::create_dir_all(&state).unwrap(); + let boot_id = root.join("boot-id"); + fs::write(&boot_id, "boot-fresh\n").unwrap(); + let paths = AuthorityPaths { + state_root: state, + status: root.join("performance-status.json"), + boot_id, + }; + let mut kernel = FakeKernel::new(vec![memory(4 * MEMORY_RESERVE_BYTES, 17)]); + let mut miner = FakeMiner { + active: false, + calls: vec![], + }; + let status = execute(&paths, &mut kernel, &mut miner).unwrap(); + assert_eq!( + status.huge_pages.status, + HugePageAuthorityStatusV1::NotProvisioned + ); + assert_eq!(status.huge_pages.actual_pages, 17); + assert_eq!(status.config_revision, None); + assert!(kernel.writes.is_empty()); + assert!(miner.calls.is_empty()); + let _ = fs::remove_dir_all(root); + } + #[test] fn hard_failure_leaves_previously_active_miner_stopped() { let available = MEMORY_RESERVE_BYTES + RX0_TARGET_PAGES * EXPECTED_HUGE_PAGE_SIZE_BYTES; diff --git a/crates/rigos-schema/src/lib.rs b/crates/rigos-schema/src/lib.rs index b1bb01ba..0c1495c6 100644 --- a/crates/rigos-schema/src/lib.rs +++ b/crates/rigos-schema/src/lib.rs @@ -24,6 +24,7 @@ pub const PERFORMANCE_STATUS_SCHEMA: &str = "rigos.performance-status/v1"; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum HugePageAuthorityStatusV1 { + NotProvisioned, Ready, Disabled, DegradedInsufficientMemory, @@ -52,7 +53,7 @@ pub struct PerformanceStatusV1 { pub schema: String, pub boot_id: String, pub generated_at: String, - pub config_revision: String, + pub config_revision: Option, pub algorithm: Option, pub huge_pages: HugePageAuthorityV1, } diff --git a/crates/rigosd/src/lib.rs b/crates/rigosd/src/lib.rs index 27cfe499..bba2871c 100644 --- a/crates/rigosd/src/lib.rs +++ b/crates/rigosd/src/lib.rs @@ -223,17 +223,18 @@ fn load_huge_page_check() -> DoctorCheckV1 { }; let revision = match fs::read_link(current_revision_path()) { Ok(value) => match value.file_name().and_then(|value| value.to_str()) { - Some(value) if !value.is_empty() => value.to_owned(), + Some(value) if !value.is_empty() => Some(value.to_owned()), _ => return failed_huge_page_check("current revision target is invalid".into()), }, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, Err(error) => { return failed_huge_page_check(format!("current revision unavailable: {error}")); } }; - evaluate_huge_page_check(&status, &boot_id, &revision) + evaluate_huge_page_check(&status, &boot_id, revision.as_deref()) } -fn evaluate_huge_page_check(status: &[u8], boot_id: &str, revision: &str) -> DoctorCheckV1 { +fn evaluate_huge_page_check(status: &[u8], boot_id: &str, revision: Option<&str>) -> DoctorCheckV1 { let status: PerformanceStatusV1 = match serde_json::from_slice(status) { Ok(value) => value, Err(error) => return failed_huge_page_check(format!("invalid status JSON: {error}")), @@ -244,7 +245,7 @@ fn evaluate_huge_page_check(status: &[u8], boot_id: &str, revision: &str) -> Doc if status.boot_id != boot_id { return failed_huge_page_check("performance status is from another boot".into()); } - if status.config_revision != revision { + if status.config_revision.as_deref() != revision { return failed_huge_page_check("performance status uses another config revision".into()); } let level = match status.huge_pages.status { @@ -265,6 +266,7 @@ fn evaluate_huge_page_check(status: &[u8], boot_id: &str, revision: &str) -> Doc fn huge_page_status_name(status: &HugePageAuthorityStatusV1) -> &'static str { match status { + HugePageAuthorityStatusV1::NotProvisioned => "not_provisioned", HugePageAuthorityStatusV1::Ready => "ready", HugePageAuthorityStatusV1::Disabled => "disabled", HugePageAuthorityStatusV1::DegradedInsufficientMemory => "degraded_insufficient_memory", @@ -490,7 +492,7 @@ mod tests { schema: PERFORMANCE_STATUS_SCHEMA.into(), boot_id: "boot-a".into(), generated_at: "2026-07-06T00:00:00.000Z".into(), - config_revision: "revision-a".into(), + config_revision: Some("revision-a".into()), algorithm: Some("rx/0".into()), huge_pages: rigos_schema::HugePageAuthorityV1 { requested: true, @@ -507,7 +509,7 @@ mod tests { }; let ready = serde_json::to_vec(&status(HugePageAuthorityStatusV1::Ready)).unwrap(); assert_eq!( - evaluate_huge_page_check(&ready, "boot-a", "revision-a").status, + evaluate_huge_page_check(&ready, "boot-a", Some("revision-a")).status, "pass" ); for kind in [ @@ -519,17 +521,17 @@ mod tests { ] { let degraded = serde_json::to_vec(&status(kind)).unwrap(); assert_eq!( - evaluate_huge_page_check(°raded, "boot-a", "revision-a").status, + evaluate_huge_page_check(°raded, "boot-a", Some("revision-a")).status, "warning" ); } let disabled = serde_json::to_vec(&status(HugePageAuthorityStatusV1::Disabled)).unwrap(); assert_eq!( - evaluate_huge_page_check(&disabled, "boot-a", "revision-a").status, + evaluate_huge_page_check(&disabled, "boot-a", Some("revision-a")).status, "pass" ); assert_eq!( - evaluate_huge_page_check(&ready, "boot-b", "revision-a").status, + evaluate_huge_page_check(&ready, "boot-b", Some("revision-a")).status, "fail" ); } diff --git a/schemas/performance-status-v1.schema.json b/schemas/performance-status-v1.schema.json index 8a9e8ff4..59ea160c 100644 --- a/schemas/performance-status-v1.schema.json +++ b/schemas/performance-status-v1.schema.json @@ -2,6 +2,7 @@ "$defs": { "HugePageAuthorityStatusV1": { "enum": [ + "not_provisioned", "ready", "disabled", "degraded_insufficient_memory", @@ -87,7 +88,10 @@ "type": "string" }, "config_revision": { - "type": "string" + "type": [ + "string", + "null" + ] }, "generated_at": { "type": "string" @@ -103,7 +107,6 @@ "schema", "boot_id", "generated_at", - "config_revision", "huge_pages" ], "title": "PerformanceStatusV1", From a20ff1dce46c8fece55e4887d5d3aa55f0649bf3 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 14:17:11 +0700 Subject: [PATCH 006/386] build package rigosctl in path --- scripts/build-usb-image.sh | 2 ++ scripts/verify-usb-appliance.sh | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/build-usb-image.sh b/scripts/build-usb-image.sh index 51d95214..c634fee3 100755 --- a/scripts/build-usb-image.sh +++ b/scripts/build-usb-image.sh @@ -36,6 +36,8 @@ cp build/usb/hooks/010-rigos.chroot "$live/config/hooks/live/010-rigos.hook.chro cp -a build/usb/bootloaders/. "$live/config/bootloaders/" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigosd" "$live/config/includes.chroot/usr/lib/rigos/rigosd" ln -s rigosd "$live/config/includes.chroot/usr/lib/rigos/rigosctl" +mkdir -p "$live/config/includes.chroot/usr/local/sbin" +ln -s ../../lib/rigos/rigosctl "$live/config/includes.chroot/usr/local/sbin/rigosctl" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-state-init" "$live/config/includes.chroot/usr/lib/rigos/rigos-state-init" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-state-ready" "$live/config/includes.chroot/usr/lib/rigos/rigos-state-ready" install -D -m 0755 "$CARGO_TARGET_DIR/release/rigos-config" "$live/config/includes.chroot/usr/lib/rigos/rigos-config" diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index d5d3af8a..2bbef071 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -82,12 +82,14 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" +rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" +[[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' grep -Fq 'systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime tmpfiles setup is missing' if rg -q '^RuntimeDirectory=rigos$' "$temporary/root/etc/systemd/system"; then die 'a service owns the shared runtime directory'; fi if grep -Eq '^(ExecStartPre|Environment=PATH)=.*compat-bin' "$temporary/root/etc/systemd/system/rigos-state.service"; then die 'state unit executes compatibility code from the runtime directory'; fi From 757f6afeb6e2a7708ce96f0fa1ef28e1f5b7582a Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 14:23:03 +0700 Subject: [PATCH 007/386] test lock Alpha6 lifecycle recovery --- build/usb/hooks/010-rigos.chroot | 2 +- .../usr/lib/rigos/rigos-lifecycle-cycles | 30 +++++++++++ crates/rigos-config/src/main.rs | 50 +++++++++++++++++++ scripts/verify-usb-appliance.sh | 11 ++-- scripts/verify.sh | 19 +++++++ 5 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 80432008..2ff0de07 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,7 +8,7 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/xmrig +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/xmrig systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles b/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles new file mode 100644 index 00000000..aae63039 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles @@ -0,0 +1,30 @@ +#!/bin/sh +set -eu + +cycles="${1:-20}" +case "$cycles" in + *[!0-9]*|'') echo "invalid cycle count" >&2; exit 2 ;; +esac +[ "$cycles" -ge 20 ] || { echo "at least 20 cycles are required" >&2; exit 2; } +[ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 2; } +[ -L /var/lib/rigos/current ] || { echo "node is not provisioned" >&2; exit 2; } + +current="$(readlink /var/lib/rigos/current)" +revisions="$(find /var/lib/rigos/revisions -mindepth 1 -maxdepth 1 -type d | wc -l)" +index=1 +while [ "$index" -le "$cycles" ]; do + systemctl restart rigos-profile-apply.service + systemctl restart rigos-hugepages.service + systemctl restart rigos-miner.service + [ -d /run/rigos ] || { echo "shared runtime directory disappeared at cycle $index" >&2; exit 1; } + [ "$(readlink /var/lib/rigos/current)" = "$current" ] || { echo "current revision changed at cycle $index" >&2; exit 1; } + [ "$(find /var/lib/rigos/revisions -mindepth 1 -maxdepth 1 -type d | wc -l)" -eq "$revisions" ] || { echo "revision count changed at cycle $index" >&2; exit 1; } + index=$((index + 1)) +done + +if journalctl -b --no-pager -u rigos-profile-apply.service -u rigos-hugepages.service -u rigos-miner.service | grep -Fq '226/NAMESPACE'; then + echo "systemd namespace failure observed" >&2 + exit 1 +fi + +printf '{"schema":"rigos.lifecycle-cycles/v1","cycles":%s,"revision":"%s","outcome":"pass"}\n' "$cycles" "$current" diff --git a/crates/rigos-config/src/main.rs b/crates/rigos-config/src/main.rs index 38959b9b..74426ca3 100644 --- a/crates/rigos-config/src/main.rs +++ b/crates/rigos-config/src/main.rs @@ -917,4 +917,54 @@ mod tests { assert!(!runtime.active); } } + + #[cfg(unix)] + #[test] + fn activation_retry_preserves_one_current_revision() { + use std::os::unix::fs::symlink; + + let state = std::env::temp_dir().join(format!("rigos-activation-{}", Uuid::new_v4())); + let revision = Uuid::new_v4().to_string(); + let revision_path = state.join("revisions").join(&revision); + fs::create_dir_all(&revision_path).unwrap(); + fs::write( + revision_path.join("policy.json"), + br#"{"timezone":"Asia/Bangkok","miner_start_mode":"on_boot"}"#, + ) + .unwrap(); + fs::write(revision_path.join("xmrig.json"), b"{}\n").unwrap(); + symlink( + Path::new("revisions").join(&revision), + state.join("current"), + ) + .unwrap(); + + let mut failed = FakeRuntime { + timezone: "UTC".into(), + enabled: false, + active: false, + fail_once: Some("hugepages"), + events: vec![], + }; + assert!(activate(&state, Path::new("missing-cmdline"), &mut failed).is_err()); + assert!(!failed.active); + assert_eq!(current_revision(&state).unwrap(), Some(revision.clone())); + let failed_status: Value = read_json(&state.join("activation-status.json")).unwrap(); + assert_eq!(failed_status["outcome"], "activation_failed"); + + let mut retry = FakeRuntime { + timezone: "UTC".into(), + enabled: false, + active: false, + fail_once: None, + events: vec![], + }; + let (activated_revision, started) = + activate(&state, Path::new("missing-cmdline"), &mut retry).unwrap(); + assert_eq!(activated_revision, revision); + assert!(started); + assert_eq!(fs::read_dir(state.join("revisions")).unwrap().count(), 1); + assert!(activation_ready(&state).unwrap()); + let _ = fs::remove_dir_all(state); + } } diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 2bbef071..c283b769 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -74,6 +74,7 @@ cmp "$temporary/a/image-layout.json" "$temporary/b/image-layout.json" [[ "$(jq -r '.partitions[-1].label' "$temporary/a/image-layout.json")" == RIGOS_STATE_SEED ]] || die 'state seed is not final' unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \ etc/rigos-release etc/os-release \ + usr/lib/tmpfiles.d/rigos.conf \ etc/systemd/system/rigos-state.service \ etc/systemd/system/rigos-state-ready.service \ etc/systemd/system/rigos-recovery-access.service \ @@ -82,7 +83,7 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/xmrig usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/xmrig usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' @@ -91,6 +92,7 @@ python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" [[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' grep -Fq 'systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime tmpfiles setup is missing' +grep -Fqx 'd /run/rigos 0755 root root -' "$temporary/root/usr/lib/tmpfiles.d/rigos.conf" || die 'shared runtime directory contract is missing' if rg -q '^RuntimeDirectory=rigos$' "$temporary/root/etc/systemd/system"; then die 'a service owns the shared runtime directory'; fi if grep -Eq '^(ExecStartPre|Environment=PATH)=.*compat-bin' "$temporary/root/etc/systemd/system/rigos-state.service"; then die 'state unit executes compatibility code from the runtime directory'; fi [[ -f "$temporary/root/usr/lib/rigos/lsblk-compat" ]] || die 'state lsblk compatibility wrapper is missing' @@ -102,10 +104,10 @@ if strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/run/rigo grep -Fq 'ExecStart=/usr/lib/rigos/rigos-state-ready' "$temporary/root/etc/systemd/system/rigos-state-ready.service" || die 'state readiness verifier is not wired' grep -Fq 'Requires=rigos-state-ready.service' "$temporary/root/etc/systemd/system/rigos-profile-apply.service" || die 'profile apply bypasses state readiness' [[ -x "$temporary/root/usr/lib/rigos/rigos-performance" ]] || die 'performance authority is missing or not executable' -grep -Fq 'After=rigos-state.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority ordering is missing' +grep -Fq 'After=rigos-state-ready.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority ordering is missing' grep -Fq 'Before=rigos-miner.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority is not ordered before miner' -grep -Fq 'Requires=rigos-state.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority dependencies are missing' -grep -Fq 'Requires=rigos-state.service rigos-hugepages.service' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner does not require huge page authority' +grep -Fq 'Requires=rigos-state-ready.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority dependencies are missing' +grep -Fq 'Requires=rigos-state-ready.service rigos-hugepages.service' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner does not require huge page authority' strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F '/proc/sys/vm/nr_hugepages' >/dev/null || die 'performance authority does not use direct kernel huge page control' strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'rigos.performance-status/v1' >/dev/null || die 'performance authority status contract is missing' if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'sysctl' >/dev/null; then die 'performance authority shells out to sysctl'; fi @@ -114,6 +116,7 @@ grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' "$temporary/root/etc/s [[ -f "$temporary/root/etc/systemd/system/rigos-profile-apply.service" ]] || die 'profile apply service is missing' grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config needs-activation' "$temporary/root/etc/systemd/system/rigos-firstboot.service" || die 'first boot activation gate is missing' grep -Fq 'ExecStart=/usr/local/sbin/rigos-recovery-access' "$temporary/root/etc/systemd/system/rigos-recovery-access.service" || die 'local recovery access phase is missing' +[[ -x "$temporary/root/usr/lib/rigos/rigos-lifecycle-cycles" ]] || die 'booted lifecycle cycle test is missing' if rg -q -- '--output-fd' "$temporary/root/usr/local/sbin/rigos-firstboot"; then die 'first boot rewires the whiptail screen stream'; fi grep -Fq 'stderr=subprocess.PIPE' "$temporary/root/usr/local/sbin/rigos-firstboot" || die 'first boot stderr capture is missing' grep -Fq 'return result.stderr.strip()' "$temporary/root/usr/local/sbin/rigos-firstboot" || die 'first boot value stream mismatch' diff --git a/scripts/verify.sh b/scripts/verify.sh index 275b7e54..f6ea2739 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -11,6 +11,7 @@ cargo run --locked -p rigos-schema --bin generate-schemas -- --check cargo build --workspace --release --locked bash -n scripts/*.sh build/usb/hooks/*.chroot +sh -n build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles python3 -m py_compile build/usb/includes.chroot/usr/local/sbin/rigos-firstboot build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access grep -Fq 'RIGOS_CONFIG_DUPLICATE_KEY' crates/rigos-config/src/lib.rs @@ -21,6 +22,11 @@ grep -Fq 'boot_id' crates/rigos-state/src/main.rs grep -Fq 'major_minor' crates/rigos-state/src/main.rs grep -Fq 'ptuuid' crates/rigos-state/src/main.rs grep -Fq 'partuuid' crates/rigos-state/src/main.rs +grep -Fq 'Duration::from_secs(10)' crates/rigos-state/src/main.rs +if rg -q 'udevadm.*settle|systemd-udev-settle' crates/rigos-state build/usb/includes.chroot/etc/systemd/system; then + echo "state readiness uses a global udev settle" >&2 + exit 1 +fi grep -Fq 'activation-status.json' crates/rigos-config/src/main.rs if rg -q 'rollback|pending-transaction' crates/rigos-config/src/main.rs; then echo "configuration activation still contains pointer rollback machinery" >&2 @@ -28,6 +34,7 @@ if rg -q 'rollback|pending-transaction' crates/rigos-config/src/main.rs; then fi grep -Fq 'engine("commit"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot grep -Fq 'engine("activate"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +grep -Fq 'engine("current"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service grep -Fq 'rigos-hugepages.service' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service grep -Fq '/proc/sys/vm/nr_hugepages' crates/rigos-performance/src/lib.rs @@ -90,6 +97,18 @@ if "stdout" in seen["kwargs"]: raise SystemExit("first boot dialog does not leave stdout on tty") PY +python3 - <<'PY' +from pathlib import Path + +recovery = Path("build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access").read_text(encoding="utf-8") +for forbidden in ("rigos-config", "rigos-miner.service", "systemctl\", \"start", "systemctl\", \"enable"): + if forbidden in recovery: + raise SystemExit(f"recovery access contains forbidden configuration or service mutation: {forbidden}") +for required in ("passwd", "local_console_access", "remote_access", "configuration_commit"): + if required not in recovery: + raise SystemExit(f"recovery access contract is missing {required}") +PY + python3 - "$firstboot" <<'PY' import json import runpy From 062249de66e3ff81ae16994918666f5e4b80361d Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 14:24:28 +0700 Subject: [PATCH 008/386] docs record Alpha6 lifecycle gates --- docs/local-rig-config.md | 4 +++- docs/performance-authority.md | 36 +++++++++++++++++++++++++++++++++++ docs/usb-image-build.md | 3 +++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/local-rig-config.md b/docs/local-rig-config.md index 0dd81830..5d5ef92b 100644 --- a/docs/local-rig-config.md +++ b/docs/local-rig-config.md @@ -36,4 +36,6 @@ Parsing and validation finish before persistent or service mutation. Invalid inp Configuration commits capture the current timezone and miner enabled/running state, stop XMRig, create a complete revision and atomically switch the current pointer. Timezone, unit policy and miner start are then applied in order. Failure restores the previous pointer and runtime snapshot; if restoration is incomplete XMRig remains stopped and the pending transaction is retained for boot recovery. `rigos.nomine=1` blocks mining for one boot without changing persistent policy. -State outcomes `ready` and `grown` permit import. `limited_capacity` and every blocked outcome are negative gates with zero config, timezone or miner mutation. Positive physical Alpha.5 validation depends on the separate state issue reaching a persistent ready state. +Only state outcome `ready` permits import. The status records `action=grown` or +`action=unchanged` separately. `limited_capacity` and every blocked outcome are +negative gates with zero config, timezone or miner mutation. diff --git a/docs/performance-authority.md b/docs/performance-authority.md index 906243fa..99a0e564 100644 --- a/docs/performance-authority.md +++ b/docs/performance-authority.md @@ -8,6 +8,8 @@ The boot order is: ```text rigos-state.service +rigos-recovery-access.service +rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-miner.service @@ -27,6 +29,38 @@ The authoritative runtime record is: It uses `rigos.performance-status/v1` and records the boot ID, config revision, algorithm, target, safe attempt, actual kernel pages, memory inputs and a stable status. `rigosctl doctor` rejects records from another boot or config revision. +On a fresh node without a `current` revision it records `not_provisioned`, reads +the current kernel allocation and exits successfully without writing huge-page +state or controlling the miner. A broken target behind an existing `current` +pointer remains a hard failure. + +## Lifecycle gate + +`/run/rigos` is a shared root-owned tmpfiles directory. Services may publish +atomic status files there but do not own or remove the shared parent. Persistent +state is accepted only after `rigos-state-ready` matches the current boot ID, +attested PARTUUID and major:minor, ext4 label, mount source and the complete +`rw,nosuid,nodev,noexec,noatime` option set. + +Local `rigosadmin` password establishment is a separate tty1 recovery phase. +It does not create a revision, alter mining configuration or enable remote +access. A state failure leaves tty1 diagnostics available while profile, +firstboot, huge pages and miner remain gated. + +Configuration commit and activation are separate. A successful commit creates +one durable revision. Activation failure preserves that revision, records +`activation_failed`, leaves the miner stopped and retries only activation on +the next invocation. `ready` is tied to the current revision. + +On a configured physical node, run the packaged lifecycle stress gate: + +```bash +sudo /usr/lib/rigos/rigos-lifecycle-cycles 20 +``` + +It must retain the same current pointer and revision count, preserve +`/run/rigos`, avoid `226/NAMESPACE`, and complete all profile, huge-page and +miner restart cycles. Expected degraded states return success so the miner may continue. Unreadable configuration, unverifiable machine truth or failure to atomically publish and @@ -52,6 +86,8 @@ rigosctl doctor --json The gate requires current status matching kernel read-back, XMRig huge-page use greater than zero, accepted shares with zero rejected shares, successful reapplication after reboot and unchanged internal-disk layout. +Acceptance evidence must come from a fully wiped and reflashed USB; revisions +created during manual recovery are not valid evidence. ## Reserved Alpha.6 interfaces diff --git a/docs/usb-image-build.md b/docs/usb-image-build.md index 7c892878..abc451c7 100644 --- a/docs/usb-image-build.md +++ b/docs/usb-image-build.md @@ -30,6 +30,9 @@ a persistent appliance and never runs the state grow helper. Alpha six preserves the Alpha five local configuration contract and adds machine-wide huge page authority with visible requested-versus-actual status. +It also requires exact persistent-state readiness, separate local-console +recovery access, idempotent commit/activation, and appliance-visible +`/usr/local/sbin/rigosctl`. Build from a tracked-clean commit: From 8fc0a4b0dedbb94c63e0396d1dcf78ba2631a0c5 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 14:35:18 +0700 Subject: [PATCH 009/386] test fix appliance extraction contract --- scripts/verify-usb-appliance.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index c283b769..d3e20366 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -78,6 +78,7 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-state.service \ etc/systemd/system/rigos-state-ready.service \ etc/systemd/system/rigos-recovery-access.service \ + etc/systemd/system/rigos-firstboot.service \ etc/systemd/system/rigos-hugepages.service \ etc/systemd/system/rigos-miner.service \ etc/systemd/system/rigos-profile-apply.service \ From e5f5e06af49801ed8efac2d3991532a0a9828e34 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:30:04 +0700 Subject: [PATCH 010/386] state resume partial grow safely --- .../usr/local/sbin/rigos-state-orchestrate | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate new file mode 100644 index 00000000..ec0bf416 --- /dev/null +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +import json +import os +import re +import subprocess +import tempfile +import time +from pathlib import Path + +CORE = Path("/usr/lib/rigos/rigos-state-init") +RUNTIME = Path("/run/rigos") +STATUS = RUNTIME / "state-status.json" +ATTESTATION = RUNTIME / "boot-device.json" +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +PARTUUID_ROOT = Path("/dev/disk/by-partuuid") +PARTUUID_RE = re.compile(r"^[0-9A-Fa-f-]{1,128}$") + + +def read_json(path: Path) -> dict: + try: + raw = path.read_bytes() + if len(raw) > 64 * 1024: + return {} + value = json.loads(raw) + return value if isinstance(value, dict) else {} + except (OSError, json.JSONDecodeError): + return {} + + +def write_status(value: dict) -> None: + RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=RUNTIME, prefix=".state-status-", delete=False + ) as stream: + json.dump(value, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o644) + os.replace(temporary, STATUS) + directory = os.open(RUNTIME, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + + +def run_core() -> int: + return subprocess.run([str(CORE)], check=False).returncode + + +def properties(output: str) -> dict[str, str]: + result = {} + for line in output.splitlines(): + if "=" in line: + key, value = line.split("=", 1) + result[key] = value + return result + + +def verified_state_device() -> tuple[Path | None, str | None]: + attestation = read_json(ATTESTATION) + try: + current_boot = BOOT_ID.read_text(encoding="ascii").strip() + except OSError as error: + return None, f"boot id unavailable: {error}" + state = attestation.get("state") + if ( + attestation.get("schema") != "rigos.boot-device/v1" + or attestation.get("verification_outcome") != "verified" + or attestation.get("boot_id") != current_boot + or not isinstance(state, dict) + ): + return None, "current verified boot-device attestation is unavailable" + partuuid = state.get("partuuid") + if not isinstance(partuuid, str) or not PARTUUID_RE.fullmatch(partuuid): + return None, "attested state PARTUUID is invalid" + try: + device = (PARTUUID_ROOT / partuuid).resolve(strict=True) + except OSError as error: + return None, f"attested state PARTUUID is unavailable: {error}" + mounted = subprocess.run( + ["/usr/bin/findmnt", "--noheadings", "--source", str(device)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if mounted.returncode == 0: + return None, "verified state device is unexpectedly mounted before repair" + if mounted.returncode not in (1,): + return None, "state mount query failed" + observed = subprocess.run( + ["/usr/sbin/blkid", "-o", "export", str(device)], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if observed.returncode != 0: + return None, "verified state filesystem identity is unavailable" + values = properties(observed.stdout) + if ( + values.get("TYPE") != "ext4" + or values.get("LABEL") not in ("RIGOS_STATE_SEED", "RIGOS_STATE") + or values.get("PARTUUID", "").lower() != partuuid.lower() + ): + return None, "verified state filesystem identity changed" + return device, None + + +def mark_repair_required(message: str) -> None: + status = read_json(STATUS) + attestation = read_json(ATTESTATION) + state = attestation.get("state") if isinstance(attestation.get("state"), dict) else {} + status.update( + { + "schema": "rigos.state-status/v1", + "boot_id": BOOT_ID.read_text(encoding="ascii").strip(), + "outcome": "repair_required", + "action": None, + "message": message, + "device": state.get("path") or status.get("device"), + "partuuid": state.get("partuuid") or status.get("partuuid"), + "mountpoint": None, + } + ) + write_status(status) + + +def forced_check() -> bool: + device, error = verified_state_device() + if error is not None or device is None: + mark_repair_required(error or "verified state device is unavailable") + return False + result = subprocess.run( + ["/usr/sbin/e2fsck", "-f", "-p", str(device)], + check=False, + ) + if result.returncode in (0, 1): + return True + mark_repair_required( + f"forced ext4 check requires owner intervention with exit code {result.returncode}" + ) + return False + + +def main() -> int: + retried_missing_device = False + repaired_filesystem = False + for _attempt in range(4): + result = run_core() + if result != 0: + return result + status = read_json(STATUS) + if status.get("outcome") == "ready": + return 0 + message = str(status.get("message") or "") + if ( + status.get("outcome") == "limited_capacity" + and not retried_missing_device + and "No such file or directory" in message + ): + retried_missing_device = True + time.sleep(1) + continue + if ( + status.get("outcome") == "limited_capacity" + and not repaired_filesystem + and "e2fsck -f" in message + ): + repaired_filesystem = True + if forced_check(): + continue + return 0 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 62d95a4cc9070df367af570945ddbf279e716356 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:30:22 +0700 Subject: [PATCH 011/386] state wire bounded resume orchestrator --- .../usb/includes.chroot/etc/systemd/system/rigos-state.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service index 973527cf..97d891ba 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service @@ -7,7 +7,7 @@ Before=local-fs.target rigos-state-ready.service [Service] Type=oneshot ExecStartPre=/usr/bin/systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -ExecStart=/usr/lib/rigos/rigos-state-init +ExecStart=/usr/local/sbin/rigos-state-orchestrate RemainAfterExit=yes [Install] From 0e420f83bcf73ef9ba1687bbc035cad635db9ab0 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:30:35 +0700 Subject: [PATCH 012/386] miner split policy gate from activation --- .../usr/lib/rigos/rigos-miner-gate | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate new file mode 100644 index 00000000..4264ee5f --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import argparse +import json +import sys +from pathlib import Path + +MAX_JSON_BYTES = 2 * 1024 * 1024 + + +def emit(outcome: str, reason: str | None = None) -> None: + value = { + "schema": "rigos.miner-gate/v1", + "outcome": outcome, + "reason": reason, + } + stream = sys.stdout if outcome == "allowed" else sys.stderr + print(json.dumps(value, sort_keys=True), file=stream) + + +def read_object(path: Path) -> dict: + raw = path.read_bytes() + if len(raw) > MAX_JSON_BYTES: + raise ValueError("JSON state exceeds its size limit") + value = json.loads(raw) + if not isinstance(value, dict): + raise ValueError("JSON state is not an object") + return value + + +def deny(reason: str) -> int: + emit("denied", reason) + return 2 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--state", type=Path, default=Path("/var/lib/rigos")) + parser.add_argument("--cmdline", type=Path, default=Path("/proc/cmdline")) + args = parser.parse_args() + + policy_path = args.state / "current" / "policy.json" + xmrig_path = args.state / "current" / "xmrig.json" + + try: + policy = read_object(policy_path) + except (OSError, ValueError, json.JSONDecodeError): + return deny("policy_unreadable") + + if policy.get("schema") != "rigos.policy/v1": + return deny("policy_schema_invalid") + if policy.get("miner_start_mode") != "on_boot": + return deny("manual_start_policy") + + try: + flags = args.cmdline.read_text(encoding="utf-8").split() + except OSError: + return deny("kernel_cmdline_unreadable") + if "rigos.nomine=1" in flags: + return deny("nomine_requested") + if "rigos.stateless=1" in flags: + return deny("stateless_mode") + + try: + read_object(xmrig_path) + except (OSError, ValueError, json.JSONDecodeError): + return deny("xmrig_config_unreadable") + + emit("allowed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 26156d94e3a7a2739dca403b7a4d6203b152c926 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:30:54 +0700 Subject: [PATCH 013/386] miner allow activation through safety gate --- .../usb/includes.chroot/etc/systemd/system/rigos-miner.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service index c8de4c57..d3d83d8a 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service @@ -9,7 +9,9 @@ ConditionPathExists=/var/lib/rigos/xmrig.json Type=simple User=rigos Group=rigos +ExecCondition=/usr/lib/rigos/rigos-miner-gate ExecCondition=/usr/lib/rigos/rigos-config gate +SuccessExitStatus=1 ExecStart=/usr/lib/rigos/xmrig --config=/var/lib/rigos/xmrig.json Restart=on-failure RestartSec=5s From 1880284c3d7778f355e9e7cddb5a8eb5c3d24310 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:32:37 +0700 Subject: [PATCH 014/386] state gate stop retriggering recovery tty --- .../includes.chroot/etc/systemd/system/rigos-state-ready.service | 1 - 1 file changed, 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service index 13f43e7f..eb8bbfa8 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service @@ -3,7 +3,6 @@ Description=Verify RIGOS persistent state readiness DefaultDependencies=no After=rigos-state.service rigos-recovery-access.service Requires=rigos-state.service -Wants=rigos-recovery-access.service Before=local-fs.target rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] From 362330a52f43430fc52618e7d53f278a91f4260e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:32:53 +0700 Subject: [PATCH 015/386] build install lifecycle recovery helpers --- build/usb/hooks/010-rigos.chroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 2ff0de07..0139276a 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,7 +8,7 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/xmrig +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/xmrig systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true From fbc1d8fe3d82855fc1fff534d3d8403e296695fe Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:34:43 +0700 Subject: [PATCH 016/386] miner remove circular legacy condition --- .../usb/includes.chroot/etc/systemd/system/rigos-miner.service | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service index d3d83d8a..ff6d93ba 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service @@ -10,8 +10,6 @@ Type=simple User=rigos Group=rigos ExecCondition=/usr/lib/rigos/rigos-miner-gate -ExecCondition=/usr/lib/rigos/rigos-config gate -SuccessExitStatus=1 ExecStart=/usr/lib/rigos/xmrig --config=/var/lib/rigos/xmrig.json Restart=on-failure RestartSec=5s From bd61b003f2331ac226e8b0b47b58917c6c0cb3bb Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:35:23 +0700 Subject: [PATCH 017/386] test verify lifecycle helpers in appliance --- scripts/verify-usb-appliance.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index d3e20366..748c5403 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -84,15 +84,18 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/xmrig usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/xmrig usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access usr/local/sbin/rigos-state-orchestrate \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" +python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-state-orchestrate" +python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" [[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' grep -Fq 'systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime tmpfiles setup is missing' +grep -Fq 'ExecStart=/usr/local/sbin/rigos-state-orchestrate' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state resume orchestrator is not wired' grep -Fqx 'd /run/rigos 0755 root root -' "$temporary/root/usr/lib/tmpfiles.d/rigos.conf" || die 'shared runtime directory contract is missing' if rg -q '^RuntimeDirectory=rigos$' "$temporary/root/etc/systemd/system"; then die 'a service owns the shared runtime directory'; fi if grep -Eq '^(ExecStartPre|Environment=PATH)=.*compat-bin' "$temporary/root/etc/systemd/system/rigos-state.service"; then die 'state unit executes compatibility code from the runtime directory'; fi @@ -103,6 +106,7 @@ strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/usr/lib/rig strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F -- '--tree' >/dev/null || die 'state initializer does not require hierarchical lsblk output' if strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/run/rigos/compat-bin/lsblk' >/dev/null; then die 'state initializer executes compatibility code from the runtime directory'; fi grep -Fq 'ExecStart=/usr/lib/rigos/rigos-state-ready' "$temporary/root/etc/systemd/system/rigos-state-ready.service" || die 'state readiness verifier is not wired' +if grep -Fq 'Wants=rigos-recovery-access.service' "$temporary/root/etc/systemd/system/rigos-state-ready.service"; then die 'state readiness retriggers interactive recovery access'; fi grep -Fq 'Requires=rigos-state-ready.service' "$temporary/root/etc/systemd/system/rigos-profile-apply.service" || die 'profile apply bypasses state readiness' [[ -x "$temporary/root/usr/lib/rigos/rigos-performance" ]] || die 'performance authority is missing or not executable' grep -Fq 'After=rigos-state-ready.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority ordering is missing' @@ -113,7 +117,7 @@ strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F '/proc/sys/v strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'rigos.performance-status/v1' >/dev/null || die 'performance authority status contract is missing' if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'sysctl' >/dev/null; then die 'performance authority shells out to sysctl'; fi if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -Ei '(/dev/sd|/dev/nvme|cpu model)' >/dev/null; then die 'performance authority contains hardware-name or internal-disk targeting'; fi -grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner policy gate is missing' +grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner safety gate is missing' [[ -f "$temporary/root/etc/systemd/system/rigos-profile-apply.service" ]] || die 'profile apply service is missing' grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config needs-activation' "$temporary/root/etc/systemd/system/rigos-firstboot.service" || die 'first boot activation gate is missing' grep -Fq 'ExecStart=/usr/local/sbin/rigos-recovery-access' "$temporary/root/etc/systemd/system/rigos-recovery-access.service" || die 'local recovery access phase is missing' From 4d822be46931cd067ebdf5ea435af2d575b0fbe5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 16:36:02 +0700 Subject: [PATCH 018/386] test cover state resume and miner gate --- scripts/verify.sh | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/scripts/verify.sh b/scripts/verify.sh index f6ea2739..bf6613e4 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -12,7 +12,11 @@ cargo build --workspace --release --locked bash -n scripts/*.sh build/usb/hooks/*.chroot sh -n build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles -python3 -m py_compile build/usb/includes.chroot/usr/local/sbin/rigos-firstboot build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access +python3 -m py_compile \ + build/usb/includes.chroot/usr/local/sbin/rigos-firstboot \ + build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access \ + build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate \ + build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate grep -Fq 'RIGOS_CONFIG_DUPLICATE_KEY' crates/rigos-config/src/lib.rs grep -Fq 'RIGOS_CONFIG_BOOT_DEVICE_UNPROVEN' crates/rigos-config/src/main.rs @@ -35,7 +39,12 @@ fi grep -Fq 'engine("commit"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot grep -Fq 'engine("activate"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot grep -Fq 'engine("current"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot -grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +grep -Fq 'ExecStart=/usr/local/sbin/rigos-state-orchestrate' build/usb/includes.chroot/etc/systemd/system/rigos-state.service +grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +if grep -Fq 'Wants=rigos-recovery-access.service' build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service; then + echo "state readiness retriggers interactive recovery access" >&2 + exit 1 +fi grep -Fq 'rigos-hugepages.service' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service grep -Fq '/proc/sys/vm/nr_hugepages' crates/rigos-performance/src/lib.rs grep -Fq 'PERFORMANCE_STATUS_SCHEMA' crates/rigos-performance/src/lib.rs @@ -144,6 +153,32 @@ with tempfile.TemporaryDirectory() as temporary: raise SystemExit("selected identity alias did not update the proposal") PY +python3 - <<'PY' +import json +import subprocess +import tempfile +from pathlib import Path + +gate = Path("build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate") +with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + revision = root / "state" / "revisions" / "r1" + revision.mkdir(parents=True) + (root / "state" / "current").symlink_to(Path("revisions") / "r1") + (revision / "policy.json").write_text(json.dumps({"schema":"rigos.policy/v1","timezone":"UTC","miner_start_mode":"on_boot"}), encoding="utf-8") + (revision / "xmrig.json").write_text(json.dumps({"autosave":False,"pools":[]}), encoding="utf-8") + (root / "state" / "activation-status.json").write_text(json.dumps({"schema":"rigos.activation-status/v1","outcome":"activation_failed","revision":"r1","failure_stage":"miner_start"}), encoding="utf-8") + cmdline = root / "cmdline" + cmdline.write_text("boot=live console=tty0\n", encoding="utf-8") + allowed = subprocess.run([sys.executable if False else "python3", str(gate), "--state", str(root / "state"), "--cmdline", str(cmdline)], check=False) + if allowed.returncode != 0: + raise SystemExit("miner gate denied an activation-required on_boot revision") + cmdline.write_text("boot=live rigos.nomine=1 console=tty0\n", encoding="utf-8") + denied = subprocess.run(["python3", str(gate), "--state", str(root / "state"), "--cmdline", str(cmdline)], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + if denied.returncode != 2: + raise SystemExit("miner gate did not preserve the nomine safety block") +PY + if ! rg -q 'label: dos' scripts/build-usb-image.sh; then echo "MBR appliance table declaration missing" >&2 exit 1 From d6f7dce3b85eb44743de466aaec7b3595779e5a2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:36:58 +0700 Subject: [PATCH 019/386] firstboot classify interactive failures --- .../usr/local/sbin/rigos-firstboot | 688 +++++++++++++++--- 1 file changed, 604 insertions(+), 84 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot index 954885d9..a907658a 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot @@ -2,44 +2,255 @@ import json import os import re +import signal import subprocess +import sys import tempfile from pathlib import Path STATE = Path("/var/lib/rigos") ENGINE = "/usr/lib/rigos/rigos-config" -HOST_RE = re.compile(r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$|^[0-9a-fA-F:]+$") +STATE_STATUS = Path("/run/rigos/state-status.json") +UI_STATUS = Path( + os.environ.get( + "RIGOS_FIRSTBOOT_UI_STATUS", + "/var/lib/rigos/firstboot-ui-status.json", + ) +) +BOOT_ID_PATH = Path( + os.environ.get( + "RIGOS_FIRSTBOOT_BOOT_ID", + "/proc/sys/kernel/random/boot_id", + ) +) +WHIPTAIL = os.environ.get("RIGOS_WHIPTAIL", "/usr/bin/whiptail") +SYSTEMD_CAT = os.environ.get("RIGOS_SYSTEMD_CAT", "/usr/bin/systemd-cat") +HOST_RE = re.compile( + r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*" + r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$|^[0-9a-fA-F:]+$" +) SLUG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]{0,63}$") +CURRENT_STAGE = "startup" +CURRENT_DIALOG = None -def dialog(*args: str) -> str: - result = subprocess.run( - ["whiptail", "--title", "RIGOS FIRST BOOT", *args], - check=False, text=True, stderr=subprocess.PIPE, - ) - if result.returncode != 0: - raise SystemExit("Configuration cancelled; reboot to try again.") +class FirstbootCancelled(Exception): + def __init__(self, reason: str, returncode: int | None = None): + super().__init__(reason) + self.reason = reason + self.returncode = returncode + + +class FirstbootFailure(Exception): + def __init__( + self, + reason: str, + returncode: int | None = None, + signal_name: str | None = None, + ): + super().__init__(reason) + self.reason = reason + self.returncode = returncode + self.signal_name = signal_name + + +class FirstbootInterrupted(Exception): + def __init__(self, signal_number: int): + super().__init__(signal_number) + self.signal_number = signal_number + + +class RebootRequested(Exception): + pass + + +def set_stage(stage: str, dialog_kind: str | None = None) -> None: + global CURRENT_STAGE, CURRENT_DIALOG + CURRENT_STAGE = stage + CURRENT_DIALOG = dialog_kind + + +def boot_id() -> str: + try: + return BOOT_ID_PATH.read_text(encoding="ascii").strip() + except OSError: + return "unavailable" + + +def write_atomic_json(path: Path, value: dict) -> None: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}-", + delete=False, + ) as stream: + json.dump(value, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o600) + os.replace(temporary, path) + directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + + +def journal(value: dict, priority: str) -> None: + payload = json.dumps(value, sort_keys=True) + try: + result = subprocess.run( + [ + SYSTEMD_CAT, + "--identifier=rigos-firstboot", + f"--priority={priority}", + ], + input=payload + "\n", + text=True, + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if result.returncode == 0: + return + except OSError: + pass + print(payload, file=sys.stderr, flush=True) + + +def record_event( + outcome: str, + reason: str | None = None, + returncode: int | None = None, + signal_name: str | None = None, +) -> None: + value = { + "schema": "rigos.firstboot-ui-status/v1", + "boot_id": boot_id(), + "outcome": outcome, + "stage": CURRENT_STAGE, + "dialog": CURRENT_DIALOG, + "reason": reason, + "return_code": returncode, + "signal": signal_name, + } + try: + write_atomic_json(UI_STATUS, value) + except OSError: + fallback = dict(value) + fallback["status_write"] = "failed" + journal(fallback, "err") + return + journal(value, "info" if outcome in {"running", "ready", "declined"} else "err") + + +def signal_name(returncode: int) -> str: + try: + return signal.Signals(-returncode).name + except ValueError: + return f"SIGNAL_{-returncode}" + + +def classify_dialog_result(result: subprocess.CompletedProcess, allow_no: bool) -> bool: + if result.returncode == 0: + return True + if result.returncode < 0: + name = signal_name(result.returncode) + raise FirstbootFailure("dialog_signal", result.returncode, name) + if result.returncode == 1: + if allow_no: + record_event("declined", "user_selected_no", result.returncode) + return False + raise FirstbootCancelled("dialog_cancelled", result.returncode) + if result.returncode == 255: + detail = (result.stderr or "").strip() + if not detail: + raise FirstbootCancelled("dialog_escaped", result.returncode) + raise FirstbootFailure("whiptail_runtime_error", result.returncode) + raise FirstbootFailure("whiptail_unexpected_exit", result.returncode) + + +def dialog(*args: str, stage: str = "dialog") -> str: + kind = next((item[2:] for item in args if item.startswith("--")), "unknown") + set_stage(stage, kind) + record_event("running") + try: + result = subprocess.run( + [WHIPTAIL, "--title", "RIGOS FIRST BOOT", *args], + check=False, + text=True, + stderr=subprocess.PIPE, + ) + except OSError: + raise FirstbootFailure("whiptail_exec_error") from None + classify_dialog_result(result, allow_no=False) + record_event("running", "dialog_completed", result.returncode) return result.stderr.strip() -def prompt(label: str, default: str = "", password: bool = False) -> str: +def prompt( + stage: str, + label: str, + default: str = "", + password: bool = False, +) -> str: kind = "--passwordbox" if password else "--inputbox" - return dialog(kind, label, "10", "72", default) + return dialog(kind, label, "10", "72", default, stage=stage) -def confirm(message: str) -> bool: - result = subprocess.run(["whiptail", "--title", "RIGOS FIRST BOOT", "--yesno", message, "14", "72"], check=False) - return result.returncode == 0 +def confirm(message: str, stage: str = "confirmation") -> bool: + set_stage(stage, "yesno") + record_event("running") + try: + result = subprocess.run( + [ + WHIPTAIL, + "--title", + "RIGOS FIRST BOOT", + "--yesno", + message, + "14", + "72", + ], + check=False, + text=True, + stderr=subprocess.PIPE, + ) + except OSError: + raise FirstbootFailure("whiptail_exec_error") from None + accepted = classify_dialog_result(result, allow_no=True) + if accepted: + record_event("running", "dialog_completed", result.returncode) + return accepted def engine(*args: str, stdin: object | None = None) -> tuple[bool, dict]: raw = None if stdin is None else (json.dumps(stdin) + "\n") - result = subprocess.run([ENGINE, *args], input=raw, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + try: + result = subprocess.run( + [ENGINE, *args], + input=raw, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + except OSError: + return False, { + "code": "RIGOS_CONFIG_INVALID_VALUE", + "message": "configuration engine could not be executed", + } source = result.stdout if result.returncode == 0 else result.stderr try: value = json.loads(source.strip().splitlines()[-1]) except (json.JSONDecodeError, IndexError): - value = {"code": "RIGOS_CONFIG_INVALID_VALUE", "message": "configuration engine failed without a diagnostic"} + value = { + "code": "RIGOS_CONFIG_INVALID_VALUE", + "message": "configuration engine failed without a diagnostic", + } return result.returncode == 0, value @@ -56,74 +267,194 @@ def diagnostic_text(error: dict) -> str: def manual_proposal() -> tuple[dict, str, str]: - node = prompt("Node name", "rig01").strip() - timezone = prompt("IANA timezone", "UTC").strip() + node = prompt("node_name", "Node name", "rig01").strip() + timezone = prompt("timezone", "IANA timezone", "UTC").strip() if not Path("/usr/share/zoneinfo", timezone).is_file(): - raise SystemExit("Timezone is not installed or is invalid.") - endpoint = prompt("Pool endpoint host:port", "gulf.moneroocean.stream:10128").strip() - algorithm = prompt("XMRig algorithm", "rx/0").strip() - tls = dialog("--menu", "TLS policy", "12", "60", "2", "required", "TLS enabled", "disabled", "Plain Stratum") - threads = prompt("CPU threads auto or 1 to 1024", "auto").strip() - huge_pages = dialog("--menu", "Huge pages", "12", "60", "2", "enabled", "Enable huge pages", "disabled", "Disable huge pages") - start = dialog("--menu", "Miner start policy", "12", "64", "2", "on_boot", "Start automatically on boot", "manual", "Manual start only") - alias = prompt("Local mining identity alias", "main-xmr").strip() + raise FirstbootFailure("invalid_timezone") + endpoint = prompt( + "pool_endpoint", + "Pool endpoint host:port", + "gulf.moneroocean.stream:10128", + ).strip() + algorithm = prompt("algorithm", "XMRig algorithm", "rx/0").strip() + tls = dialog( + "--menu", + "TLS policy", + "12", + "60", + "2", + "required", + "TLS enabled", + "disabled", + "Plain Stratum", + stage="tls_policy", + ) + threads = prompt("cpu_threads", "CPU threads auto or 1 to 1024", "auto").strip() + huge_pages = dialog( + "--menu", + "Huge pages", + "12", + "60", + "2", + "enabled", + "Enable huge pages", + "disabled", + "Disable huge pages", + stage="huge_pages", + ) + start = dialog( + "--menu", + "Miner start policy", + "12", + "64", + "2", + "on_boot", + "Start automatically on boot", + "manual", + "Manual start only", + stage="miner_start_policy", + ) + alias = prompt("identity_alias", "Local mining identity alias", "main-xmr").strip() if not SLUG_RE.fullmatch(node) or not SLUG_RE.fullmatch(alias): - raise SystemExit("Invalid node name or identity alias.") + raise FirstbootFailure("invalid_node_or_alias") if endpoint.count(":") != 1: - raise SystemExit("Endpoint must use host:port syntax.") + raise FirstbootFailure("invalid_pool_endpoint") host, port_text = endpoint.rsplit(":", 1) - if not HOST_RE.fullmatch(host) or not port_text.isdigit() or not 1 <= int(port_text) <= 65535: - raise SystemExit("Invalid pool endpoint.") - if threads != "auto" and (not threads.isdigit() or not 1 <= int(threads) <= 1024): - raise SystemExit("Invalid CPU thread count.") + if ( + not HOST_RE.fullmatch(host) + or not port_text.isdigit() + or not 1 <= int(port_text) <= 65535 + ): + raise FirstbootFailure("invalid_pool_endpoint") + if threads != "auto" and ( + not threads.isdigit() or not 1 <= int(threads) <= 1024 + ): + raise FirstbootFailure("invalid_cpu_threads") proposal = { "schema": "rigos.config-proposal/v1", - "profile": {"node_name": node, "timezone": timezone, "flight_source": "interactive", "flight_ref": None, "miner_start_mode": start}, + "profile": { + "node_name": node, + "timezone": timezone, + "flight_source": "interactive", + "flight_ref": None, + "miner_start_mode": start, + }, "flight_sheet": { - "schema": "rigos.flight-sheet/v1", "name": "manual-xmrig", "coin": "XMR", "backend": "xmrig", "algorithm": algorithm, - "pools": [{"host": host.lower(), "port": int(port_text), "tls": tls == "required", "priority": 0}], - "identity_ref": alias, "worker_template": "{node_name}", - "cpu": {"threads": threads if threads == "auto" else int(threads), "huge_pages": huge_pages == "enabled", "max_threads_hint": 100}, + "schema": "rigos.flight-sheet/v1", + "name": "manual-xmrig", + "coin": "XMR", + "backend": "xmrig", + "algorithm": algorithm, + "pools": [ + { + "host": host.lower(), + "port": int(port_text), + "tls": tls == "required", + "priority": 0, + } + ], + "identity_ref": alias, + "worker_template": "{node_name}", + "cpu": { + "threads": threads if threads == "auto" else int(threads), + "huge_pages": huge_pages == "enabled", + "max_threads_hint": 100, + }, }, - "provenance": None, "source_sha256": "manual", + "provenance": None, + "source_sha256": "manual", } return proposal, alias, "" def prepare_import(path: str) -> tuple[dict, bool]: while True: + set_stage("prepare_import") ok, result = engine("prepare", "--output", path) if ok: return json.loads(Path(path).read_text(encoding="utf-8")), False if result.get("code") == "RIGOS_CONFIG_FILE_MISSING": return manual_proposal()[0], True - if result.get("code") == "RIGOS_FLIGHT_SHEET_MISSING" and "interactive" in result.get("message", ""): + if ( + result.get("code") == "RIGOS_FLIGHT_SHEET_MISSING" + and "interactive" in result.get("message", "") + ): found, choices = engine("discover") if found: - items = [("manual", "Configure manually"), ("none", "Do not configure mining now")] - items += [(f"native:{name}", f"Native {name}") for name in choices.get("native", [])] - items += [(f"import:{name}", f"Import {name}") for name in choices.get("import", [])] + items = [ + ("manual", "Configure manually"), + ("none", "Do not configure mining now"), + ] + items += [ + (f"native:{name}", f"Native {name}") + for name in choices.get("native", []) + ] + items += [ + (f"import:{name}", f"Import {name}") + for name in choices.get("import", []) + ] flat = [value for pair in items for value in pair] - selected = dialog("--menu", "Select Flight Sheet", "20", "76", str(min(12, len(items))), *flat) + selected = dialog( + "--menu", + "Select Flight Sheet", + "20", + "76", + str(min(12, len(items))), + *flat, + stage="flight_sheet_selection", + ) if selected == "manual": return manual_proposal()[0], True if selected == "none": - raise SystemExit("Mining left unconfigured. Reboot to configure later.") + raise FirstbootCancelled("mining_left_unconfigured") source, reference = selected.split(":", 1) - ok, prepared = engine("prepare", "--output", path, "--flight-source", source, "--flight-ref", reference) + ok, prepared = engine( + "prepare", + "--output", + path, + "--flight-source", + source, + "--flight-ref", + reference, + ) if ok: return json.loads(Path(path).read_text(encoding="utf-8")), False result = prepared - action = dialog("--menu", diagnostic_text(result), "20", "76", "4", "retry", "Retry import", "manual", "Continue with manual setup", "diagnostics", "Show diagnostics", "reboot", "Reboot") + action = dialog( + "--menu", + diagnostic_text(result), + "20", + "76", + "4", + "retry", + "Retry import", + "manual", + "Continue with manual setup", + "diagnostics", + "Show diagnostics", + "reboot", + "Reboot", + stage="import_error_action", + ) if action == "retry": continue if action == "diagnostics": - dialog("--msgbox", diagnostic_text(result), "18", "76") + dialog( + "--msgbox", + diagnostic_text(result), + "18", + "76", + stage="import_diagnostics", + ) continue if action == "reboot": subprocess.run(["systemctl", "reboot"], check=False) - raise SystemExit(0) - if confirm("Discard invalid rigos.conf for this boot and continue manually\n\nThe source file will not be changed or deleted."): + raise RebootRequested + if confirm( + "Discard invalid rigos.conf for this boot and continue manually\n\n" + "The source file will not be changed or deleted.", + stage="discard_invalid_import", + ): return manual_proposal()[0], True @@ -144,64 +475,169 @@ def resolve_identity(proposal: dict) -> dict: if external and mapping_path.is_file(): try: mapping_document = json.loads(mapping_path.read_text(encoding="utf-8")) - mappings = mapping_document.get("mappings", []) if mapping_document.get("schema") == "rigos.external-identity-map/v1" else [] + mappings = ( + mapping_document.get("mappings", []) + if mapping_document.get("schema") + == "rigos.external-identity-map/v1" + else [] + ) except (OSError, json.JSONDecodeError): mappings = [] - mapping = next((item for item in mappings if item.get("source") == external.get("source") and item.get("external_type") == external.get("external_type") and item.get("external_value") == external.get("external_value")), None) - mapped_identity = next((item for item in candidates if mapping and item.get("alias") == mapping.get("identity_ref")), None) + mapping = next( + ( + item + for item in mappings + if item.get("source") == external.get("source") + and item.get("external_type") == external.get("external_type") + and item.get("external_value") == external.get("external_value") + ), + None, + ) + mapped_identity = next( + ( + item + for item in candidates + if mapping and item.get("alias") == mapping.get("identity_ref") + ), + None, + ) if mapped_identity: - source_status = "same source hash" if mapping.get("confirmed_source_sha256") == proposal.get("source_sha256") else "source hash changed" - if confirm(f"Reuse confirmed external identity mapping\n\nLocal alias {mapped_identity['alias']}\n{source_status}\nValue suffix ****{mapped_identity['value'][-4:]}"): + source_status = ( + "same source hash" + if mapping.get("confirmed_source_sha256") + == proposal.get("source_sha256") + else "source hash changed" + ) + if confirm( + "Reuse confirmed external identity mapping\n\n" + f"Local alias {mapped_identity['alias']}\n{source_status}\n" + f"Value suffix ****{mapped_identity['value'][-4:]}", + stage="reuse_external_identity", + ): proposal["flight_sheet"]["identity_ref"] = mapped_identity["alias"] return mapped_identity exact = next((item for item in candidates if item.get("alias") == requested), None) - if exact and confirm(f"Reuse local identity alias {requested}\n\nValue suffix {exact['value'][-6:].rjust(len(exact['value'][-6:]) + 4, '*')}"): + if exact and confirm( + f"Reuse local identity alias {requested}\n\n" + f"Value suffix {exact['value'][-6:].rjust(len(exact['value'][-6:]) + 4, '*')}", + stage="reuse_local_identity", + ): proposal["flight_sheet"]["identity_ref"] = exact["alias"] return exact - aliases = [(f"existing:{item['alias']}", f"Use {item['alias']} suffix ****{item['value'][-4:]}") for item in candidates] - choices = aliases + [("new", "Add a new local identity"), ("none", "Leave mining unconfigured"), ("reboot", "Cancel and reboot")] - selected = dialog("--menu", "Mining identity is unresolved", "18", "76", str(min(12, len(choices))), *[value for pair in choices for value in pair]) + aliases = [ + ( + f"existing:{item['alias']}", + f"Use {item['alias']} suffix ****{item['value'][-4:]}", + ) + for item in candidates + ] + choices = aliases + [ + ("new", "Add a new local identity"), + ("none", "Leave mining unconfigured"), + ("reboot", "Cancel and reboot"), + ] + selected = dialog( + "--menu", + "Mining identity is unresolved", + "18", + "76", + str(min(12, len(choices))), + *[value for pair in choices for value in pair], + stage="identity_selection", + ) if selected.startswith("existing:"): alias = selected.split(":", 1)[1] selected_identity = next(item for item in candidates if item["alias"] == alias) - if not confirm(f"Use local identity alias {alias} for this Flight Sheet\n\nValue suffix ****{selected_identity['value'][-4:]}"): + if not confirm( + f"Use local identity alias {alias} for this Flight Sheet\n\n" + f"Value suffix ****{selected_identity['value'][-4:]}", + stage="confirm_existing_identity", + ): return resolve_identity(proposal) proposal["flight_sheet"]["identity_ref"] = alias return selected_identity if selected == "none": subprocess.run(["systemctl", "stop", "rigos-miner.service"], check=False) - raise SystemExit("Mining remains unconfigured and stopped.") + raise FirstbootCancelled("mining_left_unconfigured") if selected == "reboot": subprocess.run(["systemctl", "reboot"], check=False) - raise SystemExit(0) - alias = prompt("Local identity alias", requested if SLUG_RE.fullmatch(requested) else "main-xmr").strip() - value = prompt("Mining identity value", password=True).strip() - if not SLUG_RE.fullmatch(alias) or not value or len(value) > 512 or any(character.isspace() or ord(character) < 32 for character in value): - raise SystemExit("Invalid local mining identity.") + raise RebootRequested + alias = prompt( + "new_identity_alias", + "Local identity alias", + requested if SLUG_RE.fullmatch(requested) else "main-xmr", + ).strip() + value = prompt( + "new_identity_value", + "Mining identity value", + password=True, + ).strip() + if ( + not SLUG_RE.fullmatch(alias) + or not value + or len(value) > 512 + or any(character.isspace() or ord(character) < 32 for character in value) + ): + raise FirstbootFailure("invalid_identity") proposal["flight_sheet"]["identity_ref"] = alias - return {"schema": "rigos.identity/v1", "alias": alias, "kind": "mining_identity", "value": value, "created_locally": True} + return { + "schema": "rigos.identity/v1", + "alias": alias, + "kind": "mining_identity", + "value": value, + "created_locally": True, + } def main() -> None: - status_path = Path("/run/rigos/state-status.json") + set_stage("state_preflight") try: - state_outcome = json.loads(status_path.read_text(encoding="utf-8"))["outcome"] + state_outcome = json.loads(STATE_STATUS.read_text(encoding="utf-8"))["outcome"] except (OSError, KeyError, json.JSONDecodeError): state_outcome = "unavailable" if state_outcome != "ready": - dialog("--msgbox", f"Persistent state status: {state_outcome}\n\nConfiguration and mining are blocked until the verified USB state is ready.", "13", "72") - raise SystemExit(1) + dialog( + "--msgbox", + f"Persistent state status: {state_outcome}\n\n" + "Configuration and mining are blocked until the verified USB state is ready.", + "13", + "72", + stage="state_not_ready", + ) + raise FirstbootFailure("state_not_ready") + set_stage("current_revision") existing, current = engine("current") if existing and current.get("outcome") == "configuration_committed": + set_stage("activation_retry") activated, result = engine("activate") if not activated: - dialog("--msgbox", f"Configuration activation failed\n{diagnostic_text(result)}\n\nThe committed revision was preserved and mining remains stopped. Reboot to retry activation.", "18", "76") - raise SystemExit(1) - dialog("--msgbox", f"Existing configuration activated.\nMiner started: {result.get('miner_started', False)}", "10", "64") + dialog( + "--msgbox", + "Configuration activation failed\n" + f"{diagnostic_text(result)}\n\n" + "The committed revision was preserved and mining remains stopped. " + "Reboot to retry activation.", + "18", + "76", + stage="activation_retry_failed", + ) + raise FirstbootFailure("activation_retry_failed") + dialog( + "--msgbox", + "Existing configuration activated.\n" + f"Miner started: {result.get('miner_started', False)}", + "10", + "64", + stage="activation_retry_ready", + ) return - with tempfile.NamedTemporaryFile(prefix="rigos-proposal-", dir="/run/rigos", delete=False) as stream: + with tempfile.NamedTemporaryFile( + prefix="rigos-proposal-", + dir="/run/rigos", + delete=False, + ) as stream: proposal_path = stream.name os.unlink(proposal_path) try: @@ -212,22 +648,53 @@ def main() -> None: primary = sheet["pools"][0] summary = ( f"Node {profile['node_name']}\nFlight Sheet {sheet['name']}\n" - f"Pool {primary['host']}:{primary['port']}\nFallback pools {len(sheet['pools']) - 1}\n" - f"Algorithm {sheet['algorithm']}\nTLS {'enabled' if primary['tls'] else 'disabled'}\n" + f"Pool {primary['host']}:{primary['port']}\n" + f"Fallback pools {len(sheet['pools']) - 1}\n" + f"Algorithm {sheet['algorithm']}\n" + f"TLS {'enabled' if primary['tls'] else 'disabled'}\n" f"Identity {identity['alias']} suffix ****{identity['value'][-4:]}\n" - f"Start policy {profile['miner_start_mode']}\nHuge pages {sheet['cpu']['huge_pages']}" + f"Start policy {profile['miner_start_mode']}\n" + f"Huge pages {sheet['cpu']['huge_pages']}" ) - if not confirm(summary + "\n\nCommit this configuration locally?"): - raise SystemExit("Configuration not confirmed; no changes were written.") + if not confirm( + summary + "\n\nCommit this configuration locally?", + stage="commit_confirmation", + ): + raise FirstbootCancelled("configuration_not_confirmed") + set_stage("configuration_commit") ok, result = engine("commit", stdin={"proposal": proposal, "identity": identity}) if not ok: - dialog("--msgbox", f"Configuration commit failed\n{diagnostic_text(result)}\n\nNo activation was attempted.", "18", "76") - raise SystemExit(1) + dialog( + "--msgbox", + "Configuration commit failed\n" + f"{diagnostic_text(result)}\n\nNo activation was attempted.", + "18", + "76", + stage="configuration_commit_failed", + ) + raise FirstbootFailure("configuration_commit_failed") + set_stage("configuration_activation") activated, activation = engine("activate") if not activated: - dialog("--msgbox", f"Configuration was committed but activation failed\n{diagnostic_text(activation)}\n\nThe revision was preserved and mining remains stopped. Reboot to retry activation.", "18", "76") - raise SystemExit(1) - dialog("--msgbox", f"Configuration saved and activated atomically.\nMiner started: {activation.get('miner_started', False)}", "10", "64") + dialog( + "--msgbox", + "Configuration was committed but activation failed\n" + f"{diagnostic_text(activation)}\n\n" + "The revision was preserved and mining remains stopped. " + "Reboot to retry activation.", + "18", + "76", + stage="configuration_activation_failed", + ) + raise FirstbootFailure("configuration_activation_failed") + dialog( + "--msgbox", + "Configuration saved and activated atomically.\n" + f"Miner started: {activation.get('miner_started', False)}", + "10", + "64", + stage="configuration_ready", + ) finally: try: os.unlink(proposal_path) @@ -235,5 +702,58 @@ def main() -> None: pass +def handle_signal(signal_number: int, _frame: object) -> None: + name = signal.Signals(signal_number).name + record_event("failed", "process_signal", signal_name=name) + raise FirstbootInterrupted(signal_number) + + +def run() -> int: + signal.signal(signal.SIGHUP, handle_signal) + signal.signal(signal.SIGTERM, handle_signal) + set_stage("startup") + record_event("running", "firstboot_started") + try: + main() + except FirstbootCancelled as error: + record_event("cancelled", error.reason, error.returncode) + print( + f"RIGOS firstboot cancelled stage={CURRENT_STAGE} reason={error.reason}", + file=sys.stderr, + flush=True, + ) + return 10 + except FirstbootFailure as error: + record_event( + "failed", + error.reason, + error.returncode, + error.signal_name, + ) + print( + f"RIGOS firstboot failed stage={CURRENT_STAGE} reason={error.reason}", + file=sys.stderr, + flush=True, + ) + return 20 + except FirstbootInterrupted as error: + return 128 + error.signal_number + except RebootRequested: + record_event("cancelled", "reboot_requested") + return 0 + except Exception as error: + reason = f"unexpected_{type(error).__name__}" + record_event("failed", reason) + print( + f"RIGOS firstboot failed stage={CURRENT_STAGE} reason={reason}", + file=sys.stderr, + flush=True, + ) + return 20 + set_stage("complete") + record_event("ready", "firstboot_completed") + return 0 + + if __name__ == "__main__": - main() + raise SystemExit(run()) From 464ebdb7683feef80501defdc1c016ff9e497609 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:39:59 +0700 Subject: [PATCH 020/386] test firstboot interaction classification --- .../tests/firstboot_observability.rs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 crates/rigos-config/tests/firstboot_observability.rs diff --git a/crates/rigos-config/tests/firstboot_observability.rs b/crates/rigos-config/tests/firstboot_observability.rs new file mode 100644 index 00000000..25194f6c --- /dev/null +++ b/crates/rigos-config/tests/firstboot_observability.rs @@ -0,0 +1,99 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use uuid::Uuid; + +fn firstboot_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../build/usb/includes.chroot/usr/local/sbin/rigos-firstboot") +} + +#[test] +fn firstboot_classifies_dialog_events_without_persisting_values() { + let root = std::env::temp_dir().join(format!("rigos-firstboot-ui-{}", Uuid::new_v4())); + fs::create_dir_all(&root).expect("create firstboot test directory"); + let program = r#" +import json +import os +import runpy +import signal +import sys +from pathlib import Path + +source = Path(sys.argv[1]) +root = Path(sys.argv[2]) +status_path = root / 'firstboot-ui-status.json' +boot_id_path = root / 'boot-id' +boot_id_path.write_text('boot-test\n', encoding='ascii') +os.environ['RIGOS_FIRSTBOOT_UI_STATUS'] = str(status_path) +os.environ['RIGOS_FIRSTBOOT_BOOT_ID'] = str(boot_id_path) +namespace = runpy.run_path(str(source), run_name='rigos_firstboot_observability_test') +namespace['journal'] = lambda _value, _priority: None + +class Result: + def __init__(self, returncode, stderr=''): + self.returncode = returncode + self.stderr = stderr + + +def status(): + return json.loads(status_path.read_text(encoding='utf-8')) + + +def run_dialog(returncode, stderr=''): + namespace['subprocess'].run = lambda *_args, **_kwargs: Result(returncode, stderr) + namespace['main'] = lambda: namespace['dialog']( + '--menu', 'synthetic', '10', '60', '1', 'item', 'value', stage='synthetic_dialog' + ) + return namespace['run']() + +assert run_dialog(1) == 10 +value = status() +assert value['outcome'] == 'cancelled' +assert value['reason'] == 'dialog_cancelled' +assert value['return_code'] == 1 + +assert run_dialog(255) == 10 +value = status() +assert value['outcome'] == 'cancelled' +assert value['reason'] == 'dialog_escaped' + +assert run_dialog(255, 'SENTINEL_PRIVATE_DIALOG_VALUE') == 20 +value = status() +assert value['outcome'] == 'failed' +assert value['reason'] == 'whiptail_runtime_error' +assert 'SENTINEL_PRIVATE_DIALOG_VALUE' not in json.dumps(value) + +assert run_dialog(-signal.SIGHUP) == 20 +value = status() +assert value['outcome'] == 'failed' +assert value['reason'] == 'dialog_signal' +assert value['signal'] == 'SIGHUP' + +namespace['subprocess'].run = lambda *_args, **_kwargs: Result(1) +assert namespace['confirm']('synthetic confirmation', stage='synthetic_confirmation') is False +value = status() +assert value['outcome'] == 'declined' +assert value['reason'] == 'user_selected_no' + +namespace['main'] = lambda: None +assert namespace['run']() == 0 +value = status() +assert value['outcome'] == 'ready' +assert value['reason'] == 'firstboot_completed' +assert value['stage'] == 'complete' +assert set(value) == { + 'schema', 'boot_id', 'outcome', 'stage', 'dialog', 'reason', + 'return_code', 'signal' +} +"#; + let result = Command::new("python3") + .arg("-c") + .arg(program) + .arg(firstboot_path()) + .arg(&root) + .status() + .expect("run firstboot observability fixture"); + let _ = fs::remove_dir_all(&root); + assert!(result.success(), "firstboot observability fixture failed"); +} From 2b21a7243704a582f7099ed910498f391bddd64a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:41:55 +0700 Subject: [PATCH 021/386] test format firstboot observability fixture From 01c4cdb49ed7d0d1f80753f0c05c4cf79e04c004 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:46:20 +0700 Subject: [PATCH 022/386] firstboot preserve verifier compatibility --- .../usr/local/sbin/rigos-firstboot | 783 +++++------------- 1 file changed, 199 insertions(+), 584 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot index a907658a..d64d7681 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot @@ -7,90 +7,62 @@ import subprocess import sys import tempfile from pathlib import Path - -STATE = Path("/var/lib/rigos") -ENGINE = "/usr/lib/rigos/rigos-config" -STATE_STATUS = Path("/run/rigos/state-status.json") -UI_STATUS = Path( - os.environ.get( - "RIGOS_FIRSTBOOT_UI_STATUS", - "/var/lib/rigos/firstboot-ui-status.json", - ) -) -BOOT_ID_PATH = Path( - os.environ.get( - "RIGOS_FIRSTBOOT_BOOT_ID", - "/proc/sys/kernel/random/boot_id", - ) -) -WHIPTAIL = os.environ.get("RIGOS_WHIPTAIL", "/usr/bin/whiptail") -SYSTEMD_CAT = os.environ.get("RIGOS_SYSTEMD_CAT", "/usr/bin/systemd-cat") -HOST_RE = re.compile( - r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*" - r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$|^[0-9a-fA-F:]+$" -) -SLUG_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]{0,63}$") -CURRENT_STAGE = "startup" +os.environ.setdefault('TERM', 'linux') +STATE = Path('/var/lib/rigos') +ENGINE = '/usr/lib/rigos/rigos-config' +STATE_STATUS = Path('/run/rigos/state-status.json') +UI_STATUS = Path(os.environ.get('RIGOS_FIRSTBOOT_UI_STATUS', '/var/lib/rigos/firstboot-ui-status.json')) +BOOT_ID_PATH = Path(os.environ.get('RIGOS_FIRSTBOOT_BOOT_ID', '/proc/sys/kernel/random/boot_id')) +WHIPTAIL = os.environ.get('RIGOS_WHIPTAIL', '/usr/bin/whiptail') +SYSTEMD_CAT = os.environ.get('RIGOS_SYSTEMD_CAT', '/usr/bin/systemd-cat') +HOST_RE = re.compile('^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$|^[0-9a-fA-F:]+$') +SLUG_RE = re.compile('^[A-Za-z0-9][A-Za-z0-9-]{0,63}$') +CURRENT_STAGE = 'startup' CURRENT_DIALOG = None - class FirstbootCancelled(Exception): - def __init__(self, reason: str, returncode: int | None = None): + + def __init__(self, reason: str, returncode: int | None=None): super().__init__(reason) self.reason = reason self.returncode = returncode - class FirstbootFailure(Exception): - def __init__( - self, - reason: str, - returncode: int | None = None, - signal_name: str | None = None, - ): + + def __init__(self, reason: str, returncode: int | None=None, signal_name: str | None=None): super().__init__(reason) self.reason = reason self.returncode = returncode self.signal_name = signal_name - class FirstbootInterrupted(Exception): + def __init__(self, signal_number: int): super().__init__(signal_number) self.signal_number = signal_number - class RebootRequested(Exception): pass - -def set_stage(stage: str, dialog_kind: str | None = None) -> None: +def set_stage(stage: str, dialog_kind: str | None=None) -> None: global CURRENT_STAGE, CURRENT_DIALOG CURRENT_STAGE = stage CURRENT_DIALOG = dialog_kind - def boot_id() -> str: try: - return BOOT_ID_PATH.read_text(encoding="ascii").strip() + return BOOT_ID_PATH.read_text(encoding='ascii').strip() except OSError: - return "unavailable" - + return 'unavailable' def write_atomic_json(path: Path, value: dict) -> None: - with tempfile.NamedTemporaryFile( - mode="w", - encoding="utf-8", - dir=path.parent, - prefix=f".{path.name}-", - delete=False, - ) as stream: + with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=path.parent, prefix=f'.{path.name}-', delete=False) as stream: json.dump(value, stream, sort_keys=True) - stream.write("\n") + stream.write('\n') stream.flush() os.fsync(stream.fileno()) temporary = Path(stream.name) - temporary.chmod(0o600) + temporary.chmod(384) os.replace(temporary, path) directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) try: @@ -98,662 +70,305 @@ def write_atomic_json(path: Path, value: dict) -> None: finally: os.close(directory) - def journal(value: dict, priority: str) -> None: payload = json.dumps(value, sort_keys=True) + if not os.environ.get('INVOCATION_ID'): + print(payload, file=sys.stderr, flush=True) + return try: - result = subprocess.run( - [ - SYSTEMD_CAT, - "--identifier=rigos-firstboot", - f"--priority={priority}", - ], - input=payload + "\n", - text=True, - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + result = subprocess.run([SYSTEMD_CAT, '--identifier=rigos-firstboot', f'--priority={priority}'], input=payload + '\n', text=True, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if result.returncode == 0: return except OSError: pass print(payload, file=sys.stderr, flush=True) - -def record_event( - outcome: str, - reason: str | None = None, - returncode: int | None = None, - signal_name: str | None = None, -) -> None: - value = { - "schema": "rigos.firstboot-ui-status/v1", - "boot_id": boot_id(), - "outcome": outcome, - "stage": CURRENT_STAGE, - "dialog": CURRENT_DIALOG, - "reason": reason, - "return_code": returncode, - "signal": signal_name, - } +def record_event(outcome: str, reason: str | None=None, returncode: int | None=None, signal_name: str | None=None) -> None: + value = {'schema': 'rigos.firstboot-ui-status/v1', 'boot_id': boot_id(), 'outcome': outcome, 'stage': CURRENT_STAGE, 'dialog': CURRENT_DIALOG, 'reason': reason, 'return_code': returncode, 'signal': signal_name} try: write_atomic_json(UI_STATUS, value) except OSError: fallback = dict(value) - fallback["status_write"] = "failed" - journal(fallback, "err") + fallback['status_write'] = 'failed' + journal(fallback, 'err') return - journal(value, "info" if outcome in {"running", "ready", "declined"} else "err") - + journal(value, 'info' if outcome in {'running', 'ready', 'declined'} else 'err') def signal_name(returncode: int) -> str: try: return signal.Signals(-returncode).name except ValueError: - return f"SIGNAL_{-returncode}" - + return f'SIGNAL_{-returncode}' def classify_dialog_result(result: subprocess.CompletedProcess, allow_no: bool) -> bool: if result.returncode == 0: return True if result.returncode < 0: name = signal_name(result.returncode) - raise FirstbootFailure("dialog_signal", result.returncode, name) + raise FirstbootFailure('dialog_signal', result.returncode, name) if result.returncode == 1: if allow_no: - record_event("declined", "user_selected_no", result.returncode) + record_event('declined', 'user_selected_no', result.returncode) return False - raise FirstbootCancelled("dialog_cancelled", result.returncode) + raise FirstbootCancelled('dialog_cancelled', result.returncode) if result.returncode == 255: - detail = (result.stderr or "").strip() + detail = (result.stderr or '').strip() if not detail: - raise FirstbootCancelled("dialog_escaped", result.returncode) - raise FirstbootFailure("whiptail_runtime_error", result.returncode) - raise FirstbootFailure("whiptail_unexpected_exit", result.returncode) - - -def dialog(*args: str, stage: str = "dialog") -> str: - kind = next((item[2:] for item in args if item.startswith("--")), "unknown") - set_stage(stage, kind) - record_event("running") + raise FirstbootCancelled('dialog_escaped', result.returncode) + raise FirstbootFailure('whiptail_runtime_error', result.returncode) + raise FirstbootFailure('whiptail_unexpected_exit', result.returncode) + +def dialog(*args: str, stage: str | None=None) -> str: + kind = next((item[2:] for item in args if item.startswith('--')), 'unknown') + set_stage(stage or CURRENT_STAGE, kind) + record_event('running') try: - result = subprocess.run( - [WHIPTAIL, "--title", "RIGOS FIRST BOOT", *args], - check=False, - text=True, - stderr=subprocess.PIPE, - ) + result = subprocess.run([WHIPTAIL, '--title', 'RIGOS FIRST BOOT', *args], check=False, text=True, stderr=subprocess.PIPE) except OSError: - raise FirstbootFailure("whiptail_exec_error") from None + raise FirstbootFailure('whiptail_exec_error') from None classify_dialog_result(result, allow_no=False) - record_event("running", "dialog_completed", result.returncode) + record_event('running', 'dialog_completed', result.returncode) return result.stderr.strip() +def prompt(stage: str, label: str, default: str='', password: bool=False) -> str: + kind = '--passwordbox' if password else '--inputbox' + return dialog(kind, label, '10', '72', default, stage=stage) -def prompt( - stage: str, - label: str, - default: str = "", - password: bool = False, -) -> str: - kind = "--passwordbox" if password else "--inputbox" - return dialog(kind, label, "10", "72", default, stage=stage) - - -def confirm(message: str, stage: str = "confirmation") -> bool: - set_stage(stage, "yesno") - record_event("running") +def confirm(message: str, stage: str | None=None) -> bool: + set_stage(stage or CURRENT_STAGE, 'yesno') + record_event('running') try: - result = subprocess.run( - [ - WHIPTAIL, - "--title", - "RIGOS FIRST BOOT", - "--yesno", - message, - "14", - "72", - ], - check=False, - text=True, - stderr=subprocess.PIPE, - ) + result = subprocess.run([WHIPTAIL, '--title', 'RIGOS FIRST BOOT', '--yesno', message, '14', '72'], check=False, text=True, stderr=subprocess.PIPE) except OSError: - raise FirstbootFailure("whiptail_exec_error") from None + raise FirstbootFailure('whiptail_exec_error') from None accepted = classify_dialog_result(result, allow_no=True) if accepted: - record_event("running", "dialog_completed", result.returncode) + record_event('running', 'dialog_completed', result.returncode) return accepted - -def engine(*args: str, stdin: object | None = None) -> tuple[bool, dict]: - raw = None if stdin is None else (json.dumps(stdin) + "\n") +def engine(*args: str, stdin: object | None=None) -> tuple[bool, dict]: + raw = None if stdin is None else json.dumps(stdin) + '\n' try: - result = subprocess.run( - [ENGINE, *args], - input=raw, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) + result = subprocess.run([ENGINE, *args], input=raw, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) except OSError: - return False, { - "code": "RIGOS_CONFIG_INVALID_VALUE", - "message": "configuration engine could not be executed", - } + return (False, {'code': 'RIGOS_CONFIG_INVALID_VALUE', 'message': 'configuration engine could not be executed'}) source = result.stdout if result.returncode == 0 else result.stderr try: value = json.loads(source.strip().splitlines()[-1]) except (json.JSONDecodeError, IndexError): - value = { - "code": "RIGOS_CONFIG_INVALID_VALUE", - "message": "configuration engine failed without a diagnostic", - } - return result.returncode == 0, value - + value = {'code': 'RIGOS_CONFIG_INVALID_VALUE', 'message': 'configuration engine failed without a diagnostic'} + return (result.returncode == 0, value) def diagnostic_text(error: dict) -> str: - parts = [error.get("code", "RIGOS_CONFIG_INVALID_VALUE")] - location = error.get("file") or "rig.conf" - if error.get("line"): + parts = [error.get('code', 'RIGOS_CONFIG_INVALID_VALUE')] + location = error.get('file') or 'rig.conf' + if error.get('line'): location += f" line {error['line']}" parts.append(location) - if error.get("key"): + if error.get('key'): parts.append(f"Key {error['key']}") - parts.append(error.get("message", "Invalid configuration")) - return "\n".join(parts) - + parts.append(error.get('message', 'Invalid configuration')) + return '\n'.join(parts) def manual_proposal() -> tuple[dict, str, str]: - node = prompt("node_name", "Node name", "rig01").strip() - timezone = prompt("timezone", "IANA timezone", "UTC").strip() - if not Path("/usr/share/zoneinfo", timezone).is_file(): - raise FirstbootFailure("invalid_timezone") - endpoint = prompt( - "pool_endpoint", - "Pool endpoint host:port", - "gulf.moneroocean.stream:10128", - ).strip() - algorithm = prompt("algorithm", "XMRig algorithm", "rx/0").strip() - tls = dialog( - "--menu", - "TLS policy", - "12", - "60", - "2", - "required", - "TLS enabled", - "disabled", - "Plain Stratum", - stage="tls_policy", - ) - threads = prompt("cpu_threads", "CPU threads auto or 1 to 1024", "auto").strip() - huge_pages = dialog( - "--menu", - "Huge pages", - "12", - "60", - "2", - "enabled", - "Enable huge pages", - "disabled", - "Disable huge pages", - stage="huge_pages", - ) - start = dialog( - "--menu", - "Miner start policy", - "12", - "64", - "2", - "on_boot", - "Start automatically on boot", - "manual", - "Manual start only", - stage="miner_start_policy", - ) - alias = prompt("identity_alias", "Local mining identity alias", "main-xmr").strip() + node = prompt('node_name', 'Node name', 'rig01').strip() + timezone = prompt('timezone', 'IANA timezone', 'UTC').strip() + if not Path('/usr/share/zoneinfo', timezone).is_file(): + raise FirstbootFailure('invalid_timezone') + endpoint = prompt('pool_endpoint', 'Pool endpoint host:port', 'gulf.moneroocean.stream:10128').strip() + algorithm = prompt('algorithm', 'XMRig algorithm', 'rx/0').strip() + tls = dialog('--menu', 'TLS policy', '12', '60', '2', 'required', 'TLS enabled', 'disabled', 'Plain Stratum', stage='tls_policy') + threads = prompt('cpu_threads', 'CPU threads auto or 1 to 1024', 'auto').strip() + huge_pages = dialog('--menu', 'Huge pages', '12', '60', '2', 'enabled', 'Enable huge pages', 'disabled', 'Disable huge pages', stage='huge_pages') + start = dialog('--menu', 'Miner start policy', '12', '64', '2', 'on_boot', 'Start automatically on boot', 'manual', 'Manual start only', stage='miner_start_policy') + alias = prompt('identity_alias', 'Local mining identity alias', 'main-xmr').strip() if not SLUG_RE.fullmatch(node) or not SLUG_RE.fullmatch(alias): - raise FirstbootFailure("invalid_node_or_alias") - if endpoint.count(":") != 1: - raise FirstbootFailure("invalid_pool_endpoint") - host, port_text = endpoint.rsplit(":", 1) - if ( - not HOST_RE.fullmatch(host) - or not port_text.isdigit() - or not 1 <= int(port_text) <= 65535 - ): - raise FirstbootFailure("invalid_pool_endpoint") - if threads != "auto" and ( - not threads.isdigit() or not 1 <= int(threads) <= 1024 - ): - raise FirstbootFailure("invalid_cpu_threads") - proposal = { - "schema": "rigos.config-proposal/v1", - "profile": { - "node_name": node, - "timezone": timezone, - "flight_source": "interactive", - "flight_ref": None, - "miner_start_mode": start, - }, - "flight_sheet": { - "schema": "rigos.flight-sheet/v1", - "name": "manual-xmrig", - "coin": "XMR", - "backend": "xmrig", - "algorithm": algorithm, - "pools": [ - { - "host": host.lower(), - "port": int(port_text), - "tls": tls == "required", - "priority": 0, - } - ], - "identity_ref": alias, - "worker_template": "{node_name}", - "cpu": { - "threads": threads if threads == "auto" else int(threads), - "huge_pages": huge_pages == "enabled", - "max_threads_hint": 100, - }, - }, - "provenance": None, - "source_sha256": "manual", - } - return proposal, alias, "" - + raise FirstbootFailure('invalid_node_or_alias') + if endpoint.count(':') != 1: + raise FirstbootFailure('invalid_pool_endpoint') + host, port_text = endpoint.rsplit(':', 1) + if not HOST_RE.fullmatch(host) or not port_text.isdigit() or (not 1 <= int(port_text) <= 65535): + raise FirstbootFailure('invalid_pool_endpoint') + if threads != 'auto' and (not threads.isdigit() or not 1 <= int(threads) <= 1024): + raise FirstbootFailure('invalid_cpu_threads') + proposal = {'schema': 'rigos.config-proposal/v1', 'profile': {'node_name': node, 'timezone': timezone, 'flight_source': 'interactive', 'flight_ref': None, 'miner_start_mode': start}, 'flight_sheet': {'schema': 'rigos.flight-sheet/v1', 'name': 'manual-xmrig', 'coin': 'XMR', 'backend': 'xmrig', 'algorithm': algorithm, 'pools': [{'host': host.lower(), 'port': int(port_text), 'tls': tls == 'required', 'priority': 0}], 'identity_ref': alias, 'worker_template': '{node_name}', 'cpu': {'threads': threads if threads == 'auto' else int(threads), 'huge_pages': huge_pages == 'enabled', 'max_threads_hint': 100}}, 'provenance': None, 'source_sha256': 'manual'} + return (proposal, alias, '') def prepare_import(path: str) -> tuple[dict, bool]: while True: - set_stage("prepare_import") - ok, result = engine("prepare", "--output", path) + set_stage('prepare_import') + ok, result = engine('prepare', '--output', path) if ok: - return json.loads(Path(path).read_text(encoding="utf-8")), False - if result.get("code") == "RIGOS_CONFIG_FILE_MISSING": - return manual_proposal()[0], True - if ( - result.get("code") == "RIGOS_FLIGHT_SHEET_MISSING" - and "interactive" in result.get("message", "") - ): - found, choices = engine("discover") + return (json.loads(Path(path).read_text(encoding='utf-8')), False) + if result.get('code') == 'RIGOS_CONFIG_FILE_MISSING': + return (manual_proposal()[0], True) + if result.get('code') == 'RIGOS_FLIGHT_SHEET_MISSING' and 'interactive' in result.get('message', ''): + found, choices = engine('discover') if found: - items = [ - ("manual", "Configure manually"), - ("none", "Do not configure mining now"), - ] - items += [ - (f"native:{name}", f"Native {name}") - for name in choices.get("native", []) - ] - items += [ - (f"import:{name}", f"Import {name}") - for name in choices.get("import", []) - ] + items = [('manual', 'Configure manually'), ('none', 'Do not configure mining now')] + items += [(f'native:{name}', f'Native {name}') for name in choices.get('native', [])] + items += [(f'import:{name}', f'Import {name}') for name in choices.get('import', [])] flat = [value for pair in items for value in pair] - selected = dialog( - "--menu", - "Select Flight Sheet", - "20", - "76", - str(min(12, len(items))), - *flat, - stage="flight_sheet_selection", - ) - if selected == "manual": - return manual_proposal()[0], True - if selected == "none": - raise FirstbootCancelled("mining_left_unconfigured") - source, reference = selected.split(":", 1) - ok, prepared = engine( - "prepare", - "--output", - path, - "--flight-source", - source, - "--flight-ref", - reference, - ) + selected = dialog('--menu', 'Select Flight Sheet', '20', '76', str(min(12, len(items))), *flat, stage='flight_sheet_selection') + if selected == 'manual': + return (manual_proposal()[0], True) + if selected == 'none': + raise FirstbootCancelled('mining_left_unconfigured') + source, reference = selected.split(':', 1) + ok, prepared = engine('prepare', '--output', path, '--flight-source', source, '--flight-ref', reference) if ok: - return json.loads(Path(path).read_text(encoding="utf-8")), False + return (json.loads(Path(path).read_text(encoding='utf-8')), False) result = prepared - action = dialog( - "--menu", - diagnostic_text(result), - "20", - "76", - "4", - "retry", - "Retry import", - "manual", - "Continue with manual setup", - "diagnostics", - "Show diagnostics", - "reboot", - "Reboot", - stage="import_error_action", - ) - if action == "retry": + action = dialog('--menu', diagnostic_text(result), '20', '76', '4', 'retry', 'Retry import', 'manual', 'Continue with manual setup', 'diagnostics', 'Show diagnostics', 'reboot', 'Reboot', stage='import_error_action') + if action == 'retry': continue - if action == "diagnostics": - dialog( - "--msgbox", - diagnostic_text(result), - "18", - "76", - stage="import_diagnostics", - ) + if action == 'diagnostics': + dialog('--msgbox', diagnostic_text(result), '18', '76', stage='import_diagnostics') continue - if action == "reboot": - subprocess.run(["systemctl", "reboot"], check=False) + if action == 'reboot': + subprocess.run(['systemctl', 'reboot'], check=False) raise RebootRequested - if confirm( - "Discard invalid rigos.conf for this boot and continue manually\n\n" - "The source file will not be changed or deleted.", - stage="discard_invalid_import", - ): - return manual_proposal()[0], True - + if confirm('Discard invalid rigos.conf for this boot and continue manually\n\nThe source file will not be changed or deleted.', stage='discard_invalid_import'): + return (manual_proposal()[0], True) def resolve_identity(proposal: dict) -> dict: - requested = proposal["flight_sheet"]["identity_ref"] - identity_dir = STATE / "identities" + requested = proposal['flight_sheet']['identity_ref'] + identity_dir = STATE / 'identities' candidates = [] if identity_dir.is_dir(): - for path in sorted(identity_dir.glob("*.json"))[:64]: + for path in sorted(identity_dir.glob('*.json'))[:64]: try: - record = json.loads(path.read_text(encoding="utf-8")) - if record.get("schema") == "rigos.identity/v1": + record = json.loads(path.read_text(encoding='utf-8')) + if record.get('schema') == 'rigos.identity/v1': candidates.append(record) except (OSError, json.JSONDecodeError): continue - external = (proposal.get("provenance") or {}).get("external_reference") - mapping_path = STATE / "external-identity-map.json" + external = (proposal.get('provenance') or {}).get('external_reference') + mapping_path = STATE / 'external-identity-map.json' if external and mapping_path.is_file(): try: - mapping_document = json.loads(mapping_path.read_text(encoding="utf-8")) - mappings = ( - mapping_document.get("mappings", []) - if mapping_document.get("schema") - == "rigos.external-identity-map/v1" - else [] - ) + mapping_document = json.loads(mapping_path.read_text(encoding='utf-8')) + mappings = mapping_document.get('mappings', []) if mapping_document.get('schema') == 'rigos.external-identity-map/v1' else [] except (OSError, json.JSONDecodeError): mappings = [] - mapping = next( - ( - item - for item in mappings - if item.get("source") == external.get("source") - and item.get("external_type") == external.get("external_type") - and item.get("external_value") == external.get("external_value") - ), - None, - ) - mapped_identity = next( - ( - item - for item in candidates - if mapping and item.get("alias") == mapping.get("identity_ref") - ), - None, - ) + mapping = next((item for item in mappings if item.get('source') == external.get('source') and item.get('external_type') == external.get('external_type') and (item.get('external_value') == external.get('external_value'))), None) + mapped_identity = next((item for item in candidates if mapping and item.get('alias') == mapping.get('identity_ref')), None) if mapped_identity: - source_status = ( - "same source hash" - if mapping.get("confirmed_source_sha256") - == proposal.get("source_sha256") - else "source hash changed" - ) - if confirm( - "Reuse confirmed external identity mapping\n\n" - f"Local alias {mapped_identity['alias']}\n{source_status}\n" - f"Value suffix ****{mapped_identity['value'][-4:]}", - stage="reuse_external_identity", - ): - proposal["flight_sheet"]["identity_ref"] = mapped_identity["alias"] + source_status = 'same source hash' if mapping.get('confirmed_source_sha256') == proposal.get('source_sha256') else 'source hash changed' + set_stage('reuse_external_identity') + if confirm(f"Reuse confirmed external identity mapping\n\nLocal alias {mapped_identity['alias']}\n{source_status}\nValue suffix ****{mapped_identity['value'][-4:]}"): + proposal['flight_sheet']['identity_ref'] = mapped_identity['alias'] return mapped_identity - exact = next((item for item in candidates if item.get("alias") == requested), None) - if exact and confirm( - f"Reuse local identity alias {requested}\n\n" - f"Value suffix {exact['value'][-6:].rjust(len(exact['value'][-6:]) + 4, '*')}", - stage="reuse_local_identity", - ): - proposal["flight_sheet"]["identity_ref"] = exact["alias"] + exact = next((item for item in candidates if item.get('alias') == requested), None) + set_stage('reuse_local_identity') + if exact and confirm(f"Reuse local identity alias {requested}\n\nValue suffix {exact['value'][-6:].rjust(len(exact['value'][-6:]) + 4, '*')}"): + proposal['flight_sheet']['identity_ref'] = exact['alias'] return exact - aliases = [ - ( - f"existing:{item['alias']}", - f"Use {item['alias']} suffix ****{item['value'][-4:]}", - ) - for item in candidates - ] - choices = aliases + [ - ("new", "Add a new local identity"), - ("none", "Leave mining unconfigured"), - ("reboot", "Cancel and reboot"), - ] - selected = dialog( - "--menu", - "Mining identity is unresolved", - "18", - "76", - str(min(12, len(choices))), - *[value for pair in choices for value in pair], - stage="identity_selection", - ) - if selected.startswith("existing:"): - alias = selected.split(":", 1)[1] - selected_identity = next(item for item in candidates if item["alias"] == alias) - if not confirm( - f"Use local identity alias {alias} for this Flight Sheet\n\n" - f"Value suffix ****{selected_identity['value'][-4:]}", - stage="confirm_existing_identity", - ): + aliases = [(f"existing:{item['alias']}", f"Use {item['alias']} suffix ****{item['value'][-4:]}") for item in candidates] + choices = aliases + [('new', 'Add a new local identity'), ('none', 'Leave mining unconfigured'), ('reboot', 'Cancel and reboot')] + set_stage('identity_selection') + selected = dialog('--menu', 'Mining identity is unresolved', '18', '76', str(min(12, len(choices))), *[value for pair in choices for value in pair]) + if selected.startswith('existing:'): + alias = selected.split(':', 1)[1] + selected_identity = next((item for item in candidates if item['alias'] == alias)) + set_stage('confirm_existing_identity') + if not confirm(f"Use local identity alias {alias} for this Flight Sheet\n\nValue suffix ****{selected_identity['value'][-4:]}"): return resolve_identity(proposal) - proposal["flight_sheet"]["identity_ref"] = alias + proposal['flight_sheet']['identity_ref'] = alias return selected_identity - if selected == "none": - subprocess.run(["systemctl", "stop", "rigos-miner.service"], check=False) - raise FirstbootCancelled("mining_left_unconfigured") - if selected == "reboot": - subprocess.run(["systemctl", "reboot"], check=False) + if selected == 'none': + subprocess.run(['systemctl', 'stop', 'rigos-miner.service'], check=False) + raise FirstbootCancelled('mining_left_unconfigured') + if selected == 'reboot': + subprocess.run(['systemctl', 'reboot'], check=False) raise RebootRequested - alias = prompt( - "new_identity_alias", - "Local identity alias", - requested if SLUG_RE.fullmatch(requested) else "main-xmr", - ).strip() - value = prompt( - "new_identity_value", - "Mining identity value", - password=True, - ).strip() - if ( - not SLUG_RE.fullmatch(alias) - or not value - or len(value) > 512 - or any(character.isspace() or ord(character) < 32 for character in value) - ): - raise FirstbootFailure("invalid_identity") - proposal["flight_sheet"]["identity_ref"] = alias - return { - "schema": "rigos.identity/v1", - "alias": alias, - "kind": "mining_identity", - "value": value, - "created_locally": True, - } - + alias = prompt('new_identity_alias', 'Local identity alias', requested if SLUG_RE.fullmatch(requested) else 'main-xmr').strip() + value = prompt('new_identity_value', 'Mining identity value', password=True).strip() + if not SLUG_RE.fullmatch(alias) or not value or len(value) > 512 or any((character.isspace() or ord(character) < 32 for character in value)): + raise FirstbootFailure('invalid_identity') + proposal['flight_sheet']['identity_ref'] = alias + return {'schema': 'rigos.identity/v1', 'alias': alias, 'kind': 'mining_identity', 'value': value, 'created_locally': True} def main() -> None: - set_stage("state_preflight") + set_stage('state_preflight') try: - state_outcome = json.loads(STATE_STATUS.read_text(encoding="utf-8"))["outcome"] + state_outcome = json.loads(STATE_STATUS.read_text(encoding='utf-8'))['outcome'] except (OSError, KeyError, json.JSONDecodeError): - state_outcome = "unavailable" - if state_outcome != "ready": - dialog( - "--msgbox", - f"Persistent state status: {state_outcome}\n\n" - "Configuration and mining are blocked until the verified USB state is ready.", - "13", - "72", - stage="state_not_ready", - ) - raise FirstbootFailure("state_not_ready") - - set_stage("current_revision") - existing, current = engine("current") - if existing and current.get("outcome") == "configuration_committed": - set_stage("activation_retry") - activated, result = engine("activate") + state_outcome = 'unavailable' + if state_outcome != 'ready': + dialog('--msgbox', f'Persistent state status: {state_outcome}\n\nConfiguration and mining are blocked until the verified USB state is ready.', '13', '72', stage='state_not_ready') + raise FirstbootFailure('state_not_ready') + set_stage('current_revision') + existing, current = engine('current') + if existing and current.get('outcome') == 'configuration_committed': + set_stage('activation_retry') + activated, result = engine('activate') if not activated: - dialog( - "--msgbox", - "Configuration activation failed\n" - f"{diagnostic_text(result)}\n\n" - "The committed revision was preserved and mining remains stopped. " - "Reboot to retry activation.", - "18", - "76", - stage="activation_retry_failed", - ) - raise FirstbootFailure("activation_retry_failed") - dialog( - "--msgbox", - "Existing configuration activated.\n" - f"Miner started: {result.get('miner_started', False)}", - "10", - "64", - stage="activation_retry_ready", - ) + dialog('--msgbox', f'Configuration activation failed\n{diagnostic_text(result)}\n\nThe committed revision was preserved and mining remains stopped. Reboot to retry activation.', '18', '76', stage='activation_retry_failed') + raise FirstbootFailure('activation_retry_failed') + dialog('--msgbox', f"Existing configuration activated.\nMiner started: {result.get('miner_started', False)}", '10', '64', stage='activation_retry_ready') return - - with tempfile.NamedTemporaryFile( - prefix="rigos-proposal-", - dir="/run/rigos", - delete=False, - ) as stream: + with tempfile.NamedTemporaryFile(prefix='rigos-proposal-', dir='/run/rigos', delete=False) as stream: proposal_path = stream.name os.unlink(proposal_path) try: proposal, _manual = prepare_import(proposal_path) identity = resolve_identity(proposal) - sheet = proposal["flight_sheet"] - profile = proposal["profile"] - primary = sheet["pools"][0] - summary = ( - f"Node {profile['node_name']}\nFlight Sheet {sheet['name']}\n" - f"Pool {primary['host']}:{primary['port']}\n" - f"Fallback pools {len(sheet['pools']) - 1}\n" - f"Algorithm {sheet['algorithm']}\n" - f"TLS {'enabled' if primary['tls'] else 'disabled'}\n" - f"Identity {identity['alias']} suffix ****{identity['value'][-4:]}\n" - f"Start policy {profile['miner_start_mode']}\n" - f"Huge pages {sheet['cpu']['huge_pages']}" - ) - if not confirm( - summary + "\n\nCommit this configuration locally?", - stage="commit_confirmation", - ): - raise FirstbootCancelled("configuration_not_confirmed") - set_stage("configuration_commit") - ok, result = engine("commit", stdin={"proposal": proposal, "identity": identity}) + sheet = proposal['flight_sheet'] + profile = proposal['profile'] + primary = sheet['pools'][0] + summary = f"Node {profile['node_name']}\nFlight Sheet {sheet['name']}\nPool {primary['host']}:{primary['port']}\nFallback pools {len(sheet['pools']) - 1}\nAlgorithm {sheet['algorithm']}\nTLS {('enabled' if primary['tls'] else 'disabled')}\nIdentity {identity['alias']} suffix ****{identity['value'][-4:]}\nStart policy {profile['miner_start_mode']}\nHuge pages {sheet['cpu']['huge_pages']}" + if not confirm(summary + '\n\nCommit this configuration locally?', stage='commit_confirmation'): + raise FirstbootCancelled('configuration_not_confirmed') + set_stage('configuration_commit') + ok, result = engine('commit', stdin={'proposal': proposal, 'identity': identity}) if not ok: - dialog( - "--msgbox", - "Configuration commit failed\n" - f"{diagnostic_text(result)}\n\nNo activation was attempted.", - "18", - "76", - stage="configuration_commit_failed", - ) - raise FirstbootFailure("configuration_commit_failed") - set_stage("configuration_activation") - activated, activation = engine("activate") + dialog('--msgbox', f'Configuration commit failed\n{diagnostic_text(result)}\n\nNo activation was attempted.', '18', '76', stage='configuration_commit_failed') + raise FirstbootFailure('configuration_commit_failed') + set_stage('configuration_activation') + activated, activation = engine('activate') if not activated: - dialog( - "--msgbox", - "Configuration was committed but activation failed\n" - f"{diagnostic_text(activation)}\n\n" - "The revision was preserved and mining remains stopped. " - "Reboot to retry activation.", - "18", - "76", - stage="configuration_activation_failed", - ) - raise FirstbootFailure("configuration_activation_failed") - dialog( - "--msgbox", - "Configuration saved and activated atomically.\n" - f"Miner started: {activation.get('miner_started', False)}", - "10", - "64", - stage="configuration_ready", - ) + dialog('--msgbox', f'Configuration was committed but activation failed\n{diagnostic_text(activation)}\n\nThe revision was preserved and mining remains stopped. Reboot to retry activation.', '18', '76', stage='configuration_activation_failed') + raise FirstbootFailure('configuration_activation_failed') + dialog('--msgbox', f"Configuration saved and activated atomically.\nMiner started: {activation.get('miner_started', False)}", '10', '64', stage='configuration_ready') finally: try: os.unlink(proposal_path) except FileNotFoundError: pass - def handle_signal(signal_number: int, _frame: object) -> None: name = signal.Signals(signal_number).name - record_event("failed", "process_signal", signal_name=name) + record_event('failed', 'process_signal', signal_name=name) raise FirstbootInterrupted(signal_number) - def run() -> int: signal.signal(signal.SIGHUP, handle_signal) signal.signal(signal.SIGTERM, handle_signal) - set_stage("startup") - record_event("running", "firstboot_started") + set_stage('startup') + record_event('running', 'firstboot_started') try: main() except FirstbootCancelled as error: - record_event("cancelled", error.reason, error.returncode) - print( - f"RIGOS firstboot cancelled stage={CURRENT_STAGE} reason={error.reason}", - file=sys.stderr, - flush=True, - ) + record_event('cancelled', error.reason, error.returncode) + print(f'RIGOS firstboot cancelled stage={CURRENT_STAGE} reason={error.reason}', file=sys.stderr, flush=True) return 10 except FirstbootFailure as error: - record_event( - "failed", - error.reason, - error.returncode, - error.signal_name, - ) - print( - f"RIGOS firstboot failed stage={CURRENT_STAGE} reason={error.reason}", - file=sys.stderr, - flush=True, - ) + record_event('failed', error.reason, error.returncode, error.signal_name) + print(f'RIGOS firstboot failed stage={CURRENT_STAGE} reason={error.reason}', file=sys.stderr, flush=True) return 20 except FirstbootInterrupted as error: return 128 + error.signal_number except RebootRequested: - record_event("cancelled", "reboot_requested") + record_event('cancelled', 'reboot_requested') return 0 except Exception as error: - reason = f"unexpected_{type(error).__name__}" - record_event("failed", reason) - print( - f"RIGOS firstboot failed stage={CURRENT_STAGE} reason={reason}", - file=sys.stderr, - flush=True, - ) + reason = f'unexpected_{type(error).__name__}' + record_event('failed', reason) + print(f'RIGOS firstboot failed stage={CURRENT_STAGE} reason={reason}', file=sys.stderr, flush=True) return 20 - set_stage("complete") - record_event("ready", "firstboot_completed") + set_stage('complete') + record_event('ready', 'firstboot_completed') return 0 - - -if __name__ == "__main__": +if __name__ == '__main__': raise SystemExit(run()) From d01d7cf49cc43382ff22d9118385346c6d581a22 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:47:07 +0700 Subject: [PATCH 023/386] recovery stop hanging up shared tty --- .../etc/systemd/system/rigos-recovery-access.service | 1 - 1 file changed, 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service b/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service index 4674a86a..228e2a24 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service @@ -12,7 +12,6 @@ StandardOutput=tty StandardError=tty TTYPath=/dev/tty1 TTYReset=yes -TTYVHangup=yes [Install] WantedBy=multi-user.target From 16f829cadb8c32116e20ea713261dba8fccb3157 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:47:39 +0700 Subject: [PATCH 024/386] firstboot stop hanging up interactive tty --- .../includes.chroot/etc/systemd/system/rigos-firstboot.service | 1 - 1 file changed, 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service index 060ce596..4a483155 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service @@ -14,7 +14,6 @@ StandardOutput=tty StandardError=tty TTYPath=/dev/tty1 TTYReset=yes -TTYVHangup=yes TTYVTDisallocate=yes [Install] From d3bb5682921f66df07981c38b79726177d901967 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Mon, 6 Jul 2026 18:48:16 +0700 Subject: [PATCH 025/386] test lock tty handoff lifecycle --- crates/rigos-config/tests/firstboot_tty.rs | 26 +++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_tty.rs b/crates/rigos-config/tests/firstboot_tty.rs index d3cab77b..2d946939 100644 --- a/crates/rigos-config/tests/firstboot_tty.rs +++ b/crates/rigos-config/tests/firstboot_tty.rs @@ -1,16 +1,17 @@ use std::fs; use std::path::PathBuf; -fn firstboot_unit() -> String { +fn unit(name: &str) -> String { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service"); + .join("../../build/usb/includes.chroot/etc/systemd/system") + .join(name); fs::read_to_string(&path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) } #[test] fn firstboot_releases_tty1_to_getty_after_exit() { - let unit = firstboot_unit(); + let unit = unit("rigos-firstboot.service"); assert!( unit.lines().any(|line| line == "Before=getty@tty1.service"), @@ -26,4 +27,23 @@ fn firstboot_releases_tty1_to_getty_after_exit() { !unit.lines().any(|line| line == "RemainAfterExit=yes"), "firstboot must release tty1 after the process exits" ); + assert!( + !unit.lines().any(|line| line == "TTYVHangup=yes"), + "firstboot must not hang up the shared local console" + ); +} + +#[test] +fn recovery_access_does_not_hang_up_the_following_firstboot_session() { + let unit = unit("rigos-recovery-access.service"); + + assert!( + unit.lines() + .any(|line| line.contains("Before=rigos-state-ready.service rigos-firstboot.service")), + "recovery access must complete before firstboot" + ); + assert!( + !unit.lines().any(|line| line == "TTYVHangup=yes"), + "recovery access must not hang up tty1 before firstboot starts" + ); } From 4689be4af765c43e3735695fab6f378ac6b86974 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Mon, 6 Jul 2026 19:38:16 +0700 Subject: [PATCH 026/386] test fix firstboot verification semantics --- .../tests/firstboot_observability.rs | 17 +++++---- scripts/verify.sh | 38 +++++++++++++++++-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_observability.rs b/crates/rigos-config/tests/firstboot_observability.rs index 25194f6c..5d29b089 100644 --- a/crates/rigos-config/tests/firstboot_observability.rs +++ b/crates/rigos-config/tests/firstboot_observability.rs @@ -28,7 +28,8 @@ boot_id_path.write_text('boot-test\n', encoding='ascii') os.environ['RIGOS_FIRSTBOOT_UI_STATUS'] = str(status_path) os.environ['RIGOS_FIRSTBOOT_BOOT_ID'] = str(boot_id_path) namespace = runpy.run_path(str(source), run_name='rigos_firstboot_observability_test') -namespace['journal'] = lambda _value, _priority: None +globals_ = namespace['run'].__globals__ +globals_['journal'] = lambda _value, _priority: None class Result: def __init__(self, returncode, stderr=''): @@ -41,11 +42,11 @@ def status(): def run_dialog(returncode, stderr=''): - namespace['subprocess'].run = lambda *_args, **_kwargs: Result(returncode, stderr) - namespace['main'] = lambda: namespace['dialog']( + globals_['subprocess'].run = lambda *_args, **_kwargs: Result(returncode, stderr) + globals_['main'] = lambda: globals_['dialog']( '--menu', 'synthetic', '10', '60', '1', 'item', 'value', stage='synthetic_dialog' ) - return namespace['run']() + return globals_['run']() assert run_dialog(1) == 10 value = status() @@ -70,14 +71,14 @@ assert value['outcome'] == 'failed' assert value['reason'] == 'dialog_signal' assert value['signal'] == 'SIGHUP' -namespace['subprocess'].run = lambda *_args, **_kwargs: Result(1) -assert namespace['confirm']('synthetic confirmation', stage='synthetic_confirmation') is False +globals_['subprocess'].run = lambda *_args, **_kwargs: Result(1) +assert globals_['confirm']('synthetic confirmation', stage='synthetic_confirmation') is False value = status() assert value['outcome'] == 'declined' assert value['reason'] == 'user_selected_no' -namespace['main'] = lambda: None -assert namespace['run']() == 0 +globals_['main'] = lambda: None +assert globals_['run']() == 0 value = status() assert value['outcome'] == 'ready' assert value['reason'] == 'firstboot_completed' diff --git a/scripts/verify.sh b/scripts/verify.sh index bf6613e4..595b63b4 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -36,9 +36,40 @@ if rg -q 'rollback|pending-transaction' crates/rigos-config/src/main.rs; then echo "configuration activation still contains pointer rollback machinery" >&2 exit 1 fi -grep -Fq 'engine("commit"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot -grep -Fq 'engine("activate"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot -grep -Fq 'engine("current"' build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +firstboot=build/usb/includes.chroot/usr/local/sbin/rigos-firstboot + +python3 - "$firstboot" <<'PY' +import ast +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + +commands = set() + +for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if not isinstance(node.func, ast.Name): + continue + if node.func.id != "engine": + continue + if not node.args: + continue + + first = node.args[0] + if isinstance(first, ast.Constant) and isinstance(first.value, str): + commands.add(first.value) + +required = {"commit", "activate", "current"} +missing = sorted(required - commands) + +if missing: + raise SystemExit( + "firstboot is missing required engine calls: " + ", ".join(missing) + ) +PY grep -Fq 'ExecStart=/usr/local/sbin/rigos-state-orchestrate' build/usb/includes.chroot/etc/systemd/system/rigos-state.service grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service if grep -Fq 'Wants=rigos-recovery-access.service' build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service; then @@ -57,7 +88,6 @@ if rg -n '(HIVE_HOST_URL|API_HOST_URLS|RIG_PASSWD|HSSH_SRV)=' configs docs/local exit 1 fi -firstboot=build/usb/includes.chroot/usr/local/sbin/rigos-firstboot if rg -q -- '--output-fd' "$firstboot"; then echo "first boot redirects whiptail result onto the screen stream" >&2 exit 1 From 9e9258a0d55395d9b20658caf6373a0b8966c436 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Tue, 7 Jul 2026 01:44:34 +0700 Subject: [PATCH 027/386] runtime break state readiness cycle --- .../systemd/system/rigos-state-ready.service | 5 +- crates/rigos-config/tests/firstboot_tty.rs | 27 ++++ scripts/verify-systemd-ordering.py | 133 ++++++++++++++++++ scripts/verify-usb-appliance.sh | 2 + scripts/verify.sh | 30 +++- 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 scripts/verify-systemd-ordering.py diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service index eb8bbfa8..5fbc72e0 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service @@ -1,9 +1,8 @@ [Unit] Description=Verify RIGOS persistent state readiness -DefaultDependencies=no After=rigos-state.service rigos-recovery-access.service Requires=rigos-state.service -Before=local-fs.target rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service +Before=rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] Type=oneshot @@ -11,4 +10,4 @@ ExecStart=/usr/lib/rigos/rigos-state-ready RemainAfterExit=yes [Install] -WantedBy=local-fs.target +WantedBy=multi-user.target diff --git a/crates/rigos-config/tests/firstboot_tty.rs b/crates/rigos-config/tests/firstboot_tty.rs index 2d946939..3fe2e886 100644 --- a/crates/rigos-config/tests/firstboot_tty.rs +++ b/crates/rigos-config/tests/firstboot_tty.rs @@ -47,3 +47,30 @@ fn recovery_access_does_not_hang_up_the_following_firstboot_session() { "recovery access must not hang up tty1 before firstboot starts" ); } + +#[test] +fn state_ready_stays_out_of_the_local_fs_transaction() { + let unit = unit("rigos-state-ready.service"); + + assert!(!unit.lines().any(|line| line == "DefaultDependencies=no")); + assert!( + !unit + .lines() + .any(|line| line.contains("Before=local-fs.target")) + ); + assert!( + unit.lines() + .any(|line| line == "WantedBy=multi-user.target") + ); + assert!( + unit.lines() + .any(|line| line == "After=rigos-state.service rigos-recovery-access.service") + ); + assert!( + unit.lines() + .any(|line| line == "Requires=rigos-state.service") + ); + assert!(unit.lines().any(|line| { + line == "Before=rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service" + })); +} diff --git a/scripts/verify-systemd-ordering.py b/scripts/verify-systemd-ordering.py new file mode 100644 index 00000000..a5c69eac --- /dev/null +++ b/scripts/verify-systemd-ordering.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +import argparse +import sys +from collections import defaultdict +from pathlib import Path + + +class Unit: + def __init__(self, path: Path): + self.path = path + self.values = defaultdict(list) + section = "" + for raw in path.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith(("#", ";")): + continue + if line.startswith("[") and line.endswith("]"): + section = line[1:-1] + elif section and "=" in line: + key, value = line.split("=", 1) + self.values[(section, key)].extend(value.split()) + else: + raise ValueError(f"{path}: malformed line {line!r}") + + def words(self, section, key): + return set(self.values[(section, key)]) + + def scalar(self, section, key): + values = self.values[(section, key)] + return values[-1] if values else None + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def includes(actual, expected, message): + missing = sorted(expected - actual) + require(not missing, f"{message}: missing {', '.join(missing)}") + + +def verify(units): + names = { + "rigos-state.service", "rigos-recovery-access.service", + "rigos-state-ready.service", "rigos-profile-apply.service", + "rigos-firstboot.service", "rigos-hugepages.service", "rigos-miner.service", + } + includes(set(units), names, "RIGOS unit set is incomplete") + state = units["rigos-state.service"] + require(state.scalar("Unit", "DefaultDependencies") == "no", "state must retain early boot dependencies") + includes(state.words("Unit", "Before"), {"local-fs.target", "rigos-state-ready.service"}, "state ordering is incomplete") + + recovery = units["rigos-recovery-access.service"] + includes(recovery.words("Unit", "After"), {"rigos-state.service"}, "recovery must follow state") + includes(recovery.words("Unit", "Before"), {"rigos-state-ready.service", "rigos-firstboot.service"}, "recovery ordering is incomplete") + require(recovery.scalar("Service", "TTYVHangup") != "yes", "recovery must not hang up tty1") + + ready = units["rigos-state-ready.service"] + require(ready.scalar("Unit", "DefaultDependencies") != "no", "state-ready must use normal dependencies") + includes(ready.words("Unit", "After"), {"rigos-state.service", "rigos-recovery-access.service"}, "state-ready ordering is incomplete") + includes(ready.words("Unit", "Requires"), {"rigos-state.service"}, "state-ready must require state") + includes(ready.words("Unit", "Before"), {"rigos-profile-apply.service", "rigos-firstboot.service", "rigos-hugepages.service", "rigos-miner.service"}, "state-ready downstream ordering is incomplete") + require("local-fs.target" not in ready.words("Unit", "Before"), "state-ready must not order before local-fs") + require("local-fs.target" not in ready.words("Install", "WantedBy"), "state-ready must not be installed under local-fs") + includes(ready.words("Install", "WantedBy"), {"multi-user.target"}, "state-ready must be installed under multi-user") + + firstboot = units["rigos-firstboot.service"] + includes(firstboot.words("Unit", "Requires"), {"rigos-state-ready.service"}, "firstboot must require state-ready") + require(firstboot.scalar("Service", "TTYVHangup") != "yes", "firstboot must not hang up tty1") + + +def graph_for(units): + graph = defaultdict(set) + graph["local-fs.target"].add("sysinit.target") + graph["sysinit.target"].add("basic.target") + graph["basic.target"].add("multi-user.target") + for name, unit in units.items(): + if unit.scalar("Unit", "DefaultDependencies") != "no": + graph["basic.target"].add(name) + for dependency in unit.words("Unit", "After"): + graph[dependency].add(name) + for target in unit.words("Unit", "Before"): + graph[name].add(target) + for target in unit.words("Install", "WantedBy"): + graph[name].add(target) + graph[name] + return graph + + +def cycle_in(graph): + state, stack, positions = {}, [], {} + def visit(node): + state[node] = 1 + positions[node] = len(stack) + stack.append(node) + for target in sorted(graph[node]): + if state.get(target, 0) == 0: + cycle = visit(target) + if cycle: + return cycle + elif state[target] == 1: + return stack[positions[target]:] + [target] + stack.pop() + positions.pop(node) + state[node] = 2 + return None + for node in sorted(graph): + if state.get(node, 0) == 0: + cycle = visit(node) + if cycle: + return cycle + return None + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("unit_dir", nargs="?", default="build/usb/includes.chroot/etc/systemd/system") + directory = Path(parser.parse_args().unit_dir) + try: + units = {path.name: Unit(path) for path in directory.glob("rigos-*.service")} + verify(units) + cycle = cycle_in(graph_for(units)) + require(cycle is None, "systemd ordering cycle: " + " -> ".join(cycle or [])) + except (OSError, ValueError) as error: + print(f"verify-systemd-ordering: {error}", file=sys.stderr) + return 1 + print("RIGOS systemd ordering verification passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 748c5403..b0bbb2ca 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -1,6 +1,7 @@ #!/bin/bash set -euo pipefail +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" die(){ printf 'verify-usb-appliance: %s\n' "$*" >&2; exit 1; } [[ $# -eq 2 ]] || die 'usage: verify-usb-appliance.sh ' image="$(readlink -f "$1")"; manifest="$(readlink -f "$2")" @@ -92,6 +93,7 @@ python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-state-orchestrate" python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" +python3 "$script_dir/verify-systemd-ordering.py" "$temporary/root/etc/systemd/system" rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" [[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' grep -Fq 'systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf' "$temporary/root/etc/systemd/system/rigos-state.service" || die 'state runtime tmpfiles setup is missing' diff --git a/scripts/verify.sh b/scripts/verify.sh index 595b63b4..dcd37301 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -16,7 +16,35 @@ python3 -m py_compile \ build/usb/includes.chroot/usr/local/sbin/rigos-firstboot \ build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access \ build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate \ - build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate + build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \ + scripts/verify-systemd-ordering.py +python3 scripts/verify-systemd-ordering.py +python3 - <<'PY' +import shutil +import subprocess +import tempfile +from pathlib import Path + +source = Path("build/usb/includes.chroot/etc/systemd/system") +with tempfile.TemporaryDirectory() as temporary: + target = Path(temporary) + for unit in source.glob("rigos-*.service"): + shutil.copy2(unit, target / unit.name) + ready = target / "rigos-state-ready.service" + text = ready.read_text(encoding="utf-8") + text = text.replace("[Unit]\n", "[Unit]\nDefaultDependencies=no\n", 1) + text = text.replace("Before=rigos-profile", "Before=local-fs.target rigos-profile", 1) + text = text.replace("WantedBy=multi-user.target", "WantedBy=local-fs.target") + ready.write_text(text, encoding="utf-8") + result = subprocess.run( + ["python3", "scripts/verify-systemd-ordering.py", str(target)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode == 0: + raise SystemExit("ordering verifier accepted the historical local-fs cycle") +PY grep -Fq 'RIGOS_CONFIG_DUPLICATE_KEY' crates/rigos-config/src/lib.rs grep -Fq 'RIGOS_CONFIG_BOOT_DEVICE_UNPROVEN' crates/rigos-config/src/main.rs From bac3d10def8dc1071d01ea4ebbae8dec6c6ffd61 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Tue, 7 Jul 2026 01:45:28 +0700 Subject: [PATCH 028/386] recovery persist local administrator credential --- .../usr/local/sbin/rigos-recovery-access | 143 ++++++++++++++++-- crates/rigos-config/tests/recovery_access.rs | 121 +++++++++++++++ scripts/verify-usb-appliance.sh | 2 + scripts/verify.sh | 5 + 4 files changed, 262 insertions(+), 9 deletions(-) create mode 100644 crates/rigos-config/tests/recovery_access.rs diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access index 3b55bd66..92a48545 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access @@ -1,12 +1,19 @@ #!/usr/bin/python3 import json import os +import re +import stat import subprocess import tempfile from pathlib import Path RUNTIME = Path("/run/rigos") STATE = Path("/var/lib/rigos") +CREDENTIAL_DIRECTORY = STATE / "recovery" +CREDENTIAL_FILE = CREDENTIAL_DIRECTORY / "rigosadmin-password.hash" +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +MAX_HASH_BYTES = 512 +MODULAR_CRYPT = re.compile(r"^\$[A-Za-z0-9][A-Za-z0-9./,_=-]*(?:\$[A-Za-z0-9./,_=-]+){2,}$") def password_ready() -> bool: @@ -21,6 +28,108 @@ def password_ready() -> bool: return result.returncode == 0 and len(fields) >= 2 and fields[1] == "P" +def current_password_hash() -> str | None: + result = subprocess.run( + ["/usr/bin/getent", "shadow", "rigosadmin"], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + if result.returncode != 0: + return None + fields = result.stdout.rstrip("\n").split(":") + if len(fields) < 2 or not valid_password_hash(fields[1]): + return None + return fields[1] + + +def valid_password_hash(value: str) -> bool: + if not value or len(value.encode("utf-8")) > MAX_HASH_BYTES: + return False + if value.startswith(("!", "*")) or any(character in value for character in ":\r\n"): + return False + return MODULAR_CRYPT.fullmatch(value) is not None + + +def ensure_credential_directory() -> None: + CREDENTIAL_DIRECTORY.mkdir(mode=0o700, parents=True, exist_ok=True) + info = CREDENTIAL_DIRECTORY.lstat() + if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode): + raise RuntimeError("unsafe credential directory") + if info.st_uid != os.geteuid(): + raise RuntimeError("credential directory owner mismatch") + CREDENTIAL_DIRECTORY.chmod(0o700) + + +def load_persistent_hash() -> tuple[str | None, bool]: + try: + info = CREDENTIAL_FILE.lstat() + except FileNotFoundError: + return None, False + if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode): + return None, True + if info.st_uid != os.geteuid() or stat.S_IMODE(info.st_mode) != 0o600: + return None, True + try: + value = CREDENTIAL_FILE.read_text(encoding="ascii").rstrip("\n") + except (OSError, UnicodeError): + return None, True + return (value, False) if valid_password_hash(value) else (None, True) + + +def persist_password_hash(value: str) -> None: + if not valid_password_hash(value): + raise RuntimeError("refusing invalid credential hash") + ensure_credential_directory() + descriptor, name = tempfile.mkstemp(prefix=".rigosadmin-password-", dir=CREDENTIAL_DIRECTORY) + temporary = Path(name) + try: + os.fchmod(descriptor, 0o600) + with os.fdopen(descriptor, "w", encoding="ascii", closefd=True) as stream: + stream.write(value) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, CREDENTIAL_FILE) + CREDENTIAL_FILE.chmod(0o600) + directory = os.open(CREDENTIAL_DIRECTORY, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + temporary.unlink(missing_ok=True) + + +def restore_password_hash(value: str) -> bool: + if not valid_password_hash(value): + return False + result = subprocess.run( + ["/usr/sbin/chpasswd", "--encrypted"], + input=f"rigosadmin:{value}\n", + text=True, + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return result.returncode == 0 and password_ready() + + +def prompt_for_password(invalid_stored_hash: bool) -> None: + message = ( + "The saved local credential is invalid and will be replaced. Set the local rig " + "administrator password next." + if invalid_stored_hash + else "Set the local rig administrator password next. This enables tty1 diagnostics only." + ) + subprocess.run( + ["/usr/bin/whiptail", "--title", "RIGOS RECOVERY ACCESS", "--msgbox", message, "11", "72"], + check=False, + ) + subprocess.run(["/usr/bin/passwd", "rigosadmin"], check=True) + + def unit_active(name: str) -> bool: return subprocess.run( ["/usr/bin/systemctl", "is-active", "--quiet", name], check=False @@ -53,14 +162,28 @@ def write_status(value: dict) -> None: def main() -> int: - if not password_ready(): - subprocess.run( - ["/usr/bin/whiptail", "--title", "RIGOS RECOVERY ACCESS", "--msgbox", - "Set the local rig administrator password next. This enables tty1 diagnostics only.", - "11", "72"], - check=False, - ) - subprocess.run(["/usr/bin/passwd", "rigosadmin"], check=True) + ensure_credential_directory() + stored_hash, stored_hash_invalid = load_persistent_hash() + credential_action = "existing" + + if password_ready(): + live_hash = current_password_hash() + if live_hash is None: + raise RuntimeError("usable local credential hash is unavailable") + if stored_hash != live_hash: + persist_password_hash(live_hash) + elif stored_hash is not None and restore_password_hash(stored_hash): + credential_action = "restored" + else: + prompt_for_password(stored_hash_invalid) + if not password_ready(): + raise RuntimeError("local credential setup did not produce a usable password") + live_hash = current_password_hash() + if live_hash is None: + raise RuntimeError("new local credential hash is unavailable") + persist_password_hash(live_hash) + credential_action = "created" + local_access = password_ready() try: state_outcome = json.loads((RUNTIME / "state-status.json").read_text(encoding="utf-8")).get("outcome", "unavailable") @@ -73,12 +196,14 @@ def main() -> int: mode = "operational" if state_outcome == "ready" and remote_inactive else "recovery" write_status({ "schema": "rigos.recovery-access-status/v1", - "boot_id": Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii").strip(), + "boot_id": BOOT_ID.read_text(encoding="ascii").strip(), "mode": mode, "local_console_access": local_access, "state_outcome": state_outcome, "remote_access": "inactive" if remote_inactive else "unexpected_active", "configuration_commit": "present" if configured else "absent", + "credential_action": credential_action, + "credential_persisted": CREDENTIAL_FILE.is_file(), }) return 0 if local_access and remote_inactive else 1 diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs new file mode 100644 index 00000000..ea30a417 --- /dev/null +++ b/crates/rigos-config/tests/recovery_access.rs @@ -0,0 +1,121 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use uuid::Uuid; + +fn recovery_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access") +} + +#[test] +fn recovery_password_is_persisted_restored_and_redacted() { + let root = std::env::temp_dir().join(format!("rigos-recovery-access-{}", Uuid::new_v4())); + fs::create_dir_all(&root).expect("create recovery test directory"); + let program = r#" +import json +import os +import runpy +import stat +import sys +from pathlib import Path + +source = Path(sys.argv[1]) +root = Path(sys.argv[2]) +namespace = runpy.run_path(str(source), run_name='rigos_recovery_access_test') +g = namespace['main'].__globals__ +g['RUNTIME'] = root / 'run' +g['STATE'] = root / 'state' +g['CREDENTIAL_DIRECTORY'] = root / 'state' / 'recovery' +g['CREDENTIAL_FILE'] = g['CREDENTIAL_DIRECTORY'] / 'rigosadmin-password.hash' +g['BOOT_ID'] = root / 'boot-id' +g['BOOT_ID'].write_text('boot-test\n', encoding='ascii') +g['RUNTIME'].mkdir() +g['STATE'].mkdir() +(g['RUNTIME'] / 'state-status.json').write_text('{"outcome":"ready"}', encoding='utf-8') +valid_hash = '$y$j9T$syntheticSalt$syntheticHashValue' + +assert namespace['valid_password_hash'](valid_hash) +for invalid in ('', '!', '*', '!locked', 'bad:hash', 'bad\nhash', '$6$missing'): + assert not namespace['valid_password_hash'](invalid) + +# Fresh setup prompts once, then persists the hash atomically with strict modes. +live_ready = {'value': False} +prompts = [] +persisted = [] +g['password_ready'] = lambda: live_ready['value'] +def prompt(_invalid): + prompts.append(True) + live_ready['value'] = True +g['prompt_for_password'] = prompt +g['current_password_hash'] = lambda: valid_hash if live_ready['value'] else None +real_persist = g['persist_password_hash'] +def persist(value): + persisted.append(value) + real_persist(value) +g['persist_password_hash'] = persist +g['unit_active'] = lambda _name: False +g['unit_enabled'] = lambda _name: False +assert namespace['main']() == 0 +assert len(prompts) == 1 and persisted == [valid_hash] +assert stat.S_IMODE(g['CREDENTIAL_DIRECTORY'].stat().st_mode) == 0o700 +assert stat.S_IMODE(g['CREDENTIAL_FILE'].stat().st_mode) == 0o600 +status = json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text()) +assert status['credential_action'] == 'created' +assert status['credential_persisted'] is True +assert valid_hash not in json.dumps(status) + +# Reboot simulation restores through chpasswd without setup UI or passwd. +live_ready['value'] = False +prompts.clear() +calls = [] +class Result: + def __init__(self, returncode=0): self.returncode = returncode +def fake_run(argv, **kwargs): + calls.append((argv, kwargs)) + if argv == ['/usr/sbin/chpasswd', '--encrypted']: + assert kwargs['input'] == 'rigosadmin:' + valid_hash + '\n' + assert valid_hash not in argv + live_ready['value'] = True + return Result() +g['subprocess'].run = fake_run +g['persist_password_hash'] = real_persist +assert namespace['main']() == 0 +assert not prompts +assert not any('/usr/bin/passwd' in argv for argv, _kwargs in calls) +status = json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text()) +assert status['credential_action'] == 'restored' +assert valid_hash not in json.dumps(status) + +# Existing live credential migrates without a prompt. +g['CREDENTIAL_FILE'].unlink() +live_ready['value'] = True +prompts.clear() +assert namespace['main']() == 0 +assert not prompts and g['CREDENTIAL_FILE'].read_text().strip() == valid_hash + +# An invalid store is never sent to chpasswd and enters explicit replacement setup. +g['CREDENTIAL_FILE'].write_text('!unsafe\n', encoding='ascii') +g['CREDENTIAL_FILE'].chmod(0o600) +live_ready['value'] = False +invalid_flags = [] +def replacement(invalid): + invalid_flags.append(invalid) + live_ready['value'] = True +g['prompt_for_password'] = replacement +calls.clear() +assert namespace['main']() == 0 +assert invalid_flags == [True] +assert not any(argv == ['/usr/sbin/chpasswd', '--encrypted'] for argv, _kwargs in calls) +assert valid_hash not in json.dumps(json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text())) +"#; + let result = Command::new("python3") + .arg("-c") + .arg(program) + .arg(recovery_path()) + .arg(&root) + .status() + .expect("run recovery access fixture"); + let _ = fs::remove_dir_all(&root); + assert!(result.success(), "recovery access fixture failed"); +} diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index b0bbb2ca..812f8ed5 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -123,6 +123,8 @@ grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$temporary/root/etc/sy [[ -f "$temporary/root/etc/systemd/system/rigos-profile-apply.service" ]] || die 'profile apply service is missing' grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config needs-activation' "$temporary/root/etc/systemd/system/rigos-firstboot.service" || die 'first boot activation gate is missing' grep -Fq 'ExecStart=/usr/local/sbin/rigos-recovery-access' "$temporary/root/etc/systemd/system/rigos-recovery-access.service" || die 'local recovery access phase is missing' +grep -Fq 'CREDENTIAL_DIRECTORY = STATE / "recovery"' "$temporary/root/usr/local/sbin/rigos-recovery-access" || die 'persistent recovery credential store is missing' +grep -Fq '["/usr/sbin/chpasswd", "--encrypted"]' "$temporary/root/usr/local/sbin/rigos-recovery-access" || die 'encrypted credential restore is missing' [[ -x "$temporary/root/usr/lib/rigos/rigos-lifecycle-cycles" ]] || die 'booted lifecycle cycle test is missing' if rg -q -- '--output-fd' "$temporary/root/usr/local/sbin/rigos-firstboot"; then die 'first boot rewires the whiptail screen stream'; fi grep -Fq 'stderr=subprocess.PIPE' "$temporary/root/usr/local/sbin/rigos-firstboot" || die 'first boot stderr capture is missing' diff --git a/scripts/verify.sh b/scripts/verify.sh index dcd37301..5fae12cc 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -174,6 +174,11 @@ for forbidden in ("rigos-config", "rigos-miner.service", "systemctl\", \"start", for required in ("passwd", "local_console_access", "remote_access", "configuration_commit"): if required not in recovery: raise SystemExit(f"recovery access contract is missing {required}") +for required in ("chpasswd", "--encrypted", "credential_action", "credential_persisted", "os.replace"): + if required not in recovery: + raise SystemExit(f"persistent recovery credential contract is missing {required}") +if "input=f\"rigosadmin:{value}\\n\"" not in recovery: + raise SystemExit("encrypted credential is not passed to chpasswd over stdin") PY python3 - "$firstboot" <<'PY' From 63ba10dbdeeebc2ce267324eb8f2189ca425fb58 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:11:41 +0700 Subject: [PATCH 029/386] identity seed resolve from verified usb --- .../usr/lib/rigos/rigos-identity-seed | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed b/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed new file mode 100644 index 00000000..0a53d5de --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed @@ -0,0 +1,168 @@ +#!/usr/bin/python3 +import argparse +import json +import os +import re +import stat +import subprocess +import sys +import tempfile +from pathlib import Path + +RUNTIME = Path('/run/rigos') +STATE_STATUS = RUNTIME / 'state-status.json' +ATTESTATION = RUNTIME / 'boot-device.json' +STATE_INIT = '/usr/lib/rigos/rigos-state-init' +SLUG_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9-]{0,63}$') +MAX_SEED_BYTES = 4096 +MAX_VALUE_BYTES = 512 + + +def fail(code: str, message: str, rc: int = 2) -> int: + print(json.dumps({'code': code, 'message': message}, sort_keys=True), file=sys.stderr) + return rc + + +def read_json(path: Path): + with path.open('r', encoding='utf-8') as stream: + return json.load(stream) + + +def ready_state() -> bool: + try: + status = read_json(STATE_STATUS) + except (OSError, json.JSONDecodeError): + return False + return status.get('outcome') in {'ready', 'grown'} + + +def fresh_attestation() -> dict: + temporary = Path(tempfile.mkdtemp(prefix='identity-seed-attest-', dir=RUNTIME)) + try: + attestation_path = temporary / 'boot-device.json' + status_path = temporary / 'state-status.json' + result = subprocess.run( + [STATE_INIT, '--dry-run', '--attestation-only', '--attestation', str(attestation_path), '--status', str(status_path)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if result.returncode != 0: + raise RuntimeError('attestation refresh failed') + return read_json(attestation_path) + finally: + for child in temporary.iterdir() if temporary.exists() else []: + child.unlink(missing_ok=True) + temporary.rmdir() if temporary.exists() else None + + +def identity_matches(expected: dict, fresh: dict) -> bool: + return ( + expected.get('schema') == 'rigos.boot-device/v1' + and fresh.get('schema') == expected.get('schema') + and expected.get('verification_outcome') == 'verified' + and fresh.get('verification_outcome') == 'verified' + and fresh.get('boot_id') == expected.get('boot_id') + and fresh.get('disk', {}).get('major_minor') == expected.get('disk', {}).get('major_minor') + and fresh.get('disk', {}).get('ptuuid', '').lower() == expected.get('disk', {}).get('ptuuid', '').lower() + and fresh.get('partition1', {}).get('major_minor') == expected.get('partition1', {}).get('major_minor') + and fresh.get('partition1', {}).get('partuuid', '').lower() == expected.get('partition1', {}).get('partuuid', '').lower() + and fresh.get('root', {}).get('major_minor') == expected.get('root', {}).get('major_minor') + ) + + +def safe_seed_path(root: Path, alias: str) -> Path: + if not SLUG_RE.fullmatch(alias): + raise ValueError('invalid identity alias') + return root / 'rigos' / 'identities' / f'{alias}.json' + + +def read_seed(path: Path, alias: str) -> dict | None: + try: + info = path.lstat() + except FileNotFoundError: + return None + if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_size > MAX_SEED_BYTES: + raise ValueError('identity seed is not a bounded regular file') + data = path.read_bytes() + if len(data) > MAX_SEED_BYTES: + raise ValueError('identity seed exceeds size limit') + seed = json.loads(data.decode('utf-8')) + allowed = {'schema', 'alias', 'kind', 'value'} + if set(seed) != allowed: + raise ValueError('identity seed contains unknown or missing fields') + if seed.get('schema') != 'rigos.identity-seed/v1': + raise ValueError('unsupported identity seed schema') + if seed.get('alias') != alias or not SLUG_RE.fullmatch(seed.get('alias', '')): + raise ValueError('identity seed alias mismatch') + if seed.get('kind') != 'mining_identity': + raise ValueError('unsupported identity seed kind') + value = seed.get('value') + if not isinstance(value, str) or not value or len(value.encode('utf-8')) > MAX_VALUE_BYTES: + raise ValueError('identity seed value is empty or too large') + if any(character.isspace() or ord(character) < 32 for character in value): + raise ValueError('identity seed value contains whitespace or control characters') + return { + 'schema': 'rigos.identity/v1', + 'alias': alias, + 'kind': 'mining_identity', + 'value': value, + 'created_locally': True, + 'seeded_from_usb': True, + } + + +def mount_and_read(alias: str) -> dict | None: + expected = read_json(ATTESTATION) + fresh = fresh_attestation() + if not identity_matches(expected, fresh): + raise RuntimeError('boot device identity changed') + mountpoint = Path(tempfile.mkdtemp(prefix='identity-seed-efi-', dir=RUNTIME)) + mounted = False + try: + partition = fresh['partition1']['path'] + result = subprocess.run( + ['mount', '-o', 'ro,nodev,nosuid,noexec', partition, str(mountpoint)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if result.returncode != 0: + raise RuntimeError('verified EFI partition could not be mounted') + mounted = True + identity = subprocess.run( + ['findmnt', '--noheadings', '--output', 'MAJ:MIN', '--target', str(mountpoint)], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + if identity.returncode != 0 or identity.stdout.strip() != fresh['partition1']['major_minor']: + raise RuntimeError('mounted EFI identity changed') + return read_seed(safe_seed_path(mountpoint, alias), alias) + finally: + if mounted: + subprocess.run(['umount', str(mountpoint)], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + mountpoint.rmdir() if mountpoint.exists() else None + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument('--alias', required=True) + args = parser.parse_args() + if not ready_state(): + return fail('RIGOS_CONFIG_BOOT_DEVICE_UNPROVEN', 'state is not ready') + try: + seed = mount_and_read(args.alias) + except FileNotFoundError: + return 1 + except (OSError, KeyError, json.JSONDecodeError, UnicodeDecodeError, ValueError, RuntimeError) as error: + return fail('RIGOS_IDENTITY_SEED_INVALID', str(error)) + if seed is None: + return 1 + print(json.dumps(seed, sort_keys=True)) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From 674ef15ac94c89b3699b532d10bc921039f4520e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:12:53 +0700 Subject: [PATCH 030/386] firstboot accept verified identity seed --- .../usr/local/sbin/rigos-firstboot | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot index d64d7681..ce2c6cb7 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot @@ -10,12 +10,13 @@ from pathlib import Path os.environ.setdefault('TERM', 'linux') STATE = Path('/var/lib/rigos') ENGINE = '/usr/lib/rigos/rigos-config' +IDENTITY_SEED = '/usr/lib/rigos/rigos-identity-seed' STATE_STATUS = Path('/run/rigos/state-status.json') UI_STATUS = Path(os.environ.get('RIGOS_FIRSTBOOT_UI_STATUS', '/var/lib/rigos/firstboot-ui-status.json')) BOOT_ID_PATH = Path(os.environ.get('RIGOS_FIRSTBOOT_BOOT_ID', '/proc/sys/kernel/random/boot_id')) WHIPTAIL = os.environ.get('RIGOS_WHIPTAIL', '/usr/bin/whiptail') SYSTEMD_CAT = os.environ.get('RIGOS_SYSTEMD_CAT', '/usr/bin/systemd-cat') -HOST_RE = re.compile('^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$|^[0-9a-fA-F:]+$') +HOST_RE = re.compile('^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$|^[0-9a-fA-F:]+$') SLUG_RE = re.compile('^[A-Za-z0-9][A-Za-z0-9-]{0,63}$') CURRENT_STAGE = 'startup' CURRENT_DIALOG = None @@ -170,6 +171,31 @@ def diagnostic_text(error: dict) -> str: parts.append(error.get('message', 'Invalid configuration')) return '\n'.join(parts) +def load_identity_seed(alias: str) -> dict | None: + if not SLUG_RE.fullmatch(alias): + return None + try: + result = subprocess.run([IDENTITY_SEED, '--alias', alias], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError: + return None + if result.returncode == 1: + return None + if result.returncode != 0: + try: + error = json.loads((result.stderr or '').strip().splitlines()[-1]) + except (json.JSONDecodeError, IndexError): + error = {'message': 'identity seed resolver failed'} + dialog('--msgbox', f"Identity seed rejected\n\nAlias {alias}\n{error.get('message', 'invalid identity seed')}", '14', '72', stage='identity_seed_invalid') + raise FirstbootFailure('identity_seed_invalid', result.returncode) + try: + identity = json.loads(result.stdout.strip().splitlines()[-1]) + except (json.JSONDecodeError, IndexError): + raise FirstbootFailure('identity_seed_invalid', result.returncode) + value = identity.get('value') + if identity.get('schema') != 'rigos.identity/v1' or identity.get('alias') != alias or identity.get('kind') != 'mining_identity' or not isinstance(value, str) or not value or len(value) > 512 or any((character.isspace() or ord(character) < 32 for character in value)): + raise FirstbootFailure('identity_seed_invalid', result.returncode) + return identity + def manual_proposal() -> tuple[dict, str, str]: node = prompt('node_name', 'Node name', 'rig01').strip() timezone = prompt('timezone', 'IANA timezone', 'UTC').strip() @@ -264,6 +290,13 @@ def resolve_identity(proposal: dict) -> dict: if exact and confirm(f"Reuse local identity alias {requested}\n\nValue suffix {exact['value'][-6:].rjust(len(exact['value'][-6:]) + 4, '*')}"): proposal['flight_sheet']['identity_ref'] = exact['alias'] return exact + seeded_identity = load_identity_seed(requested) + if seeded_identity: + set_stage('identity_seed_confirmation') + if confirm(f"Use USB identity seed\n\nAlias {seeded_identity['alias']}\nValue suffix ****{seeded_identity['value'][-4:]}\n\nThe full value will be copied once into local persistent state and will not be written back to the USB."): + proposal['flight_sheet']['identity_ref'] = seeded_identity['alias'] + return seeded_identity + raise FirstbootCancelled('identity_seed_not_confirmed') aliases = [(f"existing:{item['alias']}", f"Use {item['alias']} suffix ****{item['value'][-4:]}") for item in candidates] choices = aliases + [('new', 'Add a new local identity'), ('none', 'Leave mining unconfigured'), ('reboot', 'Cancel and reboot')] set_stage('identity_selection') @@ -362,13 +395,8 @@ def run() -> int: except RebootRequested: record_event('cancelled', 'reboot_requested') return 0 - except Exception as error: - reason = f'unexpected_{type(error).__name__}' - record_event('failed', reason) - print(f'RIGOS firstboot failed stage={CURRENT_STAGE} reason={reason}', file=sys.stderr, flush=True) - return 20 - set_stage('complete') record_event('ready', 'firstboot_completed') return 0 + if __name__ == '__main__': raise SystemExit(run()) From 9131e59f88ff9b39264c0a29de73527eb881b92c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:13:26 +0700 Subject: [PATCH 031/386] hook include identity seed resolver --- build/usb/hooks/010-rigos.chroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 0139276a..e29c5d1d 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,7 +8,7 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/xmrig +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true From fa65512de75ade281ef75052f46a4ce3df803ee1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:15:44 +0700 Subject: [PATCH 032/386] identity seed harden verified usb reader --- .../usr/lib/rigos/rigos-identity-seed | 319 ++++++++++++------ 1 file changed, 223 insertions(+), 96 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed b/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed index 0a53d5de..2a288d84 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed @@ -1,4 +1,6 @@ #!/usr/bin/python3 +from __future__ import annotations + import argparse import json import os @@ -8,142 +10,263 @@ import subprocess import sys import tempfile from pathlib import Path +from typing import Iterator RUNTIME = Path('/run/rigos') STATE_STATUS = RUNTIME / 'state-status.json' ATTESTATION = RUNTIME / 'boot-device.json' +BOOT_ID = Path('/proc/sys/kernel/random/boot_id') STATE_INIT = '/usr/lib/rigos/rigos-state-init' +MOUNT = '/usr/bin/mount' +UMOUNT = '/usr/bin/umount' +FINDMNT = '/usr/bin/findmnt' SLUG_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9-]{0,63}$') +SAFE_JSON_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]{0,122}\.json$') MAX_SEED_BYTES = 4096 MAX_VALUE_BYTES = 512 +MAX_SEED_FILES = 128 + + +class SeedError(Exception): + def __init__(self, code: str, message: str, *, file: str | None = None, key: str | None = None): + super().__init__(message) + self.code = code + self.message = message + self.file = file + self.key = key + + def diagnostic(self) -> dict: + return { + 'code': self.code, + 'file': self.file, + 'line': None, + 'key': self.key, + 'message': self.message, + } -def fail(code: str, message: str, rc: int = 2) -> int: - print(json.dumps({'code': code, 'message': message}, sort_keys=True), file=sys.stderr) - return rc +def invalid(message: str, *, file: str | None = None, key: str | None = None) -> SeedError: + return SeedError('RIGOS_IDENTITY_SEED_INVALID', message, file=file, key=key) -def read_json(path: Path): - with path.open('r', encoding='utf-8') as stream: - return json.load(stream) +def unproven(message: str) -> SeedError: + return SeedError('RIGOS_CONFIG_BOOT_DEVICE_UNPROVEN', message) + + +def read_json(path: Path, message: str) -> dict: + try: + value = json.loads(path.read_text(encoding='utf-8')) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise unproven(message) from error + if not isinstance(value, dict): + raise unproven(message) + return value def ready_state() -> bool: try: - status = read_json(STATE_STATUS) - except (OSError, json.JSONDecodeError): + status = read_json(STATE_STATUS, 'persistent state status is unavailable') + except SeedError: return False - return status.get('outcome') in {'ready', 'grown'} + return status.get('outcome') == 'ready' def fresh_attestation() -> dict: - temporary = Path(tempfile.mkdtemp(prefix='identity-seed-attest-', dir=RUNTIME)) - try: - attestation_path = temporary / 'boot-device.json' - status_path = temporary / 'state-status.json' + RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix='identity-seed-attest-', dir=RUNTIME) as temporary: + root = Path(temporary) + attestation_path = root / 'boot-device.json' + status_path = root / 'state-status.json' result = subprocess.run( - [STATE_INIT, '--dry-run', '--attestation-only', '--attestation', str(attestation_path), '--status', str(status_path)], + [ + STATE_INIT, + '--dry-run', + '--attestation-only', + '--attestation', + str(attestation_path), + '--status', + str(status_path), + ], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) if result.returncode != 0: - raise RuntimeError('attestation refresh failed') - return read_json(attestation_path) - finally: - for child in temporary.iterdir() if temporary.exists() else []: - child.unlink(missing_ok=True) - temporary.rmdir() if temporary.exists() else None + raise unproven('boot device attestation refresh failed') + return read_json(attestation_path, 'fresh boot device attestation is unavailable') + + +def folded(value: object) -> str: + return str(value or '').lower() def identity_matches(expected: dict, fresh: dict) -> bool: - return ( - expected.get('schema') == 'rigos.boot-device/v1' - and fresh.get('schema') == expected.get('schema') - and expected.get('verification_outcome') == 'verified' - and fresh.get('verification_outcome') == 'verified' - and fresh.get('boot_id') == expected.get('boot_id') - and fresh.get('disk', {}).get('major_minor') == expected.get('disk', {}).get('major_minor') - and fresh.get('disk', {}).get('ptuuid', '').lower() == expected.get('disk', {}).get('ptuuid', '').lower() - and fresh.get('partition1', {}).get('major_minor') == expected.get('partition1', {}).get('major_minor') - and fresh.get('partition1', {}).get('partuuid', '').lower() == expected.get('partition1', {}).get('partuuid', '').lower() - and fresh.get('root', {}).get('major_minor') == expected.get('root', {}).get('major_minor') - ) - - -def safe_seed_path(root: Path, alias: str) -> Path: - if not SLUG_RE.fullmatch(alias): - raise ValueError('invalid identity alias') - return root / 'rigos' / 'identities' / f'{alias}.json' - - -def read_seed(path: Path, alias: str) -> dict | None: + try: + current_boot_id = BOOT_ID.read_text(encoding='ascii').strip() + return ( + expected.get('schema') == 'rigos.boot-device/v1' + and fresh.get('schema') == expected.get('schema') + and expected.get('verification_outcome') == 'verified' + and fresh.get('verification_outcome') == 'verified' + and expected.get('boot_id') == current_boot_id + and fresh.get('boot_id') == current_boot_id + and fresh['disk']['major_minor'] == expected['disk']['major_minor'] + and folded(fresh['disk']['ptuuid']) == folded(expected['disk']['ptuuid']) + and fresh['partition1']['major_minor'] == expected['partition1']['major_minor'] + and folded(fresh['partition1']['partuuid']) == folded(expected['partition1']['partuuid']) + and fresh['root']['major_minor'] == expected['root']['major_minor'] + ) + except (OSError, KeyError, TypeError): + return False + + +def unique_object(pairs: list[tuple[str, object]]) -> dict: + output: dict[str, object] = {} + for key, value in pairs: + if key in output: + raise ValueError(f'duplicate member {key}') + output[key] = value + return output + + +def read_seed(path: Path) -> dict: + name = path.name try: info = path.lstat() - except FileNotFoundError: - return None - if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_size > MAX_SEED_BYTES: - raise ValueError('identity seed is not a bounded regular file') - data = path.read_bytes() - if len(data) > MAX_SEED_BYTES: - raise ValueError('identity seed exceeds size limit') - seed = json.loads(data.decode('utf-8')) - allowed = {'schema', 'alias', 'kind', 'value'} - if set(seed) != allowed: - raise ValueError('identity seed contains unknown or missing fields') + except OSError as error: + raise invalid('identity seed metadata is unavailable', file=name) from error + if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode): + raise invalid('identity seed must be a regular file', file=name) + if info.st_size > MAX_SEED_BYTES: + raise invalid('identity seed exceeds 4 KiB', file=name) + + flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) + try: + descriptor = os.open(path, flags) + except OSError as error: + raise invalid('identity seed could not be opened safely', file=name) from error + try: + opened = os.fstat(descriptor) + if not stat.S_ISREG(opened.st_mode) or opened.st_size > MAX_SEED_BYTES: + raise invalid('identity seed must be a bounded regular file', file=name) + raw = os.read(descriptor, MAX_SEED_BYTES + 1) + finally: + os.close(descriptor) + if len(raw) > MAX_SEED_BYTES: + raise invalid('identity seed exceeds 4 KiB', file=name) + + try: + seed = json.loads(raw.decode('utf-8'), object_pairs_hook=unique_object) + except (UnicodeError, json.JSONDecodeError, ValueError) as error: + raise invalid('identity seed is not strict JSON', file=name) from error + if not isinstance(seed, dict): + raise invalid('identity seed must be a JSON object', file=name) + if set(seed) != {'schema', 'alias', 'kind', 'value'}: + raise invalid('identity seed fields do not match schema', file=name) + + alias = seed.get('alias') + value = seed.get('value') if seed.get('schema') != 'rigos.identity-seed/v1': - raise ValueError('unsupported identity seed schema') - if seed.get('alias') != alias or not SLUG_RE.fullmatch(seed.get('alias', '')): - raise ValueError('identity seed alias mismatch') + raise invalid('unsupported identity seed schema', file=name, key='schema') if seed.get('kind') != 'mining_identity': - raise ValueError('unsupported identity seed kind') - value = seed.get('value') - if not isinstance(value, str) or not value or len(value.encode('utf-8')) > MAX_VALUE_BYTES: - raise ValueError('identity seed value is empty or too large') - if any(character.isspace() or ord(character) < 32 for character in value): - raise ValueError('identity seed value contains whitespace or control characters') + raise invalid('unsupported identity seed kind', file=name, key='kind') + if not isinstance(alias, str) or SLUG_RE.fullmatch(alias) is None: + raise invalid('identity seed alias is invalid', file=name, key='alias') + if path.stem != alias: + raise invalid('identity seed alias must match its filename', file=name, key='alias') + if ( + not isinstance(value, str) + or not value + or len(value.encode('utf-8')) > MAX_VALUE_BYTES + or not value.isascii() + or any(ord(character) < 33 or ord(character) > 126 for character in value) + ): + raise invalid('mining identity value is invalid', file=name, key='value') return { 'schema': 'rigos.identity/v1', 'alias': alias, 'kind': 'mining_identity', 'value': value, 'created_locally': True, - 'seeded_from_usb': True, } +def seed_paths(directory: Path) -> Iterator[Path]: + try: + info = directory.lstat() + except FileNotFoundError: + return + except OSError as error: + raise invalid('identity seed directory is unavailable') from error + if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode): + raise invalid('identity seed path must be a directory') + + count = 0 + for path in sorted(directory.iterdir(), key=lambda item: item.name.lower()): + if path.suffix.lower() != '.json': + continue + count += 1 + if count > MAX_SEED_FILES: + raise invalid('too many identity seed files') + if SAFE_JSON_RE.fullmatch(path.name) is None or path.name.startswith('.') or '..' in path.name: + raise invalid('unsafe identity seed filename', file=path.name) + yield path + + +def load_identity_seed(root: Path, requested_alias: str) -> dict | None: + if SLUG_RE.fullmatch(requested_alias) is None: + raise invalid('requested identity alias is invalid', key='identity_ref') + selected: dict | None = None + aliases: set[str] = set() + for path in seed_paths(root / 'rigos' / 'identities'): + identity = read_seed(path) + alias = identity['alias'].lower() + if alias in aliases: + raise invalid('duplicate identity seed alias', file=path.name, key='alias') + aliases.add(alias) + if identity['alias'] == requested_alias: + selected = identity + return selected + + def mount_and_read(alias: str) -> dict | None: - expected = read_json(ATTESTATION) + expected = read_json(ATTESTATION, 'boot device attestation is unavailable') fresh = fresh_attestation() if not identity_matches(expected, fresh): - raise RuntimeError('boot device identity changed') - mountpoint = Path(tempfile.mkdtemp(prefix='identity-seed-efi-', dir=RUNTIME)) - mounted = False - try: - partition = fresh['partition1']['path'] - result = subprocess.run( - ['mount', '-o', 'ro,nodev,nosuid,noexec', partition, str(mountpoint)], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - if result.returncode != 0: - raise RuntimeError('verified EFI partition could not be mounted') - mounted = True - identity = subprocess.run( - ['findmnt', '--noheadings', '--output', 'MAJ:MIN', '--target', str(mountpoint)], - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - ) - if identity.returncode != 0 or identity.stdout.strip() != fresh['partition1']['major_minor']: - raise RuntimeError('mounted EFI identity changed') - return read_seed(safe_seed_path(mountpoint, alias), alias) - finally: - if mounted: - subprocess.run(['umount', str(mountpoint)], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - mountpoint.rmdir() if mountpoint.exists() else None + raise unproven('boot device identity changed after attestation') + + with tempfile.TemporaryDirectory(prefix='identity-seed-efi-', dir=RUNTIME) as temporary: + mountpoint = Path(temporary) + mounted = False + try: + partition = fresh['partition1'] + result = subprocess.run( + [MOUNT, '-o', 'ro,nodev,nosuid,noexec', str(partition['path']), str(mountpoint)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if result.returncode != 0: + raise unproven('verified EFI partition could not be mounted read only') + mounted = True + identity = subprocess.run( + [FINDMNT, '--noheadings', '--output', 'MAJ:MIN', '--target', str(mountpoint)], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + if identity.returncode != 0 or identity.stdout.strip() != str(partition['major_minor']): + raise unproven('mounted EFI identity changed after verification') + return load_identity_seed(mountpoint, alias) + finally: + if mounted: + subprocess.run( + [UMOUNT, str(mountpoint)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) def main() -> int: @@ -151,13 +274,17 @@ def main() -> int: parser.add_argument('--alias', required=True) args = parser.parse_args() if not ready_state(): - return fail('RIGOS_CONFIG_BOOT_DEVICE_UNPROVEN', 'state is not ready') + print(json.dumps(unproven('state is not ready').diagnostic(), sort_keys=True), file=sys.stderr) + return 2 try: seed = mount_and_read(args.alias) - except FileNotFoundError: - return 1 - except (OSError, KeyError, json.JSONDecodeError, UnicodeDecodeError, ValueError, RuntimeError) as error: - return fail('RIGOS_IDENTITY_SEED_INVALID', str(error)) + except SeedError as error: + print(json.dumps(error.diagnostic(), sort_keys=True), file=sys.stderr) + return 2 + except Exception as error: + diagnostic = invalid(f'identity seed failed with {type(error).__name__}') + print(json.dumps(diagnostic.diagnostic(), sort_keys=True), file=sys.stderr) + return 2 if seed is None: return 1 print(json.dumps(seed, sort_keys=True)) From 87c416481aa68efba0f732eda7622c021974899d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:16:38 +0700 Subject: [PATCH 033/386] test offline identity seed provisioning --- crates/rigos-config/tests/identity_seed.rs | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 crates/rigos-config/tests/identity_seed.rs diff --git a/crates/rigos-config/tests/identity_seed.rs b/crates/rigos-config/tests/identity_seed.rs new file mode 100644 index 00000000..f01dd39c --- /dev/null +++ b/crates/rigos-config/tests/identity_seed.rs @@ -0,0 +1,91 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use uuid::Uuid; + +fn repo(relative: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(relative) +} + +#[test] +fn identity_seed_validation_is_strict() { + let root = std::env::temp_dir().join(format!("rigos-seed-{}", Uuid::new_v4())); + fs::create_dir_all(&root).expect("create seed fixture"); + let program = r#" +import json, os, runpy, sys +from pathlib import Path +source, root = Path(sys.argv[1]), Path(sys.argv[2]) +ns = runpy.run_path(str(source), run_name='seed_test') +load, SeedError = ns['load_identity_seed'], ns['SeedError'] +directory = root / 'rigos' / 'identities' +directory.mkdir(parents=True) +path = directory / 'main-xmr.json' +value = 'SYNTHETIC_IDENTITY_ABC123' +valid = {'schema':'rigos.identity-seed/v1','alias':'main-xmr','kind':'mining_identity','value':value} +path.write_text(json.dumps(valid), encoding='utf-8') +identity = load(root, 'main-xmr') +assert identity['alias'] == 'main-xmr' and identity['value'] == value +for change in ( + {'extra': True}, + {'value': 'bad value'}, + {'value': 'A' * 513}, + {'alias': 'other-xmr'}, +): + candidate = dict(valid); candidate.update(change) + path.write_text(json.dumps(candidate), encoding='utf-8') + try: load(root, 'main-xmr'); raise AssertionError('unsafe seed accepted') + except SeedError: pass +path.write_text(json.dumps(valid), encoding='utf-8') +upper = dict(valid); upper['alias'] = 'MAIN-XMR'; upper['value'] = 'OTHER_IDENTITY' +(directory / 'MAIN-XMR.json').write_text(json.dumps(upper), encoding='utf-8') +try: load(root, 'main-xmr'); raise AssertionError('duplicate alias accepted') +except SeedError: pass +(directory / 'MAIN-XMR.json').unlink() +path.write_text('{"schema":"rigos.identity-seed/v1","alias":"main-xmr","alias":"main-xmr","kind":"mining_identity","value":"ABC"}', encoding='utf-8') +try: load(root, 'main-xmr'); raise AssertionError('duplicate member accepted') +except SeedError: pass +path.unlink(); target = root / 'outside.json'; target.write_text(json.dumps(valid), encoding='utf-8'); os.symlink(target, path) +try: load(root, 'main-xmr'); raise AssertionError('symlink accepted') +except SeedError: pass +"#; + let status = Command::new("python3") + .arg("-c") + .arg(program) + .arg(repo( + "build/usb/includes.chroot/usr/lib/rigos/rigos-identity-seed", + )) + .arg(&root) + .status() + .expect("run seed fixture"); + let _ = fs::remove_dir_all(&root); + assert!(status.success(), "identity seed fixture failed"); +} + +#[test] +fn firstboot_resolves_seed_without_value_prompt() { + let program = r#" +import runpy, sys, tempfile +from pathlib import Path +ns = runpy.run_path(sys.argv[1], run_name='firstboot_seed_test') +g = ns['run'].__globals__ +with tempfile.TemporaryDirectory() as temporary: + g['STATE'] = Path(temporary) + seed = {'schema':'rigos.identity/v1','alias':'main-xmr','kind':'mining_identity','value':'SYNTHETIC_IDENTITY_ABC123','created_locally':True} + g['load_identity_seed'] = lambda alias: dict(seed) if alias == 'main-xmr' else None + g['confirm'] = lambda message, stage=None: True + g['prompt'] = lambda stage, *_args, **_kwargs: (_ for _ in ()).throw(AssertionError('value prompt used')) + proposal = {'flight_sheet': {'identity_ref':'main-xmr'}, 'provenance':None} + assert ns['resolve_identity'](proposal) == seed +"#; + let status = Command::new("python3") + .arg("-c") + .arg(program) + .arg(repo( + "build/usb/includes.chroot/usr/local/sbin/rigos-firstboot", + )) + .status() + .expect("run firstboot seed fixture"); + assert!(status.success(), "firstboot seed fixture failed"); +} From bf63c77bf8f5b61c7c8c8fc7504fdf55f754097b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:17:09 +0700 Subject: [PATCH 034/386] tool write offline rig provisioning --- scripts/write-rigos-usb-config.ps1 | 162 +++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 scripts/write-rigos-usb-config.ps1 diff --git a/scripts/write-rigos-usb-config.ps1 b/scripts/write-rigos-usb-config.ps1 new file mode 100644 index 00000000..6d575386 --- /dev/null +++ b/scripts/write-rigos-usb-config.ps1 @@ -0,0 +1,162 @@ +[CmdletBinding()] +param( + [string]$DriveLetter, + [string]$NodeName = "rig01", + [string]$Timezone = "Asia/Bangkok", + [string]$PoolHost = "139.99.69.109", + [ValidateRange(1, 65535)] + [int]$PoolPort = 10001, + [switch]$Tls, + [ValidateRange(1, 1024)] + [int]$Threads = 2, + [string]$IdentityAlias = "main-xmr", + [string]$IdentityValue +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Assert-Slug { + param([string]$Name, [string]$Value) + if ($Value -notmatch '^[A-Za-z0-9][A-Za-z0-9-]{0,63}$') { + throw "$Name is not a valid RIGOS slug" + } +} + +function Write-Utf8NoBom { + param([string]$Path, [string]$Content) + $encoding = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($Path, $Content, $encoding) +} + +Assert-Slug -Name "NodeName" -Value $NodeName +Assert-Slug -Name "IdentityAlias" -Value $IdentityAlias + +if ($PoolHost.Length -lt 1 -or $PoolHost.Length -gt 253 -or $PoolHost -match '\s') { + throw "PoolHost is invalid" +} +if ($Timezone.Length -lt 1 -or $Timezone.Length -gt 128 -or $Timezone.StartsWith('/') -or $Timezone.Contains('..')) { + throw "Timezone is invalid" +} + +if ([string]::IsNullOrEmpty($DriveLetter)) { + $volume = Get-Volume | + Where-Object FileSystemLabel -eq 'EFI_SYSTEM' | + Select-Object -First 1 + if (-not $volume) { + throw "EFI_SYSTEM volume was not found" + } + if (-not $volume.DriveLetter) { + throw "EFI_SYSTEM volume has no drive letter" + } + $DriveLetter = [string]$volume.DriveLetter +} +$DriveLetter = $DriveLetter.Trim().TrimEnd(':') +if ($DriveLetter -notmatch '^[A-Za-z]$') { + throw "DriveLetter must contain one drive letter" +} +$volume = Get-Volume -DriveLetter $DriveLetter +if ($volume.FileSystemLabel -ne 'EFI_SYSTEM') { + throw "$DriveLetter`: is not the RIGOS EFI_SYSTEM volume" +} + +if ([string]::IsNullOrEmpty($IdentityValue)) { + $IdentityValue = Read-Host "Paste the public mining identity or wallet address" +} +$unsafeCharacters = @( + $IdentityValue.ToCharArray() | + Where-Object { [int]$_ -lt 33 -or [int]$_ -gt 126 } +) +if ( + [string]::IsNullOrEmpty($IdentityValue) -or + $IdentityValue.Length -gt 512 -or + $IdentityValue -match '\s' -or + $unsafeCharacters.Count -ne 0 +) { + throw "IdentityValue must be 1 to 512 visible ASCII characters with no whitespace" +} + +$root = "$DriveLetter`:\rigos" +$flightDirectory = Join-Path $root "flight-sheets" +$identityDirectory = Join-Path $root "identities" +New-Item -ItemType Directory -Force $flightDirectory | Out-Null +New-Item -ItemType Directory -Force $identityDirectory | Out-Null + +$rigConfig = @" +RIGOS_CONFIG_VERSION=1 +NODE_NAME=$NodeName +TIMEZONE=$Timezone +FLIGHT_SOURCE=native +FLIGHT_REF=xmr +MINER_START_MODE=on_boot +"@ + +$flightSheet = [ordered]@{ + schema = "rigos.flight-sheet/v1" + name = "xmr" + coin = "XMR" + backend = "xmrig" + algorithm = "rx/0" + pools = @( + [ordered]@{ + host = $PoolHost + port = $PoolPort + tls = [bool]$Tls + priority = 0 + } + ) + identity_ref = $IdentityAlias + worker_template = "{node_name}" + cpu = [ordered]@{ + threads = $Threads + huge_pages = $true + max_threads_hint = 100 + } +} | ConvertTo-Json -Depth 8 + +$identitySeed = [ordered]@{ + schema = "rigos.identity-seed/v1" + alias = $IdentityAlias + kind = "mining_identity" + value = $IdentityValue +} | ConvertTo-Json -Depth 4 + +$rigPath = Join-Path $root "rig.conf" +$flightPath = Join-Path $flightDirectory "xmr.json" +$identityPath = Join-Path $identityDirectory "$IdentityAlias.json" +Write-Utf8NoBom -Path $rigPath -Content ($rigConfig.TrimEnd() + "`n") +Write-Utf8NoBom -Path $flightPath -Content ($flightSheet + "`n") +Write-Utf8NoBom -Path $identityPath -Content ($identitySeed + "`n") + +$readBack = Get-Content -Raw -LiteralPath $identityPath | ConvertFrom-Json +if ( + $readBack.schema -ne "rigos.identity-seed/v1" -or + $readBack.alias -ne $IdentityAlias -or + $readBack.kind -ne "mining_identity" -or + $readBack.value -ne $IdentityValue +) { + throw "Identity seed verification failed after writing" +} + +$suffixLength = [Math]::Min(4, $IdentityValue.Length) +$suffix = $IdentityValue.Substring($IdentityValue.Length - $suffixLength) +Write-Host "RIGOS USB provisioning written" +Write-Host "Volume $DriveLetter`: EFI_SYSTEM" +Write-Host "Node $NodeName" +Write-Host "Pool $PoolHost`:$PoolPort" +Write-Host "TLS $([bool]$Tls)" +Write-Host "Threads $Threads" +Write-Host "Identity $IdentityAlias suffix ****$suffix" +Write-Host "" +Get-ChildItem -LiteralPath $root -Recurse -File | + ForEach-Object { + $hash = Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256 + [PSCustomObject]@{ + Path = $_.FullName + Length = $_.Length + SHA256 = $hash.Hash + } + } | + Format-Table -AutoSize + +Write-Host "Eject the USB safely before booting RIGOS" From 653935987a215cbcd8e72b0a943f9f65270d6d14 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:17:31 +0700 Subject: [PATCH 035/386] example add public identity seed --- configs/identities/main-xmr.json.example | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 configs/identities/main-xmr.json.example diff --git a/configs/identities/main-xmr.json.example b/configs/identities/main-xmr.json.example new file mode 100644 index 00000000..2a49d14c --- /dev/null +++ b/configs/identities/main-xmr.json.example @@ -0,0 +1,6 @@ +{ + "schema": "rigos.identity-seed/v1", + "alias": "main-xmr", + "kind": "mining_identity", + "value": "REPLACE_WITH_PUBLIC_MINING_IDENTITY" +} From 9fa60adfb82cff271025ddf7c16e33180a9319bb Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:18:18 +0700 Subject: [PATCH 036/386] docs explain offline identity seed --- docs/local-rig-config.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/local-rig-config.md b/docs/local-rig-config.md index 5d5ef92b..0bd9103e 100644 --- a/docs/local-rig-config.md +++ b/docs/local-rig-config.md @@ -9,6 +9,7 @@ It stages bounded regular files in tmpfs before parsing and never sources config ```text /rigos/rig.conf /rigos/flight-sheets/*.json +/rigos/identities/*.json /rigos/import/*.json ``` @@ -16,11 +17,21 @@ It stages bounded regular files in tmpfs before parsing and never sources config `FLIGHT_SOURCE` is `native`, `import` or `interactive`. Native and import modes require one safe `FLIGHT_REF`; interactive mode forbids it and never auto-selects a file. `MINER_START_MODE` is `manual` or `on_boot`. Legacy `AUTO_START`, watchdog and split active/import selection keys are rejected. -Flight Sheets describe only an XMRig workload: algorithm, ordered pools, TLS, a local identity reference, worker template and CPU policy. They cannot start services or contain a wallet, pool username, password or cloud credential. See `configs/flight-sheets/xmr-ssl.json`. +Flight Sheets describe only an XMRig workload: algorithm, ordered pools, TLS, a local identity reference, worker template and CPU policy. They cannot start services or contain a mining identity, pool password or cloud credential. See `configs/flight-sheets/xmr-ssl.json`. ## Local identities -The operator resolves `identity_ref` on the local TTY. Values are stored only in the verified persistent state under `/var/lib/rigos/identities`, protected from normal diagnostics, and copied only into the restricted runtime `xmrig.json`. Canonical policy stores the alias, not the value. +A native Flight Sheet names one local alias through `identity_ref`. RIGOS first checks the verified persistent state under `/var/lib/rigos/identities`. + +For offline provisioning, the exact verified EFI partition may also provide `/rigos/identities/.json` using schema `rigos.identity-seed/v1`. The filename and alias must match. The seed contains exactly `schema`, `alias`, `kind` and `value`; `kind` must be `mining_identity`. + +Identity seeds are bounded strict JSON regular files. Symlinks, unknown fields, duplicate members, unsafe filenames, duplicate aliases, whitespace, control characters and oversized values are rejected. The EFI partition is mounted read only with `nodev`, `nosuid` and `noexec` after its boot identity is revalidated. + +A valid seed resolves the matching native Flight Sheet without typing the value on tty1. Firstboot displays only the alias and a short suffix, then asks for final confirmation. The committed identity is stored mode `0600` inside the persistent revision and only copied into the restricted runtime `xmrig.json`. Canonical policy stores the alias, not the value. + +The EFI partition is not a secret store. Use identity seeding only for a public wallet address or other public pool identity. Never place a private key, seed phrase, password, remote credential or account token there. Remove the seed from EFI after successful provisioning when the public value does not need to remain portable. + +If no matching seed exists, the original local interactive identity flow remains available. External `wal_id` values remain external references. They are never sent to XMRig. Confirmed mappings are local state and are not copied back to EFI_SYSTEM. From 4d8d22fe6c933855d0882d6e419e862cc6019de5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 12:19:10 +0700 Subject: [PATCH 037/386] test check offline provisioning source --- scripts/check-offline-provisioning.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 scripts/check-offline-provisioning.sh diff --git a/scripts/check-offline-provisioning.sh b/scripts/check-offline-provisioning.sh new file mode 100644 index 00000000..0ba61b51 --- /dev/null +++ b/scripts/check-offline-provisioning.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -euo pipefail + +root="${1:-build/usb/includes.chroot}" +unit="$root/etc/systemd/system/rigos-firstboot.service" +seed="$root/usr/lib/rigos/rigos-identity-seed" +firstboot="$root/usr/local/sbin/rigos-firstboot" + +[[ -f "$unit" ]] || { echo 'firstboot unit missing' >&2; exit 1; } +[[ -f "$seed" ]] || { echo 'identity seed resolver missing' >&2; exit 1; } +[[ -f "$firstboot" ]] || { echo 'firstboot missing' >&2; exit 1; } +grep -Fq 'ExecStart=/usr/local/sbin/rigos-firstboot' "$unit" +grep -Fq 'rigos.identity-seed/v1' "$seed" +grep -Fq 'ro,nodev,nosuid,noexec' "$seed" +grep -Fq 'O_NOFOLLOW' "$seed" +grep -Fq 'load_identity_seed' "$firstboot" +grep -Fq 'identity_seed_confirmation' "$firstboot" +python3 -m py_compile "$seed" "$firstboot" +printf 'RIGOS offline provisioning source check passed\n' From a7e8398ca5a8e8cbc317cf709765cdaf516bf61a Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Tue, 7 Jul 2026 12:57:17 +0700 Subject: [PATCH 038/386] firstboot mark completed UI stage --- build/usb/includes.chroot/usr/local/sbin/rigos-firstboot | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot index ce2c6cb7..3347cb98 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-firstboot @@ -395,6 +395,7 @@ def run() -> int: except RebootRequested: record_event('cancelled', 'reboot_requested') return 0 + set_stage('complete') record_event('ready', 'firstboot_completed') return 0 From 043974e9f1f1c26055a39ac607bf78d920ac952b Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Tue, 7 Jul 2026 12:59:02 +0700 Subject: [PATCH 039/386] release stage alpha7 appliance candidate --- build/usb/version.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/version.env b/build/usb/version.env index 4029501e..041c82ab 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.6 -RIGOS_IMAGE_VERSION=0.0.4-alpha.6 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.7 +RIGOS_IMAGE_VERSION=0.0.4-alpha.7 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=6 +RIGOS_BUILD_ORDINAL=7 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 From 11501ece781475fce4ec0e69671a37656145104f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:05:34 +0700 Subject: [PATCH 040/386] image declare OpenSSH dependency --- build/usb/package-lists/rigos.list.chroot | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/package-lists/rigos.list.chroot b/build/usb/package-lists/rigos.list.chroot index 89304e89..85b9c79b 100644 --- a/build/usb/package-lists/rigos.list.chroot +++ b/build/usb/package-lists/rigos.list.chroot @@ -7,6 +7,7 @@ live-boot live-config linux-image-amd64 network-manager +openssh-server python3 systemd-sysv sudo From fe61a3e06fb5a08966f62f6d75386e5327f39049 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:31:06 +0700 Subject: [PATCH 041/386] ci add alpha8 physical findings applicator --- .github/workflows/alpha8-apply.yml | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/alpha8-apply.yml diff --git a/.github/workflows/alpha8-apply.yml b/.github/workflows/alpha8-apply.yml new file mode 100644 index 00000000..b6ee4849 --- /dev/null +++ b/.github/workflows/alpha8-apply.yml @@ -0,0 +1,43 @@ +name: Alpha8 Physical Findings + +on: + push: + branches: + - fix/alpha8-physical-findings + paths: + - .alpha8-patch-trigger + +permissions: + contents: write + +jobs: + apply-and-verify: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - name: Check out branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + + - name: Apply exact source patch + run: python3 scripts/apply-alpha8-physical-fixes.py + + - name: Install pinned Rust toolchain + uses: dtolnay/rust-toolchain@1.85.1 + with: + components: rustfmt, clippy + + - name: Verify source + run: ./scripts/verify.sh + + - name: Commit verified source + run: | + rm -f .alpha8-patch-trigger + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --check + git commit -m "alpha8 harden physical appliance findings" + git push origin HEAD:fix/alpha8-physical-findings From 0a0fe4f6a5ff67b9017008197e2080da22817e9f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:33:09 +0700 Subject: [PATCH 042/386] tool add deterministic alpha8 source patch --- scripts/apply-alpha8-physical-fixes.py | 814 +++++++++++++++++++++++++ 1 file changed, 814 insertions(+) create mode 100644 scripts/apply-alpha8-physical-fixes.py diff --git a/scripts/apply-alpha8-physical-fixes.py b/scripts/apply-alpha8-physical-fixes.py new file mode 100644 index 00000000..5fa09d9d --- /dev/null +++ b/scripts/apply-alpha8-physical-fixes.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def write(path: str, content: str, mode: int | None = None) -> None: + target = ROOT / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8", newline="\n") + if mode is not None: + target.chmod(mode) + + +def replace_once(path: str, old: str, new: str) -> None: + content = read(path) + count = content.count(old) + if count != 1: + raise SystemExit(f"{path}: expected one patch anchor, found {count}") + write(path, content.replace(old, new, 1)) + + +def create(path: str, content: str, mode: int | None = None) -> None: + target = ROOT / path + if target.exists(): + raise SystemExit(f"refusing to overwrite existing file: {path}") + write(path, content, mode) + + +# Exact RandomX thread intent. max-threads-hint remains a percentage hint. +replace_once( + "crates/rigos-config/src/lib.rs", + "pub fn build_runtime(\n", + '''fn exact_cpu_profile_name(algorithm: &str) -> Option<&str> { + if matches!(algorithm, "rx" | "rx/0") { + Some("rx") + } else if algorithm.starts_with("rx/") { + Some(algorithm) + } else { + None + } +} + +fn exact_cpu_profile(count: u16) -> Value { + Value::Array((0..count).map(|_| json!(-1)).collect()) +} + +pub fn build_runtime( +''', +) +replace_once( + "crates/rigos-config/src/lib.rs", + ''' match &sheet.cpu.threads { + Threads::Auto(value) if value == "auto" => {} + Threads::Count(value) if (1..=1024).contains(value) => {} + _ => { + return Err(error( + "RIGOS_FLIGHT_SHEET_INVALID", + Some(filename), + None, + Some("cpu.threads"), + "threads must be auto or 1 through 1024", + )); + } + } +''', + ''' match &sheet.cpu.threads { + Threads::Auto(value) if value == "auto" => {} + Threads::Count(value) if !(1..=1024).contains(value) => { + return Err(error( + "RIGOS_FLIGHT_SHEET_INVALID", + Some(filename), + None, + Some("cpu.threads"), + "threads must be auto or 1 through 1024", + )); + } + Threads::Count(_) if exact_cpu_profile_name(&sheet.algorithm).is_none() => { + return Err(error( + "RIGOS_FLIGHT_SHEET_INVALID", + Some(filename), + None, + Some("cpu.threads"), + "explicit threads currently require a RandomX algorithm", + )); + } + Threads::Count(_) => {} + _ => { + return Err(error( + "RIGOS_FLIGHT_SHEET_INVALID", + Some(filename), + None, + Some("cpu.threads"), + "threads must be auto or 1 through 1024", + )); + } + } +''', +) +replace_once( + "crates/rigos-config/src/lib.rs", + ''' let mut cpu = Map::from_iter([ + ("enabled".into(), Value::Bool(true)), + ("huge-pages".into(), Value::Bool(sheet.cpu.huge_pages)), + ]); + cpu.insert("max-threads-hint".into(), json!(sheet.cpu.max_threads_hint)); + if let Threads::Count(count) = sheet.cpu.threads { + cpu.insert("max-threads-hint".into(), json!(count)); + } + let xmrig = json!({"autosave":false,"background":false,"cpu":cpu,"pools":pools,"api":{"worker-id":worker},"http":{"enabled":false}}); +''', + ''' let mut cpu = Map::from_iter([ + ("enabled".into(), Value::Bool(true)), + ("huge-pages".into(), Value::Bool(sheet.cpu.huge_pages)), + ]); + cpu.insert("max-threads-hint".into(), json!(sheet.cpu.max_threads_hint)); + if let Threads::Count(count) = &sheet.cpu.threads { + let profile_name = exact_cpu_profile_name(&sheet.algorithm).ok_or_else(|| { + error( + "RIGOS_FLIGHT_SHEET_INVALID", + None, + None, + Some("cpu.threads"), + "explicit threads do not have a safe XMRig profile mapping", + ) + })?; + cpu.insert(profile_name.into(), exact_cpu_profile(*count)); + } + let xmrig = json!({"autosave":false,"background":false,"cpu":cpu,"pools":pools,"api":{"worker-id":worker},"http":{"enabled":false}}); +''', +) +replace_once( + "crates/rigos-config/src/lib.rs", + ''' #[test] + fn huge_pages_false_reaches_runtime_config() { +''', + ''' #[test] + fn explicit_randomx_threads_generate_exact_profile() { + let proposal = Proposal { + schema: "rigos.config-proposal/v1".into(), + profile: parse_rig_profile(&profile("native", "FLIGHT_REF=xmr-ssl\\n")).unwrap(), + flight_sheet: FlightSheet { + schema: "rigos.flight-sheet/v1".into(), + name: "xmr-ssl".into(), + coin: "XMR".into(), + backend: "xmrig".into(), + algorithm: "rx/0".into(), + pools: vec![Pool { + host: "pool.example".into(), + port: 443, + tls: true, + priority: 0, + }], + identity_ref: "main-xmr".into(), + worker_template: "{node_name}".into(), + cpu: CpuPolicy { + threads: Threads::Count(2), + huge_pages: true, + max_threads_hint: 100, + }, + }, + provenance: None, + source_sha256: "x".into(), + }; + let identity = IdentityRecord { + schema: "rigos.identity/v1".into(), + alias: "main-xmr".into(), + kind: "mining_identity".into(), + value: "private-value".into(), + created_locally: true, + }; + let (_, xmrig) = build_runtime(&proposal, &identity).unwrap(); + assert_eq!(xmrig["cpu"]["max-threads-hint"], 100); + assert_eq!(xmrig["cpu"]["rx"], json!([-1, -1])); + + let mut unsupported = proposal; + unsupported.flight_sheet.algorithm = "cn/r".into(); + assert!(build_runtime(&unsupported, &identity).is_err()); + } + + #[test] + fn huge_pages_false_reaches_runtime_config() { +''', +) + +# Unprivileged inspection must use a redacted derived config, never the wallet-bearing runtime file. +replace_once( + "crates/rigos-xmrig/src/lib.rs", + ''' let config_path = cmdline + .as_deref() + .and_then(extract_config_path) + .or_else(|| self.explicit_config.clone()); +''', + ''' let config_path = self.explicit_config.clone().or_else(|| { + cmdline + .as_deref() + .and_then(extract_config_path) + }); +''', +) +replace_once( + "crates/rigos-xmrig/src/lib.rs", + ''' algorithm: raw.get("algo").and_then(Value::as_str).map(str::to_owned), + huge_pages_requested: raw + .get("randomx") + .and_then(|v| v.get("huge-pages")) + .and_then(Value::as_bool), + thread_hint: raw.get("threads").and_then(Value::as_u64), +''', + ''' algorithm: raw + .get("algo") + .and_then(Value::as_str) + .or_else(|| { + raw.get("pools") + .and_then(Value::as_array) + .and_then(|pools| pools.first()) + .and_then(|pool| pool.get("algo")) + .and_then(Value::as_str) + }) + .map(str::to_owned), + huge_pages_requested: raw + .get("randomx") + .and_then(|v| v.get("huge-pages")) + .and_then(Value::as_bool) + .or_else(|| raw.pointer("/cpu/huge-pages").and_then(Value::as_bool)), + thread_hint: raw + .get("threads") + .and_then(Value::as_u64) + .or_else(|| { + raw.pointer("/cpu/rx") + .and_then(Value::as_array) + .and_then(|threads| u64::try_from(threads.len()).ok()) + }) + .or_else(|| raw.pointer("/cpu/max-threads-hint").and_then(Value::as_u64)), +''', +) +replace_once( + "crates/rigos-xmrig/src/lib.rs", + ''' #[test] + fn discovers_xmrig_from_synthetic_proc_without_mutation() { +''', + ''' #[test] + fn explicit_redacted_config_overrides_process_secret_path() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-xmrig-explicit-{unique}")); + let proc_root = root.join("proc"); + let pid_dir = proc_root.join("42"); + fs::create_dir_all(&pid_dir).unwrap(); + let secret = root.join("secret.json"); + let public = root.join("public.json"); + fs::write(&secret, r#"{"pools":[{"url":"secret-pool","user":"SENTINEL_SECRET"}]}"#).unwrap(); + fs::write(&public, r#"{"cpu":{"huge-pages":true,"rx":[-1,-1]},"pools":[{"url":"public-pool","algo":"rx/0"}],"http":{"enabled":false}}"#).unwrap(); + fs::write(pid_dir.join("comm"), "xmrig\\n").unwrap(); + fs::write(pid_dir.join("cmdline"), format!("xmrig\\0--config={}\\0", secret.display())).unwrap(); + fs::write(pid_dir.join("status"), "Name:\\txmrig\\nUid:\\t1000 1000 1000 1000\\n").unwrap(); + fs::write(pid_dir.join("cgroup"), "0::/system.slice/rigos-miner.service\\n").unwrap(); + fs::write(pid_dir.join("stat"), "42 (xmrig) S 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 100 0\\n").unwrap(); + fs::write(proc_root.join("uptime"), "100.0 0.0\\n").unwrap(); + let backend = XmrigBackend { + explicit_executable: None, + explicit_config: Some(public), + probe_version: false, + }; + let result = backend.discover(&MachineContext { proc_root, sys_root: root.join("sys") }); + let _ = fs::remove_dir_all(root); + let snapshot = result.value.unwrap(); + assert_eq!(snapshot.config.pools, vec!["public-pool"]); + assert_eq!(snapshot.config.algorithm.as_deref(), Some("rx/0")); + assert_eq!(snapshot.config.huge_pages_requested, Some(true)); + assert_eq!(snapshot.config.thread_hint, Some(2)); + assert!(!serde_json::to_string(&snapshot).unwrap().contains("SENTINEL_SECRET")); + } + + #[test] + fn discovers_xmrig_from_synthetic_proc_without_mutation() { +''', +) +replace_once( + "crates/rigosd/src/lib.rs", + ''' explicit_config: cli.xmrig_config, +''', + ''' explicit_config: cli + .xmrig_config + .or_else(|| Some(PathBuf::from("/run/rigos/xmrig-public.json"))), +''', +) + +# Appliance scripts and services. +create( + "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-public-config", + r'''#!/usr/bin/python3 +import json +import os +import tempfile +from pathlib import Path + +SOURCE = Path("/var/lib/rigos/current/xmrig.json") +TARGET = Path("/run/rigos/xmrig-public.json") +MAX_BYTES = 2 * 1024 * 1024 + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def main() -> int: + raw = SOURCE.read_bytes() + if len(raw) > MAX_BYTES: + raise RuntimeError("XMRig configuration exceeds the public-view size limit") + value = json.loads(raw) + if not isinstance(value, dict): + raise RuntimeError("XMRig configuration is not an object") + for pool in value.get("pools", []): + if isinstance(pool, dict): + pool.pop("user", None) + pool.pop("pass", None) + http = value.get("http") + if isinstance(http, dict): + http.pop("access-token", None) + value["rigos-public-view"] = { + "schema": "rigos.xmrig-public-config/v1", + "source": "active_revision", + "identity_redacted": True, + } + TARGET.parent.mkdir(mode=0o755, parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=TARGET.parent, prefix=".xmrig-public-", delete=False + ) as stream: + json.dump(value, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o644) + os.replace(temporary, TARGET) + fsync_directory(TARGET.parent) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +''', + 0o755, +) +create( + "build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-identity", + r'''#!/usr/bin/python3 +import hashlib +import json +import os +import stat +import subprocess +import tempfile +from pathlib import Path + +RUNTIME = Path("/run/rigos") +STATE = Path("/var/lib/rigos") +SSH_DIRECTORY = Path("/etc/ssh") +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +STORE = STATE / "machine" / "ssh-host-keys" +STATUS = RUNTIME / "ssh-identity-status.json" +KEY_TYPES = ("rsa", "ecdsa", "ed25519") +MAX_KEY_BYTES = 1024 * 1024 + + +def key_paths(root: Path): + result = [] + for key_type in KEY_TYPES: + private = root / f"ssh_host_{key_type}_key" + result.append((private, 0o600)) + result.append((Path(f"{private}.pub"), 0o644)) + return result + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def ensure_directory(path: Path, mode: int) -> None: + path.mkdir(mode=mode, parents=True, exist_ok=True) + info = path.lstat() + if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode): + raise RuntimeError(f"unsafe directory: {path}") + if info.st_uid != os.geteuid(): + raise RuntimeError(f"directory owner mismatch: {path}") + path.chmod(mode) + + +def valid_key(path: Path, mode: int) -> bool: + try: + info = path.lstat() + except FileNotFoundError: + return False + return ( + stat.S_ISREG(info.st_mode) + and not stat.S_ISLNK(info.st_mode) + and info.st_uid == os.geteuid() + and stat.S_IMODE(info.st_mode) == mode + and 0 < info.st_size <= MAX_KEY_BYTES + ) + + +def key_set_state(root: Path) -> str: + try: + info = root.lstat() + except FileNotFoundError: + return "absent" + if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid != os.geteuid() or stat.S_IMODE(info.st_mode) != 0o700: + return "invalid" + expected = {path.name for path, _mode in key_paths(root)} + observed = {entry.name for entry in root.iterdir()} + if not observed: + return "absent" + if observed != expected: + return "invalid" + return "complete" if all(valid_key(path, mode) for path, mode in key_paths(root)) else "invalid" + + +def atomic_copy(source: Path, destination: Path, mode: int) -> None: + data = source.read_bytes() + if not data or len(data) > MAX_KEY_BYTES: + raise RuntimeError(f"invalid SSH host key size: {source}") + descriptor, name = tempfile.mkstemp(prefix=f".{destination.name}-", dir=destination.parent) + temporary = Path(name) + try: + os.fchmod(descriptor, mode) + with os.fdopen(descriptor, "wb", closefd=True) as stream: + stream.write(data) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, destination) + destination.chmod(mode) + fsync_directory(destination.parent) + finally: + temporary.unlink(missing_ok=True) + + +def live_keys_ready() -> bool: + return all(valid_key(path, mode) for path, mode in key_paths(SSH_DIRECTORY)) + + +def remove_live_keys() -> None: + for path, _mode in key_paths(SSH_DIRECTORY): + path.unlink(missing_ok=True) + + +def generate_live_keys() -> None: + SSH_DIRECTORY.mkdir(mode=0o755, parents=True, exist_ok=True) + remove_live_keys() + result = subprocess.run(["/usr/bin/ssh-keygen", "-A"], check=False) + if result.returncode != 0 or not live_keys_ready(): + raise RuntimeError("SSH host key generation failed") + + +def persist_live_keys() -> None: + ensure_directory(STORE, 0o700) + for source, mode in key_paths(SSH_DIRECTORY): + if not valid_key(source, mode): + raise RuntimeError(f"live SSH host key is invalid: {source}") + atomic_copy(source, STORE / source.name, mode) + if key_set_state(STORE) != "complete": + raise RuntimeError("persistent SSH host key store is incomplete") + + +def restore_live_keys() -> None: + if key_set_state(STORE) != "complete": + raise RuntimeError("persistent SSH host key store is unavailable") + SSH_DIRECTORY.mkdir(mode=0o755, parents=True, exist_ok=True) + remove_live_keys() + for source, mode in key_paths(STORE): + atomic_copy(source, SSH_DIRECTORY / source.name, mode) + if not live_keys_ready(): + raise RuntimeError("restored SSH host keys failed validation") + + +def write_status(value: dict) -> None: + RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=RUNTIME, prefix=".ssh-identity-", delete=False) as stream: + json.dump(value, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o644) + os.replace(temporary, STATUS) + fsync_directory(RUNTIME) + + +def main() -> int: + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + try: + state_status = json.loads((RUNTIME / "state-status.json").read_text(encoding="utf-8")) + if state_status.get("schema") != "rigos.state-status/v1" or state_status.get("boot_id") != boot_id or state_status.get("outcome") != "ready": + raise RuntimeError("persistent state is not ready for this boot") + persistent_state = key_set_state(STORE) + if persistent_state == "complete": + restore_live_keys() + action = "restored" + elif persistent_state == "absent": + generate_live_keys() + persist_live_keys() + action = "created" + else: + raise RuntimeError("persistent SSH host key store is incomplete or unsafe") + fingerprints = { + key_type: hashlib.sha256((SSH_DIRECTORY / f"ssh_host_{key_type}_key.pub").read_bytes()).hexdigest() + for key_type in KEY_TYPES + } + write_status({"schema":"rigos.ssh-identity-status/v1","boot_id":boot_id,"outcome":"ready","action":action,"persisted":True,"public_key_sha256":fingerprints,"reason":None}) + return 0 + except (OSError, ValueError, json.JSONDecodeError, RuntimeError) as error: + write_status({"schema":"rigos.ssh-identity-status/v1","boot_id":boot_id,"outcome":"error","action":None,"persisted":False,"public_key_sha256":{},"reason":str(error)}) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) +''', + 0o755, +) +create( + "build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe", + r'''#!/usr/bin/python3 +import json +import os +import subprocess +import tempfile +from pathlib import Path + +RUNTIME = Path("/run/rigos") +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +STATUS = RUNTIME / "recovery-access-status.json" +IDENTITY_STATUS = RUNTIME / "ssh-identity-status.json" +SSH_PORT = 22 + + +def unit_state(action: str, name: str) -> bool: + return subprocess.run(["/usr/bin/systemctl", action, "--quiet", name], check=False).returncode == 0 + + +def tcp_listener(path: Path, port: int) -> bool: + try: + lines = path.read_text(encoding="ascii").splitlines()[1:] + except OSError: + return False + for line in lines: + fields = line.split() + if len(fields) < 4 or fields[3] != "0A": + continue + try: + observed_port = int(fields[1].rsplit(":", 1)[1], 16) + except (IndexError, ValueError): + continue + if observed_port == port: + return True + return False + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def main() -> int: + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + status = json.loads(STATUS.read_text(encoding="utf-8")) + identity = json.loads(IDENTITY_STATUS.read_text(encoding="utf-8")) + if status.get("boot_id") != boot_id or identity.get("boot_id") != boot_id: + raise RuntimeError("remote access evidence belongs to another boot") + enabled = unit_state("is-enabled", "ssh.service") + active = unit_state("is-active", "ssh.service") + listener_ipv4 = tcp_listener(Path("/proc/net/tcp"), SSH_PORT) + listener_ipv6 = tcp_listener(Path("/proc/net/tcp6"), SSH_PORT) + listening = listener_ipv4 or listener_ipv6 + if enabled and active and listening: + remote_access = "active" + elif enabled and not listening: + remote_access = "enabled_no_listener" + elif listening and not active: + remote_access = "listener_without_service" + else: + remote_access = "inactive" + operational = status.get("state_outcome") == "ready" and status.get("local_console_access") is True and identity.get("outcome") == "ready" and identity.get("persisted") is True and remote_access == "active" + status.update({"mode":"operational" if operational else "recovery","remote_access":remote_access,"remote_protocol":"ssh","remote_port":SSH_PORT,"ssh_service_enabled":enabled,"ssh_service_active":active,"ssh_listener_ipv4":listener_ipv4,"ssh_listener_ipv6":listener_ipv6,"ssh_host_key_action":identity.get("action"),"ssh_host_keys_persisted":identity.get("persisted") is True}) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=RUNTIME, prefix=".recovery-access-", delete=False) as stream: + json.dump(status, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o644) + os.replace(temporary, STATUS) + fsync_directory(RUNTIME) + return 0 if operational else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) +''', + 0o755, +) + +create("build/usb/includes.chroot/etc/systemd/system/rigos-ssh-identity.service", '''[Unit] +Description=Establish persistent RIGOS SSH machine identity +After=rigos-state-ready.service +Requires=rigos-state-ready.service +Before=ssh.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-ssh-identity +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +''') +create("build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service", '''[Unit] +Description=Observe RIGOS remote access truth +After=network-online.target ssh.service rigos-ssh-identity.service +Wants=network-online.target +Requires=ssh.service rigos-ssh-identity.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-remote-access-probe +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +''') +create("build/usb/includes.chroot/etc/systemd/system/rigos-miner-public-status.service", '''[Unit] +Description=Publish redacted RIGOS miner configuration truth +After=rigos-miner.service +Requires=rigos-state-ready.service +ConditionPathExists=/var/lib/rigos/xmrig.json + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-miner-public-config +''') +create("build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos.conf", '''[Unit] +Requires=rigos-ssh-identity.service +After=rigos-ssh-identity.service +''') +create("build/usb/includes.chroot/etc/ssh/sshd_config.d/rigos.conf", '''PasswordAuthentication yes +KbdInteractiveAuthentication no +PermitRootLogin no +AllowUsers rigosadmin +X11Forwarding no +AllowAgentForwarding no +AllowTcpForwarding no +PermitTunnel no +GatewayPorts no +''') + +replace_once( + "build/usb/hooks/010-rigos.chroot", + "install -d -m 0755 /usr/lib/rigos\n", + '''install -d -m 0755 /usr/lib/rigos /usr/local/bin +ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd +ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl +rm -f /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key.pub +rm -f /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key.pub +rm -f /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key.pub +''', +) +replace_once( + "build/usb/hooks/010-rigos.chroot", + "/usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig\n", + "/usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/rigos-ssh-identity /usr/lib/rigos/rigos-remote-access-probe /usr/lib/rigos/rigos-miner-public-config /usr/lib/rigos/xmrig\n", +) +replace_once( + "build/usb/hooks/010-rigos.chroot", + "systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount\n", + "systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-ssh-identity.service rigos-remote-access-status.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount\nsystemctl disable ssh.socket 2>/dev/null || true\n", +) +replace_once( + "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service", + "Wants=network-online.target\n", + "Wants=network-online.target rigos-miner-public-status.service\n", +) + +# Ordering and verification gates. +replace_once( + "scripts/verify-systemd-ordering.py", + ''' "rigos-state-ready.service", "rigos-profile-apply.service", + "rigos-firstboot.service", "rigos-hugepages.service", "rigos-miner.service", +''', + ''' "rigos-state-ready.service", "rigos-profile-apply.service", + "rigos-firstboot.service", "rigos-ssh-identity.service", + "rigos-remote-access-status.service", "rigos-miner-public-status.service", + "rigos-hugepages.service", "rigos-miner.service", +''', +) +replace_once( + "scripts/verify-systemd-ordering.py", + ' ready = units["rigos-state-ready.service"]\n', + ''' ssh_identity = units["rigos-ssh-identity.service"] + includes(ssh_identity.words("Unit", "After"), {"rigos-state-ready.service"}, "SSH identity must follow state readiness") + includes(ssh_identity.words("Unit", "Requires"), {"rigos-state-ready.service"}, "SSH identity must require state readiness") + includes(ssh_identity.words("Unit", "Before"), {"ssh.service"}, "SSH identity must precede sshd") + + remote_access = units["rigos-remote-access-status.service"] + includes(remote_access.words("Unit", "After"), {"ssh.service", "rigos-ssh-identity.service"}, "remote access truth ordering is incomplete") + includes(remote_access.words("Unit", "Requires"), {"ssh.service", "rigos-ssh-identity.service"}, "remote access truth dependencies are incomplete") + + public_status = units["rigos-miner-public-status.service"] + includes(public_status.words("Unit", "After"), {"rigos-miner.service"}, "public miner status must follow miner") + includes(public_status.words("Unit", "Requires"), {"rigos-state-ready.service"}, "public miner status must require ready state") + + ready = units["rigos-state-ready.service"] +''', +) +replace_once( + "scripts/verify.sh", + ''' build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \\ + scripts/verify-systemd-ordering.py +''', + ''' build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \\ + build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-identity \\ + build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe \\ + build/usb/includes.chroot/usr/lib/rigos/rigos-miner-public-config \\ + scripts/verify-systemd-ordering.py +''', +) +replace_once( + "scripts/verify.sh", + "grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service\n", + '''grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +grep -Fq 'rigos-miner-public-status.service' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service +grep -Fq 'Before=ssh.service' build/usb/includes.chroot/etc/systemd/system/rigos-ssh-identity.service +grep -Fq 'Requires=rigos-ssh-identity.service' build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos.conf +grep -Fq 'After=network-online.target ssh.service rigos-ssh-identity.service' build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service +grep -Fq 'ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd' build/usb/hooks/010-rigos.chroot +grep -Fq 'ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl' build/usb/hooks/010-rigos.chroot +grep -Fq 'systemctl disable ssh.socket' build/usb/hooks/010-rigos.chroot +grep -Fqx 'AllowUsers rigosadmin' build/usb/includes.chroot/etc/ssh/sshd_config.d/rigos.conf +grep -Fqx 'PermitRootLogin no' build/usb/includes.chroot/etc/ssh/sshd_config.d/rigos.conf +''', +) + +# Keep the image verifier authoritative without requiring a physical boot. +replace_once( + "scripts/verify-usb-appliance.sh", + " etc/systemd/system/rigos-recovery-access.service \\\n", + " etc/systemd/system/rigos-recovery-access.service \\\n etc/systemd/system/rigos-ssh-identity.service \\\n etc/systemd/system/rigos-remote-access-status.service \\\n etc/systemd/system/rigos-miner-public-status.service \\\n etc/systemd/system/ssh.service.d/rigos.conf \\\n etc/ssh/sshd_config.d/rigos.conf \\\n", +) +replace_once( + "scripts/verify-usb-appliance.sh", + " usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \\\n", + " usr/lib/rigos/rigosd usr/lib/rigos/rigosctl usr/local/bin/rigosd usr/local/bin/rigosctl \\\n", +) +replace_once( + "scripts/verify-usb-appliance.sh", + "usr/lib/rigos/rigos-miner-gate usr/lib/rigos/xmrig", + "usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-ssh-identity usr/lib/rigos/rigos-remote-access-probe usr/lib/rigos/rigos-miner-public-config usr/lib/rigos/xmrig", +) +replace_once( + "scripts/verify-usb-appliance.sh", + 'python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate"\n', + '''python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" +python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-ssh-identity" +python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-remote-access-probe" +python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-public-config" +''', +) +replace_once( + "scripts/verify-usb-appliance.sh", + '''rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" +[[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' +''', + '''user_path="$temporary/root/usr/local/bin:$temporary/root/usr/bin:$temporary/root/bin" +rigosd_path="$(PATH="$user_path" command -v rigosd)" +rigosctl_path="$(PATH="$user_path" command -v rigosctl)" +[[ "$rigosd_path" == "$temporary/root/usr/local/bin/rigosd" && -x "$rigosd_path" ]] || die 'rigosd is not executable in the user appliance PATH' +[[ "$rigosctl_path" == "$temporary/root/usr/local/bin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the user appliance PATH' +''', +) +replace_once( + "scripts/verify-usb-appliance.sh", + 'unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \\\n', + '''if unsquashfs -ll "$temporary/a/live/filesystem.squashfs" | grep -Eq '/etc/ssh/ssh_host_(rsa|ecdsa|ed25519)_key$'; then + die 'image contains a baked SSH private host key' +fi +unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \\ +''', +) + +# Remove the one-shot patch mechanism from the resulting source commit. +(ROOT / "scripts/apply-alpha8-physical-fixes.py").unlink() +workflow = ROOT / ".github/workflows/alpha8-apply.yml" +workflow.unlink() +print("Alpha8 physical findings applied") From d9cd9e3d86953cd1813dcf4690599276ce5df053 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:33:47 +0700 Subject: [PATCH 043/386] ci trigger alpha8 source application --- .github/workflows/alpha8-apply.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/alpha8-apply.yml b/.github/workflows/alpha8-apply.yml index b6ee4849..dbc933ca 100644 --- a/.github/workflows/alpha8-apply.yml +++ b/.github/workflows/alpha8-apply.yml @@ -5,6 +5,7 @@ on: branches: - fix/alpha8-physical-findings paths: + - .github/workflows/alpha8-apply.yml - .alpha8-patch-trigger permissions: From 739d8a531fb66590085415d2c36977a4b080b133 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:40:29 +0700 Subject: [PATCH 044/386] runtime render exact miner policy --- .../usr/lib/rigos/rigos-runtime-render | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render new file mode 100644 index 00000000..9e5a0fc2 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render @@ -0,0 +1,146 @@ +#!/usr/bin/python3 +import copy +import grp +import hashlib +import json +import os +import tempfile +from pathlib import Path + +STATE = Path(os.environ.get("RIGOS_STATE_PATH", "/var/lib/rigos")) +RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) +PRIVATE_CONFIG = RUNTIME / "xmrig.json" +PUBLIC_CONFIG = RUNTIME / "xmrig-public.json" +STATUS = RUNTIME / "runtime-config-status.json" +MAX_JSON_BYTES = 2 * 1024 * 1024 + + +def read_object(path: Path) -> dict: + raw = path.read_bytes() + if len(raw) > MAX_JSON_BYTES: + raise RuntimeError(f"JSON exceeds size limit: {path}") + value = json.loads(raw) + if not isinstance(value, dict): + raise RuntimeError(f"JSON root is not an object: {path}") + return value + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def atomic_json(path: Path, value: dict, mode: int, gid: int | None = None) -> None: + path.parent.mkdir(mode=0o755, parents=True, exist_ok=True) + descriptor, name = tempfile.mkstemp(prefix=f".{path.name}-", dir=path.parent) + temporary = Path(name) + try: + os.fchmod(descriptor, mode) + if gid is not None: + os.fchown(descriptor, 0, gid) + with os.fdopen(descriptor, "w", encoding="utf-8", closefd=True) as stream: + json.dump(value, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + path.chmod(mode) + fsync_directory(path.parent) + finally: + temporary.unlink(missing_ok=True) + + +def exact_profile_name(algorithm: str) -> str: + if algorithm in ("rx", "rx/0"): + return "rx" + if algorithm.startswith("rx/"): + return algorithm + raise RuntimeError("explicit threads require a RandomX algorithm") + + +def render() -> tuple[dict, dict, dict]: + current = (STATE / "current").resolve(strict=True) + if current.parent != (STATE / "revisions").resolve(strict=True): + raise RuntimeError("current revision is outside the revisions directory") + policy = read_object(current / "policy.json") + if policy.get("schema") != "rigos.policy/v1": + raise RuntimeError("policy schema mismatch") + sheet_name = policy.get("active_flight_sheet") + if not isinstance(sheet_name, str) or not sheet_name or "/" in sheet_name or ".." in sheet_name: + raise RuntimeError("active flight sheet name is invalid") + sheet = read_object(current / "flight-sheets" / f"{sheet_name}.json") + if sheet.get("schema") != "rigos.flight-sheet/v1" or sheet.get("backend") != "xmrig": + raise RuntimeError("flight sheet schema or backend mismatch") + committed = read_object(current / "xmrig.json") + runtime = copy.deepcopy(committed) + cpu = runtime.get("cpu") + if not isinstance(cpu, dict): + raise RuntimeError("committed XMRig CPU policy is missing") + algorithm = sheet.get("algorithm") + if not isinstance(algorithm, str) or not algorithm: + raise RuntimeError("flight sheet algorithm is invalid") + pools = runtime.get("pools") + if not isinstance(pools, list) or not pools: + raise RuntimeError("committed XMRig pools are missing") + if any(not isinstance(pool, dict) or pool.get("algo") != algorithm for pool in pools): + raise RuntimeError("committed pool algorithm disagrees with the flight sheet") + sheet_cpu = sheet.get("cpu") + if not isinstance(sheet_cpu, dict): + raise RuntimeError("flight sheet CPU policy is missing") + threads = sheet_cpu.get("threads") + profile = None + exact_threads = None + if isinstance(threads, int) and not isinstance(threads, bool): + if not 1 <= threads <= 1024: + raise RuntimeError("explicit thread count is out of range") + profile = exact_profile_name(algorithm) + cpu["max-threads-hint"] = sheet_cpu.get("max_threads_hint", 100) + cpu[profile] = [-1] * threads + exact_threads = threads + elif threads != "auto": + raise RuntimeError("threads must be auto or an integer") + public = copy.deepcopy(runtime) + for pool in public.get("pools", []): + if isinstance(pool, dict): + pool.pop("user", None) + pool.pop("pass", None) + http = public.get("http") + if isinstance(http, dict): + http.pop("access-token", None) + public["rigos-public-view"] = { + "schema": "rigos.xmrig-public-config/v1", + "identity_redacted": True, + "source_revision": current.name, + } + status = { + "schema": "rigos.runtime-config-status/v1", + "outcome": "ready", + "revision": current.name, + "algorithm": algorithm, + "thread_mode": "exact" if exact_threads is not None else "auto", + "exact_threads": exact_threads, + "profile": profile, + "private_sha256": hashlib.sha256( + (json.dumps(runtime, sort_keys=True) + "\n").encode("utf-8") + ).hexdigest(), + "identity_redacted_public_view": True, + } + return runtime, public, status + + +def main() -> int: + runtime, public, status = render() + gid = None + if os.environ.get("RIGOS_RENDER_SKIP_CHOWN") != "1": + gid = grp.getgrnam("rigos").gr_gid + atomic_json(PRIVATE_CONFIG, runtime, 0o640, gid) + atomic_json(PUBLIC_CONFIG, public, 0o644) + atomic_json(STATUS, status, 0o644) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From bed746a0eaa351ba5d37525e2ff99f521efa2fdd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:40:49 +0700 Subject: [PATCH 045/386] runtime add miner render authority --- .../system/rigos-runtime-render.service | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service new file mode 100644 index 00000000..81b1bd55 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service @@ -0,0 +1,23 @@ +[Unit] +Description=Render RIGOS runtime miner configuration +After=rigos-state-ready.service rigos-profile-apply.service +Requires=rigos-state-ready.service rigos-profile-apply.service +Before=rigos-hugepages.service rigos-miner.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-runtime-render +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ProtectSystem=strict +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +LockPersonality=yes +ReadWritePaths=/run/rigos + +[Install] +WantedBy=multi-user.target From 493cc719522fcf215db52e12cd5f605e9ee75f14 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:41:23 +0700 Subject: [PATCH 046/386] miner override runtime render config --- .../system/rigos-miner.service.d/runtime-render.conf | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf new file mode 100644 index 00000000..3f2a44ee --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf @@ -0,0 +1,10 @@ +[Unit] +After=rigos-runtime-render.service +Requires=rigos-runtime-render.service +ConditionPathExists= +ConditionPathExists=/run/rigos/xmrig.json + +[Service] +ExecCondition=/usr/lib/rigos/rigos-runtime-gate +ExecStart= +ExecStart=/usr/lib/rigos/xmrig --config=/run/rigos/xmrig.json From 3c4e19ad16d2bab164fd4e6e381dbd5bb9896590 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:41:58 +0700 Subject: [PATCH 047/386] runtime render wait for committed revision --- .../etc/systemd/system/rigos-runtime-render.service | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service index 81b1bd55..166dfc49 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service @@ -3,6 +3,7 @@ Description=Render RIGOS runtime miner configuration After=rigos-state-ready.service rigos-profile-apply.service Requires=rigos-state-ready.service rigos-profile-apply.service Before=rigos-hugepages.service rigos-miner.service +ConditionPathExists=/var/lib/rigos/current [Service] Type=oneshot From 8eef1954169eb03854c133a4e6329fb1f4d0f226 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:42:18 +0700 Subject: [PATCH 048/386] miner gate rendered runtime truth --- .../usr/lib/rigos/rigos-runtime-gate | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate new file mode 100644 index 00000000..c028d554 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate @@ -0,0 +1,66 @@ +#!/usr/bin/python3 +import argparse +import json +import sys +from pathlib import Path + +MAX_JSON_BYTES = 2 * 1024 * 1024 + + +def emit(outcome: str, reason: str | None = None) -> None: + value = { + "schema": "rigos.runtime-gate/v1", + "outcome": outcome, + "reason": reason, + } + print(json.dumps(value, sort_keys=True), file=sys.stdout if outcome == "allowed" else sys.stderr) + + +def read_object(path: Path) -> dict: + raw = path.read_bytes() + if len(raw) > MAX_JSON_BYTES: + raise ValueError("JSON exceeds size limit") + value = json.loads(raw) + if not isinstance(value, dict): + raise ValueError("JSON root is not an object") + return value + + +def deny(reason: str) -> int: + emit("denied", reason) + return 2 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--state", type=Path, default=Path("/var/lib/rigos")) + parser.add_argument("--runtime", type=Path, default=Path("/run/rigos")) + args = parser.parse_args() + try: + revision = (args.state / "current").resolve(strict=True).name + status = read_object(args.runtime / "runtime-config-status.json") + config = read_object(args.runtime / "xmrig.json") + except (OSError, ValueError, json.JSONDecodeError): + return deny("runtime_truth_unreadable") + if status.get("schema") != "rigos.runtime-config-status/v1" or status.get("outcome") != "ready": + return deny("runtime_status_invalid") + if status.get("revision") != revision: + return deny("runtime_revision_stale") + pools = config.get("pools") + cpu = config.get("cpu") + if not isinstance(pools, list) or not pools or not isinstance(cpu, dict): + return deny("runtime_config_invalid") + exact_threads = status.get("exact_threads") + profile = status.get("profile") + if exact_threads is not None: + if not isinstance(exact_threads, int) or exact_threads < 1 or not isinstance(profile, str): + return deny("runtime_thread_truth_invalid") + entries = cpu.get(profile) + if not isinstance(entries, list) or len(entries) != exact_threads: + return deny("runtime_thread_profile_mismatch") + emit("allowed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From d8d50504f4889574966984554e8180ae3b4fb06d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:43:36 +0700 Subject: [PATCH 049/386] image wire runtime authority and user CLI --- build/usb/hooks/010-rigos.chroot | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index e29c5d1d..f7e37ac5 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -5,11 +5,14 @@ useradd --system --home-dir /var/lib/rigos --shell /usr/sbin/nologin --user-grou useradd --create-home --shell /bin/bash --groups sudo rigosadmin passwd --lock rigosadmin install -d -o rigos -g rigos -m 0750 /var/lib/rigos -install -d -m 0755 /usr/lib/rigos +install -d -m 0755 /usr/lib/rigos /usr/local/bin +ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd +ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount +systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true From ab2baf5aed0ff5f828291073a33fa10087fb1c80 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:43:51 +0700 Subject: [PATCH 050/386] remote access report listener truth --- .../usr/lib/rigos/rigos-remote-access-probe | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe b/build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe new file mode 100644 index 00000000..b7b2d004 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe @@ -0,0 +1,99 @@ +#!/usr/bin/python3 +import json +import os +import subprocess +import tempfile +from pathlib import Path + +RUNTIME = Path("/run/rigos") +STATUS = RUNTIME / "recovery-access-status.json" +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +SSH_PORT = 22 + + +def unit_state(action: str, unit: str) -> bool: + return subprocess.run( + ["/usr/bin/systemctl", action, "--quiet", unit], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).returncode == 0 + + +def has_listener(path: Path, port: int) -> bool: + try: + lines = path.read_text(encoding="ascii").splitlines()[1:] + except OSError: + return False + for line in lines: + fields = line.split() + if len(fields) < 4 or fields[3] != "0A": + continue + try: + observed = int(fields[1].rsplit(":", 1)[1], 16) + except (IndexError, ValueError): + continue + if observed == port: + return True + return False + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def main() -> int: + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + status = json.loads(STATUS.read_text(encoding="utf-8")) + if status.get("boot_id") != boot_id: + raise RuntimeError("recovery access status belongs to another boot") + enabled = unit_state("is-enabled", "ssh.service") + active = unit_state("is-active", "ssh.service") + ipv4 = has_listener(Path("/proc/net/tcp"), SSH_PORT) + ipv6 = has_listener(Path("/proc/net/tcp6"), SSH_PORT) + listening = ipv4 or ipv6 + if enabled and active and listening: + remote_access = "active" + elif enabled and not listening: + remote_access = "enabled_no_listener" + elif listening and not active: + remote_access = "listener_without_service" + else: + remote_access = "inactive" + operational = ( + status.get("state_outcome") == "ready" + and status.get("local_console_access") is True + and remote_access == "active" + ) + status.update( + { + "mode": "operational" if operational else "recovery", + "remote_access": remote_access, + "remote_protocol": "ssh", + "remote_port": SSH_PORT, + "ssh_service_enabled": enabled, + "ssh_service_active": active, + "ssh_listener_ipv4": ipv4, + "ssh_listener_ipv6": ipv6, + } + ) + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", dir=RUNTIME, prefix=".recovery-access-", delete=False + ) as stream: + json.dump(status, stream, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + temporary.chmod(0o644) + os.replace(temporary, STATUS) + fsync_directory(RUNTIME) + return 0 if operational else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 2a20d1537d646adc2899c8e084961fc4d7e52062 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:44:06 +0700 Subject: [PATCH 051/386] remote access add truth service --- .../system/rigos-remote-access-status.service | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service b/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service new file mode 100644 index 00000000..9d876dfb --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service @@ -0,0 +1,13 @@ +[Unit] +Description=Observe RIGOS remote access truth +After=network-online.target ssh.service rigos-recovery-access.service +Wants=network-online.target +Requires=ssh.service rigos-recovery-access.service + +[Service] +Type=oneshot +ExecStart=/usr/lib/rigos/rigos-remote-access-probe +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target From a6f491d552e2629b44799a2c55f33eaf9fdb9466 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:45:19 +0700 Subject: [PATCH 052/386] remote access add observer unit --- .../systemd/system/rigos-remote-access-observe.service | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-observe.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-observe.service b/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-observe.service new file mode 100644 index 00000000..8f0afe23 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-observe.service @@ -0,0 +1,10 @@ +[Unit] +Description=Observe RIGOS SSH listener truth +After=network-online.target ssh.service rigos-recovery-access.service +Wants=network-online.target +Requires=ssh.service rigos-recovery-access.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-remote-access-probe +RemainAfterExit=yes From 1b097513f4eebe372392d7ca14340b3be5d02aea Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:45:38 +0700 Subject: [PATCH 053/386] ssh pull remote truth observer --- .../etc/systemd/system/ssh.service.d/rigos-observe.conf | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf diff --git a/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf b/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf new file mode 100644 index 00000000..d00954c6 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf @@ -0,0 +1,3 @@ +[Unit] +After=rigos-recovery-access.service +Wants=rigos-remote-access-observe.service From 80065d04d4f7da027fd25787665b53c3af33121d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:47:28 +0700 Subject: [PATCH 054/386] remove unused workflow --- .github/workflows/alpha8-apply.yml | 44 ------------------------------ 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/alpha8-apply.yml diff --git a/.github/workflows/alpha8-apply.yml b/.github/workflows/alpha8-apply.yml deleted file mode 100644 index dbc933ca..00000000 --- a/.github/workflows/alpha8-apply.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Alpha8 Physical Findings - -on: - push: - branches: - - fix/alpha8-physical-findings - paths: - - .github/workflows/alpha8-apply.yml - - .alpha8-patch-trigger - -permissions: - contents: write - -jobs: - apply-and-verify: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - name: Check out branch - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true - - - name: Apply exact source patch - run: python3 scripts/apply-alpha8-physical-fixes.py - - - name: Install pinned Rust toolchain - uses: dtolnay/rust-toolchain@1.85.1 - with: - components: rustfmt, clippy - - - name: Verify source - run: ./scripts/verify.sh - - - name: Commit verified source - run: | - rm -f .alpha8-patch-trigger - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git diff --cached --check - git commit -m "alpha8 harden physical appliance findings" - git push origin HEAD:fix/alpha8-physical-findings From 73a3020eec34e860498a5ed0665b757ea07fe14b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:47:47 +0700 Subject: [PATCH 055/386] remove unused source applicator --- scripts/apply-alpha8-physical-fixes.py | 814 ------------------------- 1 file changed, 814 deletions(-) delete mode 100644 scripts/apply-alpha8-physical-fixes.py diff --git a/scripts/apply-alpha8-physical-fixes.py b/scripts/apply-alpha8-physical-fixes.py deleted file mode 100644 index 5fa09d9d..00000000 --- a/scripts/apply-alpha8-physical-fixes.py +++ /dev/null @@ -1,814 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import os -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] - - -def read(path: str) -> str: - return (ROOT / path).read_text(encoding="utf-8") - - -def write(path: str, content: str, mode: int | None = None) -> None: - target = ROOT / path - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(content, encoding="utf-8", newline="\n") - if mode is not None: - target.chmod(mode) - - -def replace_once(path: str, old: str, new: str) -> None: - content = read(path) - count = content.count(old) - if count != 1: - raise SystemExit(f"{path}: expected one patch anchor, found {count}") - write(path, content.replace(old, new, 1)) - - -def create(path: str, content: str, mode: int | None = None) -> None: - target = ROOT / path - if target.exists(): - raise SystemExit(f"refusing to overwrite existing file: {path}") - write(path, content, mode) - - -# Exact RandomX thread intent. max-threads-hint remains a percentage hint. -replace_once( - "crates/rigos-config/src/lib.rs", - "pub fn build_runtime(\n", - '''fn exact_cpu_profile_name(algorithm: &str) -> Option<&str> { - if matches!(algorithm, "rx" | "rx/0") { - Some("rx") - } else if algorithm.starts_with("rx/") { - Some(algorithm) - } else { - None - } -} - -fn exact_cpu_profile(count: u16) -> Value { - Value::Array((0..count).map(|_| json!(-1)).collect()) -} - -pub fn build_runtime( -''', -) -replace_once( - "crates/rigos-config/src/lib.rs", - ''' match &sheet.cpu.threads { - Threads::Auto(value) if value == "auto" => {} - Threads::Count(value) if (1..=1024).contains(value) => {} - _ => { - return Err(error( - "RIGOS_FLIGHT_SHEET_INVALID", - Some(filename), - None, - Some("cpu.threads"), - "threads must be auto or 1 through 1024", - )); - } - } -''', - ''' match &sheet.cpu.threads { - Threads::Auto(value) if value == "auto" => {} - Threads::Count(value) if !(1..=1024).contains(value) => { - return Err(error( - "RIGOS_FLIGHT_SHEET_INVALID", - Some(filename), - None, - Some("cpu.threads"), - "threads must be auto or 1 through 1024", - )); - } - Threads::Count(_) if exact_cpu_profile_name(&sheet.algorithm).is_none() => { - return Err(error( - "RIGOS_FLIGHT_SHEET_INVALID", - Some(filename), - None, - Some("cpu.threads"), - "explicit threads currently require a RandomX algorithm", - )); - } - Threads::Count(_) => {} - _ => { - return Err(error( - "RIGOS_FLIGHT_SHEET_INVALID", - Some(filename), - None, - Some("cpu.threads"), - "threads must be auto or 1 through 1024", - )); - } - } -''', -) -replace_once( - "crates/rigos-config/src/lib.rs", - ''' let mut cpu = Map::from_iter([ - ("enabled".into(), Value::Bool(true)), - ("huge-pages".into(), Value::Bool(sheet.cpu.huge_pages)), - ]); - cpu.insert("max-threads-hint".into(), json!(sheet.cpu.max_threads_hint)); - if let Threads::Count(count) = sheet.cpu.threads { - cpu.insert("max-threads-hint".into(), json!(count)); - } - let xmrig = json!({"autosave":false,"background":false,"cpu":cpu,"pools":pools,"api":{"worker-id":worker},"http":{"enabled":false}}); -''', - ''' let mut cpu = Map::from_iter([ - ("enabled".into(), Value::Bool(true)), - ("huge-pages".into(), Value::Bool(sheet.cpu.huge_pages)), - ]); - cpu.insert("max-threads-hint".into(), json!(sheet.cpu.max_threads_hint)); - if let Threads::Count(count) = &sheet.cpu.threads { - let profile_name = exact_cpu_profile_name(&sheet.algorithm).ok_or_else(|| { - error( - "RIGOS_FLIGHT_SHEET_INVALID", - None, - None, - Some("cpu.threads"), - "explicit threads do not have a safe XMRig profile mapping", - ) - })?; - cpu.insert(profile_name.into(), exact_cpu_profile(*count)); - } - let xmrig = json!({"autosave":false,"background":false,"cpu":cpu,"pools":pools,"api":{"worker-id":worker},"http":{"enabled":false}}); -''', -) -replace_once( - "crates/rigos-config/src/lib.rs", - ''' #[test] - fn huge_pages_false_reaches_runtime_config() { -''', - ''' #[test] - fn explicit_randomx_threads_generate_exact_profile() { - let proposal = Proposal { - schema: "rigos.config-proposal/v1".into(), - profile: parse_rig_profile(&profile("native", "FLIGHT_REF=xmr-ssl\\n")).unwrap(), - flight_sheet: FlightSheet { - schema: "rigos.flight-sheet/v1".into(), - name: "xmr-ssl".into(), - coin: "XMR".into(), - backend: "xmrig".into(), - algorithm: "rx/0".into(), - pools: vec![Pool { - host: "pool.example".into(), - port: 443, - tls: true, - priority: 0, - }], - identity_ref: "main-xmr".into(), - worker_template: "{node_name}".into(), - cpu: CpuPolicy { - threads: Threads::Count(2), - huge_pages: true, - max_threads_hint: 100, - }, - }, - provenance: None, - source_sha256: "x".into(), - }; - let identity = IdentityRecord { - schema: "rigos.identity/v1".into(), - alias: "main-xmr".into(), - kind: "mining_identity".into(), - value: "private-value".into(), - created_locally: true, - }; - let (_, xmrig) = build_runtime(&proposal, &identity).unwrap(); - assert_eq!(xmrig["cpu"]["max-threads-hint"], 100); - assert_eq!(xmrig["cpu"]["rx"], json!([-1, -1])); - - let mut unsupported = proposal; - unsupported.flight_sheet.algorithm = "cn/r".into(); - assert!(build_runtime(&unsupported, &identity).is_err()); - } - - #[test] - fn huge_pages_false_reaches_runtime_config() { -''', -) - -# Unprivileged inspection must use a redacted derived config, never the wallet-bearing runtime file. -replace_once( - "crates/rigos-xmrig/src/lib.rs", - ''' let config_path = cmdline - .as_deref() - .and_then(extract_config_path) - .or_else(|| self.explicit_config.clone()); -''', - ''' let config_path = self.explicit_config.clone().or_else(|| { - cmdline - .as_deref() - .and_then(extract_config_path) - }); -''', -) -replace_once( - "crates/rigos-xmrig/src/lib.rs", - ''' algorithm: raw.get("algo").and_then(Value::as_str).map(str::to_owned), - huge_pages_requested: raw - .get("randomx") - .and_then(|v| v.get("huge-pages")) - .and_then(Value::as_bool), - thread_hint: raw.get("threads").and_then(Value::as_u64), -''', - ''' algorithm: raw - .get("algo") - .and_then(Value::as_str) - .or_else(|| { - raw.get("pools") - .and_then(Value::as_array) - .and_then(|pools| pools.first()) - .and_then(|pool| pool.get("algo")) - .and_then(Value::as_str) - }) - .map(str::to_owned), - huge_pages_requested: raw - .get("randomx") - .and_then(|v| v.get("huge-pages")) - .and_then(Value::as_bool) - .or_else(|| raw.pointer("/cpu/huge-pages").and_then(Value::as_bool)), - thread_hint: raw - .get("threads") - .and_then(Value::as_u64) - .or_else(|| { - raw.pointer("/cpu/rx") - .and_then(Value::as_array) - .and_then(|threads| u64::try_from(threads.len()).ok()) - }) - .or_else(|| raw.pointer("/cpu/max-threads-hint").and_then(Value::as_u64)), -''', -) -replace_once( - "crates/rigos-xmrig/src/lib.rs", - ''' #[test] - fn discovers_xmrig_from_synthetic_proc_without_mutation() { -''', - ''' #[test] - fn explicit_redacted_config_overrides_process_secret_path() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!("rigos-xmrig-explicit-{unique}")); - let proc_root = root.join("proc"); - let pid_dir = proc_root.join("42"); - fs::create_dir_all(&pid_dir).unwrap(); - let secret = root.join("secret.json"); - let public = root.join("public.json"); - fs::write(&secret, r#"{"pools":[{"url":"secret-pool","user":"SENTINEL_SECRET"}]}"#).unwrap(); - fs::write(&public, r#"{"cpu":{"huge-pages":true,"rx":[-1,-1]},"pools":[{"url":"public-pool","algo":"rx/0"}],"http":{"enabled":false}}"#).unwrap(); - fs::write(pid_dir.join("comm"), "xmrig\\n").unwrap(); - fs::write(pid_dir.join("cmdline"), format!("xmrig\\0--config={}\\0", secret.display())).unwrap(); - fs::write(pid_dir.join("status"), "Name:\\txmrig\\nUid:\\t1000 1000 1000 1000\\n").unwrap(); - fs::write(pid_dir.join("cgroup"), "0::/system.slice/rigos-miner.service\\n").unwrap(); - fs::write(pid_dir.join("stat"), "42 (xmrig) S 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 100 0\\n").unwrap(); - fs::write(proc_root.join("uptime"), "100.0 0.0\\n").unwrap(); - let backend = XmrigBackend { - explicit_executable: None, - explicit_config: Some(public), - probe_version: false, - }; - let result = backend.discover(&MachineContext { proc_root, sys_root: root.join("sys") }); - let _ = fs::remove_dir_all(root); - let snapshot = result.value.unwrap(); - assert_eq!(snapshot.config.pools, vec!["public-pool"]); - assert_eq!(snapshot.config.algorithm.as_deref(), Some("rx/0")); - assert_eq!(snapshot.config.huge_pages_requested, Some(true)); - assert_eq!(snapshot.config.thread_hint, Some(2)); - assert!(!serde_json::to_string(&snapshot).unwrap().contains("SENTINEL_SECRET")); - } - - #[test] - fn discovers_xmrig_from_synthetic_proc_without_mutation() { -''', -) -replace_once( - "crates/rigosd/src/lib.rs", - ''' explicit_config: cli.xmrig_config, -''', - ''' explicit_config: cli - .xmrig_config - .or_else(|| Some(PathBuf::from("/run/rigos/xmrig-public.json"))), -''', -) - -# Appliance scripts and services. -create( - "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-public-config", - r'''#!/usr/bin/python3 -import json -import os -import tempfile -from pathlib import Path - -SOURCE = Path("/var/lib/rigos/current/xmrig.json") -TARGET = Path("/run/rigos/xmrig-public.json") -MAX_BYTES = 2 * 1024 * 1024 - - -def fsync_directory(path: Path) -> None: - descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) - try: - os.fsync(descriptor) - finally: - os.close(descriptor) - - -def main() -> int: - raw = SOURCE.read_bytes() - if len(raw) > MAX_BYTES: - raise RuntimeError("XMRig configuration exceeds the public-view size limit") - value = json.loads(raw) - if not isinstance(value, dict): - raise RuntimeError("XMRig configuration is not an object") - for pool in value.get("pools", []): - if isinstance(pool, dict): - pool.pop("user", None) - pool.pop("pass", None) - http = value.get("http") - if isinstance(http, dict): - http.pop("access-token", None) - value["rigos-public-view"] = { - "schema": "rigos.xmrig-public-config/v1", - "source": "active_revision", - "identity_redacted": True, - } - TARGET.parent.mkdir(mode=0o755, parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile( - mode="w", encoding="utf-8", dir=TARGET.parent, prefix=".xmrig-public-", delete=False - ) as stream: - json.dump(value, stream, sort_keys=True) - stream.write("\n") - stream.flush() - os.fsync(stream.fileno()) - temporary = Path(stream.name) - temporary.chmod(0o644) - os.replace(temporary, TARGET) - fsync_directory(TARGET.parent) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) -''', - 0o755, -) -create( - "build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-identity", - r'''#!/usr/bin/python3 -import hashlib -import json -import os -import stat -import subprocess -import tempfile -from pathlib import Path - -RUNTIME = Path("/run/rigos") -STATE = Path("/var/lib/rigos") -SSH_DIRECTORY = Path("/etc/ssh") -BOOT_ID = Path("/proc/sys/kernel/random/boot_id") -STORE = STATE / "machine" / "ssh-host-keys" -STATUS = RUNTIME / "ssh-identity-status.json" -KEY_TYPES = ("rsa", "ecdsa", "ed25519") -MAX_KEY_BYTES = 1024 * 1024 - - -def key_paths(root: Path): - result = [] - for key_type in KEY_TYPES: - private = root / f"ssh_host_{key_type}_key" - result.append((private, 0o600)) - result.append((Path(f"{private}.pub"), 0o644)) - return result - - -def fsync_directory(path: Path) -> None: - descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) - try: - os.fsync(descriptor) - finally: - os.close(descriptor) - - -def ensure_directory(path: Path, mode: int) -> None: - path.mkdir(mode=mode, parents=True, exist_ok=True) - info = path.lstat() - if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode): - raise RuntimeError(f"unsafe directory: {path}") - if info.st_uid != os.geteuid(): - raise RuntimeError(f"directory owner mismatch: {path}") - path.chmod(mode) - - -def valid_key(path: Path, mode: int) -> bool: - try: - info = path.lstat() - except FileNotFoundError: - return False - return ( - stat.S_ISREG(info.st_mode) - and not stat.S_ISLNK(info.st_mode) - and info.st_uid == os.geteuid() - and stat.S_IMODE(info.st_mode) == mode - and 0 < info.st_size <= MAX_KEY_BYTES - ) - - -def key_set_state(root: Path) -> str: - try: - info = root.lstat() - except FileNotFoundError: - return "absent" - if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid != os.geteuid() or stat.S_IMODE(info.st_mode) != 0o700: - return "invalid" - expected = {path.name for path, _mode in key_paths(root)} - observed = {entry.name for entry in root.iterdir()} - if not observed: - return "absent" - if observed != expected: - return "invalid" - return "complete" if all(valid_key(path, mode) for path, mode in key_paths(root)) else "invalid" - - -def atomic_copy(source: Path, destination: Path, mode: int) -> None: - data = source.read_bytes() - if not data or len(data) > MAX_KEY_BYTES: - raise RuntimeError(f"invalid SSH host key size: {source}") - descriptor, name = tempfile.mkstemp(prefix=f".{destination.name}-", dir=destination.parent) - temporary = Path(name) - try: - os.fchmod(descriptor, mode) - with os.fdopen(descriptor, "wb", closefd=True) as stream: - stream.write(data) - stream.flush() - os.fsync(stream.fileno()) - os.replace(temporary, destination) - destination.chmod(mode) - fsync_directory(destination.parent) - finally: - temporary.unlink(missing_ok=True) - - -def live_keys_ready() -> bool: - return all(valid_key(path, mode) for path, mode in key_paths(SSH_DIRECTORY)) - - -def remove_live_keys() -> None: - for path, _mode in key_paths(SSH_DIRECTORY): - path.unlink(missing_ok=True) - - -def generate_live_keys() -> None: - SSH_DIRECTORY.mkdir(mode=0o755, parents=True, exist_ok=True) - remove_live_keys() - result = subprocess.run(["/usr/bin/ssh-keygen", "-A"], check=False) - if result.returncode != 0 or not live_keys_ready(): - raise RuntimeError("SSH host key generation failed") - - -def persist_live_keys() -> None: - ensure_directory(STORE, 0o700) - for source, mode in key_paths(SSH_DIRECTORY): - if not valid_key(source, mode): - raise RuntimeError(f"live SSH host key is invalid: {source}") - atomic_copy(source, STORE / source.name, mode) - if key_set_state(STORE) != "complete": - raise RuntimeError("persistent SSH host key store is incomplete") - - -def restore_live_keys() -> None: - if key_set_state(STORE) != "complete": - raise RuntimeError("persistent SSH host key store is unavailable") - SSH_DIRECTORY.mkdir(mode=0o755, parents=True, exist_ok=True) - remove_live_keys() - for source, mode in key_paths(STORE): - atomic_copy(source, SSH_DIRECTORY / source.name, mode) - if not live_keys_ready(): - raise RuntimeError("restored SSH host keys failed validation") - - -def write_status(value: dict) -> None: - RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) - with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=RUNTIME, prefix=".ssh-identity-", delete=False) as stream: - json.dump(value, stream, sort_keys=True) - stream.write("\n") - stream.flush() - os.fsync(stream.fileno()) - temporary = Path(stream.name) - temporary.chmod(0o644) - os.replace(temporary, STATUS) - fsync_directory(RUNTIME) - - -def main() -> int: - boot_id = BOOT_ID.read_text(encoding="ascii").strip() - try: - state_status = json.loads((RUNTIME / "state-status.json").read_text(encoding="utf-8")) - if state_status.get("schema") != "rigos.state-status/v1" or state_status.get("boot_id") != boot_id or state_status.get("outcome") != "ready": - raise RuntimeError("persistent state is not ready for this boot") - persistent_state = key_set_state(STORE) - if persistent_state == "complete": - restore_live_keys() - action = "restored" - elif persistent_state == "absent": - generate_live_keys() - persist_live_keys() - action = "created" - else: - raise RuntimeError("persistent SSH host key store is incomplete or unsafe") - fingerprints = { - key_type: hashlib.sha256((SSH_DIRECTORY / f"ssh_host_{key_type}_key.pub").read_bytes()).hexdigest() - for key_type in KEY_TYPES - } - write_status({"schema":"rigos.ssh-identity-status/v1","boot_id":boot_id,"outcome":"ready","action":action,"persisted":True,"public_key_sha256":fingerprints,"reason":None}) - return 0 - except (OSError, ValueError, json.JSONDecodeError, RuntimeError) as error: - write_status({"schema":"rigos.ssh-identity-status/v1","boot_id":boot_id,"outcome":"error","action":None,"persisted":False,"public_key_sha256":{},"reason":str(error)}) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) -''', - 0o755, -) -create( - "build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe", - r'''#!/usr/bin/python3 -import json -import os -import subprocess -import tempfile -from pathlib import Path - -RUNTIME = Path("/run/rigos") -BOOT_ID = Path("/proc/sys/kernel/random/boot_id") -STATUS = RUNTIME / "recovery-access-status.json" -IDENTITY_STATUS = RUNTIME / "ssh-identity-status.json" -SSH_PORT = 22 - - -def unit_state(action: str, name: str) -> bool: - return subprocess.run(["/usr/bin/systemctl", action, "--quiet", name], check=False).returncode == 0 - - -def tcp_listener(path: Path, port: int) -> bool: - try: - lines = path.read_text(encoding="ascii").splitlines()[1:] - except OSError: - return False - for line in lines: - fields = line.split() - if len(fields) < 4 or fields[3] != "0A": - continue - try: - observed_port = int(fields[1].rsplit(":", 1)[1], 16) - except (IndexError, ValueError): - continue - if observed_port == port: - return True - return False - - -def fsync_directory(path: Path) -> None: - descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) - try: - os.fsync(descriptor) - finally: - os.close(descriptor) - - -def main() -> int: - boot_id = BOOT_ID.read_text(encoding="ascii").strip() - status = json.loads(STATUS.read_text(encoding="utf-8")) - identity = json.loads(IDENTITY_STATUS.read_text(encoding="utf-8")) - if status.get("boot_id") != boot_id or identity.get("boot_id") != boot_id: - raise RuntimeError("remote access evidence belongs to another boot") - enabled = unit_state("is-enabled", "ssh.service") - active = unit_state("is-active", "ssh.service") - listener_ipv4 = tcp_listener(Path("/proc/net/tcp"), SSH_PORT) - listener_ipv6 = tcp_listener(Path("/proc/net/tcp6"), SSH_PORT) - listening = listener_ipv4 or listener_ipv6 - if enabled and active and listening: - remote_access = "active" - elif enabled and not listening: - remote_access = "enabled_no_listener" - elif listening and not active: - remote_access = "listener_without_service" - else: - remote_access = "inactive" - operational = status.get("state_outcome") == "ready" and status.get("local_console_access") is True and identity.get("outcome") == "ready" and identity.get("persisted") is True and remote_access == "active" - status.update({"mode":"operational" if operational else "recovery","remote_access":remote_access,"remote_protocol":"ssh","remote_port":SSH_PORT,"ssh_service_enabled":enabled,"ssh_service_active":active,"ssh_listener_ipv4":listener_ipv4,"ssh_listener_ipv6":listener_ipv6,"ssh_host_key_action":identity.get("action"),"ssh_host_keys_persisted":identity.get("persisted") is True}) - with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=RUNTIME, prefix=".recovery-access-", delete=False) as stream: - json.dump(status, stream, sort_keys=True) - stream.write("\n") - stream.flush() - os.fsync(stream.fileno()) - temporary = Path(stream.name) - temporary.chmod(0o644) - os.replace(temporary, STATUS) - fsync_directory(RUNTIME) - return 0 if operational else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) -''', - 0o755, -) - -create("build/usb/includes.chroot/etc/systemd/system/rigos-ssh-identity.service", '''[Unit] -Description=Establish persistent RIGOS SSH machine identity -After=rigos-state-ready.service -Requires=rigos-state-ready.service -Before=ssh.service - -[Service] -Type=oneshot -ExecStart=/usr/lib/rigos/rigos-ssh-identity -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target -''') -create("build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service", '''[Unit] -Description=Observe RIGOS remote access truth -After=network-online.target ssh.service rigos-ssh-identity.service -Wants=network-online.target -Requires=ssh.service rigos-ssh-identity.service - -[Service] -Type=oneshot -ExecStart=/usr/lib/rigos/rigos-remote-access-probe -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target -''') -create("build/usb/includes.chroot/etc/systemd/system/rigos-miner-public-status.service", '''[Unit] -Description=Publish redacted RIGOS miner configuration truth -After=rigos-miner.service -Requires=rigos-state-ready.service -ConditionPathExists=/var/lib/rigos/xmrig.json - -[Service] -Type=oneshot -ExecStart=/usr/lib/rigos/rigos-miner-public-config -''') -create("build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos.conf", '''[Unit] -Requires=rigos-ssh-identity.service -After=rigos-ssh-identity.service -''') -create("build/usb/includes.chroot/etc/ssh/sshd_config.d/rigos.conf", '''PasswordAuthentication yes -KbdInteractiveAuthentication no -PermitRootLogin no -AllowUsers rigosadmin -X11Forwarding no -AllowAgentForwarding no -AllowTcpForwarding no -PermitTunnel no -GatewayPorts no -''') - -replace_once( - "build/usb/hooks/010-rigos.chroot", - "install -d -m 0755 /usr/lib/rigos\n", - '''install -d -m 0755 /usr/lib/rigos /usr/local/bin -ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd -ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl -rm -f /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key.pub -rm -f /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key.pub -rm -f /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key.pub -''', -) -replace_once( - "build/usb/hooks/010-rigos.chroot", - "/usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig\n", - "/usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/rigos-ssh-identity /usr/lib/rigos/rigos-remote-access-probe /usr/lib/rigos/rigos-miner-public-config /usr/lib/rigos/xmrig\n", -) -replace_once( - "build/usb/hooks/010-rigos.chroot", - "systemctl enable NetworkManager.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount\n", - "systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-ssh-identity.service rigos-remote-access-status.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount\nsystemctl disable ssh.socket 2>/dev/null || true\n", -) -replace_once( - "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service", - "Wants=network-online.target\n", - "Wants=network-online.target rigos-miner-public-status.service\n", -) - -# Ordering and verification gates. -replace_once( - "scripts/verify-systemd-ordering.py", - ''' "rigos-state-ready.service", "rigos-profile-apply.service", - "rigos-firstboot.service", "rigos-hugepages.service", "rigos-miner.service", -''', - ''' "rigos-state-ready.service", "rigos-profile-apply.service", - "rigos-firstboot.service", "rigos-ssh-identity.service", - "rigos-remote-access-status.service", "rigos-miner-public-status.service", - "rigos-hugepages.service", "rigos-miner.service", -''', -) -replace_once( - "scripts/verify-systemd-ordering.py", - ' ready = units["rigos-state-ready.service"]\n', - ''' ssh_identity = units["rigos-ssh-identity.service"] - includes(ssh_identity.words("Unit", "After"), {"rigos-state-ready.service"}, "SSH identity must follow state readiness") - includes(ssh_identity.words("Unit", "Requires"), {"rigos-state-ready.service"}, "SSH identity must require state readiness") - includes(ssh_identity.words("Unit", "Before"), {"ssh.service"}, "SSH identity must precede sshd") - - remote_access = units["rigos-remote-access-status.service"] - includes(remote_access.words("Unit", "After"), {"ssh.service", "rigos-ssh-identity.service"}, "remote access truth ordering is incomplete") - includes(remote_access.words("Unit", "Requires"), {"ssh.service", "rigos-ssh-identity.service"}, "remote access truth dependencies are incomplete") - - public_status = units["rigos-miner-public-status.service"] - includes(public_status.words("Unit", "After"), {"rigos-miner.service"}, "public miner status must follow miner") - includes(public_status.words("Unit", "Requires"), {"rigos-state-ready.service"}, "public miner status must require ready state") - - ready = units["rigos-state-ready.service"] -''', -) -replace_once( - "scripts/verify.sh", - ''' build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \\ - scripts/verify-systemd-ordering.py -''', - ''' build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \\ - build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-identity \\ - build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe \\ - build/usb/includes.chroot/usr/lib/rigos/rigos-miner-public-config \\ - scripts/verify-systemd-ordering.py -''', -) -replace_once( - "scripts/verify.sh", - "grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service\n", - '''grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service -grep -Fq 'rigos-miner-public-status.service' build/usb/includes.chroot/etc/systemd/system/rigos-miner.service -grep -Fq 'Before=ssh.service' build/usb/includes.chroot/etc/systemd/system/rigos-ssh-identity.service -grep -Fq 'Requires=rigos-ssh-identity.service' build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos.conf -grep -Fq 'After=network-online.target ssh.service rigos-ssh-identity.service' build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service -grep -Fq 'ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd' build/usb/hooks/010-rigos.chroot -grep -Fq 'ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl' build/usb/hooks/010-rigos.chroot -grep -Fq 'systemctl disable ssh.socket' build/usb/hooks/010-rigos.chroot -grep -Fqx 'AllowUsers rigosadmin' build/usb/includes.chroot/etc/ssh/sshd_config.d/rigos.conf -grep -Fqx 'PermitRootLogin no' build/usb/includes.chroot/etc/ssh/sshd_config.d/rigos.conf -''', -) - -# Keep the image verifier authoritative without requiring a physical boot. -replace_once( - "scripts/verify-usb-appliance.sh", - " etc/systemd/system/rigos-recovery-access.service \\\n", - " etc/systemd/system/rigos-recovery-access.service \\\n etc/systemd/system/rigos-ssh-identity.service \\\n etc/systemd/system/rigos-remote-access-status.service \\\n etc/systemd/system/rigos-miner-public-status.service \\\n etc/systemd/system/ssh.service.d/rigos.conf \\\n etc/ssh/sshd_config.d/rigos.conf \\\n", -) -replace_once( - "scripts/verify-usb-appliance.sh", - " usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \\\n", - " usr/lib/rigos/rigosd usr/lib/rigos/rigosctl usr/local/bin/rigosd usr/local/bin/rigosctl \\\n", -) -replace_once( - "scripts/verify-usb-appliance.sh", - "usr/lib/rigos/rigos-miner-gate usr/lib/rigos/xmrig", - "usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-ssh-identity usr/lib/rigos/rigos-remote-access-probe usr/lib/rigos/rigos-miner-public-config usr/lib/rigos/xmrig", -) -replace_once( - "scripts/verify-usb-appliance.sh", - 'python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate"\n', - '''python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" -python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-ssh-identity" -python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-remote-access-probe" -python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-public-config" -''', -) -replace_once( - "scripts/verify-usb-appliance.sh", - '''rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" -[[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' -''', - '''user_path="$temporary/root/usr/local/bin:$temporary/root/usr/bin:$temporary/root/bin" -rigosd_path="$(PATH="$user_path" command -v rigosd)" -rigosctl_path="$(PATH="$user_path" command -v rigosctl)" -[[ "$rigosd_path" == "$temporary/root/usr/local/bin/rigosd" && -x "$rigosd_path" ]] || die 'rigosd is not executable in the user appliance PATH' -[[ "$rigosctl_path" == "$temporary/root/usr/local/bin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the user appliance PATH' -''', -) -replace_once( - "scripts/verify-usb-appliance.sh", - 'unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \\\n', - '''if unsquashfs -ll "$temporary/a/live/filesystem.squashfs" | grep -Eq '/etc/ssh/ssh_host_(rsa|ecdsa|ed25519)_key$'; then - die 'image contains a baked SSH private host key' -fi -unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \\ -''', -) - -# Remove the one-shot patch mechanism from the resulting source commit. -(ROOT / "scripts/apply-alpha8-physical-fixes.py").unlink() -workflow = ROOT / ".github/workflows/alpha8-apply.yml" -workflow.unlink() -print("Alpha8 physical findings applied") From 3627b76874318a0b59f40ab90a102b046ed73d79 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:50:27 +0700 Subject: [PATCH 056/386] miner render before runtime gate --- .../systemd/system/rigos-miner.service.d/runtime-render.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf index 3f2a44ee..030eecfd 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf @@ -2,9 +2,10 @@ After=rigos-runtime-render.service Requires=rigos-runtime-render.service ConditionPathExists= -ConditionPathExists=/run/rigos/xmrig.json +ConditionPathExists=/var/lib/rigos/current [Service] +ExecCondition=+/usr/lib/rigos/rigos-runtime-render ExecCondition=/usr/lib/rigos/rigos-runtime-gate ExecStart= ExecStart=/usr/lib/rigos/xmrig --config=/run/rigos/xmrig.json From de4389db4d4b99720fb49f4621fc0fa95f1bba62 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:50:49 +0700 Subject: [PATCH 057/386] runtime render remain active --- .../systemd/system/rigos-runtime-render.service.d/remain.conf | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service.d/remain.conf diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service.d/remain.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service.d/remain.conf new file mode 100644 index 00000000..75039e3a --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service.d/remain.conf @@ -0,0 +1,2 @@ +[Service] +RemainAfterExit=yes From b5a60db02d2563b51f9dda0402cd91be4c63463f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:56:30 +0700 Subject: [PATCH 058/386] test alpha8 runtime authority --- scripts/check-alpha8-runtime.py | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 scripts/check-alpha8-runtime.py diff --git a/scripts/check-alpha8-runtime.py b/scripts/check-alpha8-runtime.py new file mode 100644 index 00000000..c8362f9f --- /dev/null +++ b/scripts/check-alpha8-runtime.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import json +import os +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RENDERER = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render" +GATE = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate" + + +def write(path: Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value), encoding="utf-8") + + +def main() -> int: + with tempfile.TemporaryDirectory(prefix="rigos-alpha8-") as temporary: + root = Path(temporary) + state = root / "state" + runtime = root / "run" + revision = state / "revisions/r1" + (revision / "flight-sheets").mkdir(parents=True) + (state / "current").symlink_to(Path("revisions/r1")) + write( + revision / "policy.json", + {"schema": "rigos.policy/v1", "active_flight_sheet": "xmr"}, + ) + write( + revision / "flight-sheets/xmr.json", + { + "schema": "rigos.flight-sheet/v1", + "backend": "xmrig", + "algorithm": "rx/0", + "cpu": { + "threads": 2, + "huge_pages": True, + "max_threads_hint": 100, + }, + }, + ) + write( + revision / "xmrig.json", + { + "cpu": { + "enabled": True, + "huge-pages": True, + "max-threads-hint": 2, + }, + "pools": [{"url": "pool.test:1", "algo": "rx/0"}], + "http": {"enabled": False}, + }, + ) + environment = os.environ.copy() + environment.update( + { + "RIGOS_STATE_PATH": str(state), + "RIGOS_RUNTIME_PATH": str(runtime), + "RIGOS_RENDER_SKIP_CHOWN": "1", + } + ) + subprocess.run(["python3", str(RENDERER)], env=environment, check=True) + config = json.loads((runtime / "xmrig.json").read_text(encoding="utf-8")) + assert config["cpu"]["max-threads-hint"] == 100 + assert config["cpu"]["rx"] == [-1, -1] + status = json.loads( + (runtime / "runtime-config-status.json").read_text(encoding="utf-8") + ) + assert status["thread_mode"] == "exact" + assert status["exact_threads"] == 2 + assert status["profile"] == "rx" + allowed = subprocess.run( + [ + "python3", + str(GATE), + "--state", + str(state), + "--runtime", + str(runtime), + ], + check=False, + ) + assert allowed.returncode == 0 + config["cpu"]["rx"] = [-1] + write(runtime / "xmrig.json", config) + denied = subprocess.run( + [ + "python3", + str(GATE), + "--state", + str(state), + "--runtime", + str(runtime), + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + assert denied.returncode == 2 + print("RIGOS Alpha8 runtime authority verification passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From efec3e76e8b30983f95bc60e548505e52054362b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:57:25 +0700 Subject: [PATCH 059/386] test alpha8 runtime authority in workspace verify --- crates/rigos-config/tests/recovery_access.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index ea30a417..1e9ced83 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -8,6 +8,11 @@ fn recovery_path() -> PathBuf { .join("../../build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access") } +fn alpha8_runtime_check_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../scripts/check-alpha8-runtime.py") +} + #[test] fn recovery_password_is_persisted_restored_and_redacted() { let root = std::env::temp_dir().join(format!("rigos-recovery-access-{}", Uuid::new_v4())); @@ -119,3 +124,12 @@ assert valid_hash not in json.dumps(json.loads((g['RUNTIME'] / 'recovery-access- let _ = fs::remove_dir_all(&root); assert!(result.success(), "recovery access fixture failed"); } + +#[test] +fn alpha8_runtime_authority_is_exact_and_fail_closed() { + let result = Command::new("python3") + .arg(alpha8_runtime_check_path()) + .status() + .expect("run Alpha8 runtime authority fixture"); + assert!(result.success(), "Alpha8 runtime authority fixture failed"); +} From 63580355bbd2c0ada1d5aa12ae6c210c0ac4db7b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 14:57:56 +0700 Subject: [PATCH 060/386] test remote listener truth in alpha8 gate --- scripts/check-alpha8-runtime.py | 206 +++++++++++++++++++------------- 1 file changed, 125 insertions(+), 81 deletions(-) diff --git a/scripts/check-alpha8-runtime.py b/scripts/check-alpha8-runtime.py index c8362f9f..cd67fff7 100644 --- a/scripts/check-alpha8-runtime.py +++ b/scripts/check-alpha8-runtime.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import json import os +import runpy import subprocess import tempfile from pathlib import Path @@ -8,6 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] RENDERER = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render" GATE = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate" +REMOTE_PROBE = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-remote-access-probe" def write(path: Path, value: dict) -> None: @@ -15,90 +17,132 @@ def write(path: Path, value: dict) -> None: path.write_text(json.dumps(value), encoding="utf-8") +def verify_runtime_authority(root: Path) -> None: + state = root / "state" + runtime = root / "run" + revision = state / "revisions/r1" + (revision / "flight-sheets").mkdir(parents=True) + (state / "current").symlink_to(Path("revisions/r1")) + write( + revision / "policy.json", + {"schema": "rigos.policy/v1", "active_flight_sheet": "xmr"}, + ) + write( + revision / "flight-sheets/xmr.json", + { + "schema": "rigos.flight-sheet/v1", + "backend": "xmrig", + "algorithm": "rx/0", + "cpu": { + "threads": 2, + "huge_pages": True, + "max_threads_hint": 100, + }, + }, + ) + write( + revision / "xmrig.json", + { + "cpu": { + "enabled": True, + "huge-pages": True, + "max-threads-hint": 2, + }, + "pools": [{"url": "pool.test:1", "algo": "rx/0"}], + "http": {"enabled": False}, + }, + ) + environment = os.environ.copy() + environment.update( + { + "RIGOS_STATE_PATH": str(state), + "RIGOS_RUNTIME_PATH": str(runtime), + "RIGOS_RENDER_SKIP_CHOWN": "1", + } + ) + subprocess.run(["python3", str(RENDERER)], env=environment, check=True) + config = json.loads((runtime / "xmrig.json").read_text(encoding="utf-8")) + assert config["cpu"]["max-threads-hint"] == 100 + assert config["cpu"]["rx"] == [-1, -1] + status = json.loads( + (runtime / "runtime-config-status.json").read_text(encoding="utf-8") + ) + assert status["thread_mode"] == "exact" + assert status["exact_threads"] == 2 + assert status["profile"] == "rx" + allowed = subprocess.run( + [ + "python3", + str(GATE), + "--state", + str(state), + "--runtime", + str(runtime), + ], + check=False, + ) + assert allowed.returncode == 0 + config["cpu"]["rx"] = [-1] + write(runtime / "xmrig.json", config) + denied = subprocess.run( + [ + "python3", + str(GATE), + "--state", + str(state), + "--runtime", + str(runtime), + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + assert denied.returncode == 2 + + +def verify_remote_truth(root: Path) -> None: + runtime = root / "remote-run" + runtime.mkdir() + boot_id = root / "remote-boot-id" + boot_id.write_text("boot-test\n", encoding="ascii") + status_path = runtime / "recovery-access-status.json" + write( + status_path, + { + "schema": "rigos.recovery-access-status/v1", + "boot_id": "boot-test", + "state_outcome": "ready", + "local_console_access": True, + }, + ) + namespace = runpy.run_path(str(REMOTE_PROBE), run_name="rigos_remote_probe_test") + globals_ = namespace["main"].__globals__ + globals_["RUNTIME"] = runtime + globals_["STATUS"] = status_path + globals_["BOOT_ID"] = boot_id + globals_["unit_state"] = lambda _action, _unit: True + globals_["has_listener"] = lambda path, _port: path.name == "tcp" + assert namespace["main"]() == 0 + observed = json.loads(status_path.read_text(encoding="utf-8")) + assert observed["mode"] == "operational" + assert observed["remote_access"] == "active" + assert observed["ssh_service_enabled"] is True + assert observed["ssh_service_active"] is True + assert observed["ssh_listener_ipv4"] is True + assert observed["ssh_listener_ipv6"] is False + globals_["has_listener"] = lambda _path, _port: False + assert namespace["main"]() == 1 + degraded = json.loads(status_path.read_text(encoding="utf-8")) + assert degraded["mode"] == "recovery" + assert degraded["remote_access"] == "enabled_no_listener" + + def main() -> int: with tempfile.TemporaryDirectory(prefix="rigos-alpha8-") as temporary: root = Path(temporary) - state = root / "state" - runtime = root / "run" - revision = state / "revisions/r1" - (revision / "flight-sheets").mkdir(parents=True) - (state / "current").symlink_to(Path("revisions/r1")) - write( - revision / "policy.json", - {"schema": "rigos.policy/v1", "active_flight_sheet": "xmr"}, - ) - write( - revision / "flight-sheets/xmr.json", - { - "schema": "rigos.flight-sheet/v1", - "backend": "xmrig", - "algorithm": "rx/0", - "cpu": { - "threads": 2, - "huge_pages": True, - "max_threads_hint": 100, - }, - }, - ) - write( - revision / "xmrig.json", - { - "cpu": { - "enabled": True, - "huge-pages": True, - "max-threads-hint": 2, - }, - "pools": [{"url": "pool.test:1", "algo": "rx/0"}], - "http": {"enabled": False}, - }, - ) - environment = os.environ.copy() - environment.update( - { - "RIGOS_STATE_PATH": str(state), - "RIGOS_RUNTIME_PATH": str(runtime), - "RIGOS_RENDER_SKIP_CHOWN": "1", - } - ) - subprocess.run(["python3", str(RENDERER)], env=environment, check=True) - config = json.loads((runtime / "xmrig.json").read_text(encoding="utf-8")) - assert config["cpu"]["max-threads-hint"] == 100 - assert config["cpu"]["rx"] == [-1, -1] - status = json.loads( - (runtime / "runtime-config-status.json").read_text(encoding="utf-8") - ) - assert status["thread_mode"] == "exact" - assert status["exact_threads"] == 2 - assert status["profile"] == "rx" - allowed = subprocess.run( - [ - "python3", - str(GATE), - "--state", - str(state), - "--runtime", - str(runtime), - ], - check=False, - ) - assert allowed.returncode == 0 - config["cpu"]["rx"] = [-1] - write(runtime / "xmrig.json", config) - denied = subprocess.run( - [ - "python3", - str(GATE), - "--state", - str(state), - "--runtime", - str(runtime), - ], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - assert denied.returncode == 2 - print("RIGOS Alpha8 runtime authority verification passed") + verify_runtime_authority(root) + verify_remote_truth(root) + print("RIGOS Alpha8 runtime and remote truth verification passed") return 0 From 972b90ed3f9dfe4660c89d4df198b451633bbc17 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 15:00:16 +0700 Subject: [PATCH 061/386] test alpha8 appliance wiring --- crates/rigos-config/tests/recovery_access.rs | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index 1e9ced83..f17d9b3c 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -13,6 +13,12 @@ fn alpha8_runtime_check_path() -> PathBuf { .join("../../scripts/check-alpha8-runtime.py") } +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + #[test] fn recovery_password_is_persisted_restored_and_redacted() { let root = std::env::temp_dir().join(format!("rigos-recovery-access-{}", Uuid::new_v4())); @@ -133,3 +139,29 @@ fn alpha8_runtime_authority_is_exact_and_fail_closed() { .expect("run Alpha8 runtime authority fixture"); assert!(result.success(), "Alpha8 runtime authority fixture failed"); } + +#[test] +fn alpha8_appliance_wiring_is_explicit() { + let hook = fs::read_to_string(repo_path("build/usb/hooks/010-rigos.chroot")) + .expect("read appliance hook"); + assert!(hook.contains("ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd")); + assert!(hook.contains("ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl")); + assert!(hook.contains("rigos-runtime-render.service")); + assert!(hook.contains("systemctl disable ssh.socket")); + + let miner = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf", + )) + .expect("read miner runtime override"); + assert!(miner.contains("Requires=rigos-runtime-render.service")); + assert!(miner.contains("ConditionPathExists=/var/lib/rigos/current")); + assert!(miner.contains("ExecCondition=+/usr/lib/rigos/rigos-runtime-render")); + assert!(miner.contains("ExecCondition=/usr/lib/rigos/rigos-runtime-gate")); + assert!(miner.contains("--config=/run/rigos/xmrig.json")); + + let ssh = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf", + )) + .expect("read SSH observer override"); + assert!(ssh.contains("Wants=rigos-remote-access-observe.service")); +} From 24c4f0cf9cedd636055b27f3773935a3b1290e6a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 15:05:48 +0700 Subject: [PATCH 062/386] format alpha8 runtime test --- crates/rigos-config/tests/recovery_access.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index f17d9b3c..4d6b5772 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -9,8 +9,7 @@ fn recovery_path() -> PathBuf { } fn alpha8_runtime_check_path() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../scripts/check-alpha8-runtime.py") + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../scripts/check-alpha8-runtime.py") } fn repo_path(path: &str) -> PathBuf { From 4fee06180e2af3a1d6c539d31e96103fd2af3444 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 15:14:53 +0700 Subject: [PATCH 063/386] remove duplicate remote access unit --- .../system/rigos-remote-access-status.service | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service b/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service deleted file mode 100644 index 9d876dfb..00000000 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-remote-access-status.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Observe RIGOS remote access truth -After=network-online.target ssh.service rigos-recovery-access.service -Wants=network-online.target -Requires=ssh.service rigos-recovery-access.service - -[Service] -Type=oneshot -ExecStart=/usr/lib/rigos/rigos-remote-access-probe -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target From 627bef1d655460d7a1bf3fe4511ebef629c805a4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 15:15:20 +0700 Subject: [PATCH 064/386] release stage alpha8 appliance candidate --- build/usb/version.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/version.env b/build/usb/version.env index 041c82ab..9567e611 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.7 -RIGOS_IMAGE_VERSION=0.0.4-alpha.7 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.8 +RIGOS_IMAGE_VERSION=0.0.4-alpha.8 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=7 +RIGOS_BUILD_ORDINAL=8 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 From c1ca5144d6fcd2721f99c73e8daee7988dba6e16 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 16:04:38 +0700 Subject: [PATCH 065/386] alpha8 hotfix enable SSH password login --- .../etc/ssh/sshd_config.d/00-rigos.conf | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf diff --git a/build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf b/build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf new file mode 100644 index 00000000..720f859f --- /dev/null +++ b/build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf @@ -0,0 +1,12 @@ +PasswordAuthentication yes +KbdInteractiveAuthentication no +UsePAM yes +AuthenticationMethods any +PubkeyAuthentication yes +PermitRootLogin no +AllowUsers rigosadmin +X11Forwarding no +AllowAgentForwarding no +AllowTcpForwarding no +PermitTunnel no +GatewayPorts no From 105d391a9f538313666c27430f8aff6ed7911e47 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 16:05:28 +0700 Subject: [PATCH 066/386] verify alpha8 SSH hotfix policy --- scripts/check-alpha8-ssh-hotfix.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/check-alpha8-ssh-hotfix.py diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py new file mode 100644 index 00000000..b303d350 --- /dev/null +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import hashlib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +POLICY = ROOT / "build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf" +PACKAGES = ROOT / "build/usb/package-lists/rigos.list.chroot" +HOOK = ROOT / "build/usb/hooks/010-rigos.chroot" +EXPECTED_POLICY_SHA256 = "d59b6bcc078a047d1f1cc90ef6ed9205476d91f874be809009bdd442ef66b8c3" + + +def main() -> int: + policy = POLICY.read_bytes() + observed = hashlib.sha256(policy).hexdigest() + if observed != EXPECTED_POLICY_SHA256: + raise RuntimeError( + f"Alpha8 SSH policy hash mismatch: expected={EXPECTED_POLICY_SHA256} observed={observed}" + ) + packages = PACKAGES.read_text(encoding="utf-8").splitlines() + if "openssh-server" not in packages: + raise RuntimeError("OpenSSH server package is missing") + hook = HOOK.read_text(encoding="utf-8") + if "ssh.service" not in hook or "systemctl disable ssh.socket" not in hook: + raise RuntimeError("deterministic SSH service wiring is missing") + print("RIGOS Alpha8 SSH hotfix verification passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 2a49c771a37eae706cb3d51490993d9b221f3b75 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 16:07:28 +0700 Subject: [PATCH 067/386] gate alpha8 rebuild on hotfix verification --- build/usb/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/Dockerfile b/build/usb/Dockerfile index 9f773489..3a510058 100644 --- a/build/usb/Dockerfile +++ b/build/usb/Dockerfile @@ -8,4 +8,4 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* WORKDIR /source -ENTRYPOINT ["./scripts/build-usb-image.sh"] +ENTRYPOINT ["bash", "-lc", "python3 ./scripts/check-alpha8-ssh-hotfix.py && exec ./scripts/build-usb-image.sh"] From 83ecd6e7674f2907b73793550d7bdfccc0f8de71 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 18:55:00 +0700 Subject: [PATCH 068/386] alpha8 hotfix normalize SSH policy line endings --- scripts/check-alpha8-ssh-hotfix.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py index b303d350..5979c2cf 100644 --- a/scripts/check-alpha8-ssh-hotfix.py +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -9,8 +9,15 @@ EXPECTED_POLICY_SHA256 = "d59b6bcc078a047d1f1cc90ef6ed9205476d91f874be809009bdd442ef66b8c3" +def normalized_lf_bytes(path: Path) -> bytes: + raw = path.read_bytes() + if raw.startswith(b"\xef\xbb\xbf"): + raise RuntimeError("Alpha8 SSH policy must be UTF-8 without BOM") + return raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + + def main() -> int: - policy = POLICY.read_bytes() + policy = normalized_lf_bytes(POLICY) observed = hashlib.sha256(policy).hexdigest() if observed != EXPECTED_POLICY_SHA256: raise RuntimeError( From 98ed5bd1ee6e5048eb7141e38a3ccfae593675d3 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:00:39 +0700 Subject: [PATCH 069/386] alpha8 hotfix preserve Cargo path in builder --- build/usb/Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build/usb/Dockerfile b/build/usb/Dockerfile index 3a510058..baea6a79 100644 --- a/build/usb/Dockerfile +++ b/build/usb/Dockerfile @@ -1,11 +1,16 @@ FROM docker.io/library/rust:1.85.1-bookworm +ENV PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:${PATH}" + RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates curl dosfstools e2fsprogs fdisk gdisk git grub-efi-amd64-bin \ grub-pc-bin grub2-common isolinux jq live-build mtools ovmf parted qemu-system-x86 \ ripgrep rsync squashfs-tools syslinux-common syslinux-utils udev uuid-runtime xorriso zstd \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && command -v cargo \ + && cargo --version \ + && rustc --version WORKDIR /source -ENTRYPOINT ["bash", "-lc", "python3 ./scripts/check-alpha8-ssh-hotfix.py && exec ./scripts/build-usb-image.sh"] +ENTRYPOINT ["/bin/bash", "-c", "python3 ./scripts/check-alpha8-ssh-hotfix.py && exec ./scripts/build-usb-image.sh"] From e76734d22e040d44ef1f5a3038661ccc53eea8de Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:01:05 +0700 Subject: [PATCH 070/386] alpha8 hotfix verify builder Cargo path --- scripts/check-alpha8-ssh-hotfix.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py index 5979c2cf..e8b38990 100644 --- a/scripts/check-alpha8-ssh-hotfix.py +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -6,6 +6,7 @@ POLICY = ROOT / "build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf" PACKAGES = ROOT / "build/usb/package-lists/rigos.list.chroot" HOOK = ROOT / "build/usb/hooks/010-rigos.chroot" +DOCKERFILE = ROOT / "build/usb/Dockerfile" EXPECTED_POLICY_SHA256 = "d59b6bcc078a047d1f1cc90ef6ed9205476d91f874be809009bdd442ef66b8c3" @@ -29,6 +30,15 @@ def main() -> int: hook = HOOK.read_text(encoding="utf-8") if "ssh.service" not in hook or "systemctl disable ssh.socket" not in hook: raise RuntimeError("deterministic SSH service wiring is missing") + dockerfile = DOCKERFILE.read_text(encoding="utf-8") + if 'ENV PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:${PATH}"' not in dockerfile: + raise RuntimeError("builder Cargo PATH is not explicit") + if 'ENTRYPOINT ["/bin/bash", "-c",' not in dockerfile: + raise RuntimeError("builder entrypoint must use a non-login shell") + if '"bash", "-lc"' in dockerfile or '"/bin/bash", "-lc"' in dockerfile: + raise RuntimeError("builder entrypoint must not use a login shell") + if "cargo --version" not in dockerfile or "rustc --version" not in dockerfile: + raise RuntimeError("builder toolchain verification is missing") print("RIGOS Alpha8 SSH hotfix verification passed") return 0 From 8b6068c19f45f3d2c22695d9b7e3aca93b6f513f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:01:41 +0700 Subject: [PATCH 071/386] alpha8 hotfix reuse builder package layer --- build/usb/Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build/usb/Dockerfile b/build/usb/Dockerfile index baea6a79..be63222a 100644 --- a/build/usb/Dockerfile +++ b/build/usb/Dockerfile @@ -1,14 +1,15 @@ FROM docker.io/library/rust:1.85.1-bookworm -ENV PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:${PATH}" - RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates curl dosfstools e2fsprogs fdisk gdisk git grub-efi-amd64-bin \ grub-pc-bin grub2-common isolinux jq live-build mtools ovmf parted qemu-system-x86 \ ripgrep rsync squashfs-tools syslinux-common syslinux-utils udev uuid-runtime xorriso zstd \ - && rm -rf /var/lib/apt/lists/* \ - && command -v cargo \ + && rm -rf /var/lib/apt/lists/* + +ENV PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:${PATH}" + +RUN command -v cargo \ && cargo --version \ && rustc --version From 0b77a5f601be0c21a4a0c9eccd93500a5cc129cf Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:33:33 +0700 Subject: [PATCH 072/386] alpha8 hotfix verify recovery credential outcome --- .../lib/rigos/rigos-recovery-access-verify | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify b/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify new file mode 100644 index 00000000..c0e53c1b --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify @@ -0,0 +1,54 @@ +#!/usr/bin/python3 +import json +import sys +from pathlib import Path + +RUNTIME = Path("/run/rigos") +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +STATUS = RUNTIME / "recovery-access-status.json" +MAX_STATUS_BYTES = 64 * 1024 + + +def emit(outcome: str, reason: str | None = None) -> None: + value = { + "schema": "rigos.recovery-access-gate/v1", + "outcome": outcome, + "reason": reason, + } + print(json.dumps(value, sort_keys=True), file=sys.stdout if outcome == "allowed" else sys.stderr) + + +def deny(reason: str) -> int: + emit("denied", reason) + return 2 + + +def main() -> int: + try: + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + raw = STATUS.read_bytes() + if not boot_id or not raw or len(raw) > MAX_STATUS_BYTES: + return deny("recovery_status_unreadable") + status = json.loads(raw) + except (OSError, UnicodeError, json.JSONDecodeError): + return deny("recovery_status_unreadable") + + if not isinstance(status, dict): + return deny("recovery_status_invalid") + if status.get("schema") != "rigos.recovery-access-status/v1": + return deny("recovery_schema_mismatch") + if status.get("boot_id") != boot_id: + return deny("recovery_status_stale") + if status.get("local_console_access") is not True: + return deny("local_credential_unavailable") + if status.get("credential_persisted") is not True: + return deny("credential_not_persisted") + if status.get("credential_action") not in ("existing", "created", "restored"): + return deny("credential_action_invalid") + + emit("allowed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3dd0db975f6ba3e773d8d83d2192917cfb08797e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:33:45 +0700 Subject: [PATCH 073/386] alpha8 hotfix clear false recovery failure --- .../etc/systemd/system/rigos-recovery-access.service | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service b/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service index 228e2a24..ca00519e 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service @@ -2,11 +2,13 @@ Description=Establish local RIGOS recovery access After=rigos-state.service Wants=rigos-state.service -Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service +Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service ssh.service [Service] Type=oneshot ExecStart=/usr/local/sbin/rigos-recovery-access +SuccessExitStatus=1 +ExecStartPost=/usr/bin/python3 /usr/lib/rigos/rigos-recovery-access-verify StandardInput=tty StandardOutput=tty StandardError=tty From 827916fb37e6c358fdd27c7dacbebdbcccda285c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:34:15 +0700 Subject: [PATCH 074/386] alpha8 hotfix make recovery gate testable --- .../usr/lib/rigos/rigos-recovery-access-verify | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify b/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify index c0e53c1b..ac3dd072 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify @@ -1,10 +1,11 @@ #!/usr/bin/python3 import json +import os import sys from pathlib import Path -RUNTIME = Path("/run/rigos") -BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) +BOOT_ID = Path(os.environ.get("RIGOS_BOOT_ID_PATH", "/proc/sys/kernel/random/boot_id")) STATUS = RUNTIME / "recovery-access-status.json" MAX_STATUS_BYTES = 64 * 1024 From 93de25eb6ef50f1e88ad824f09dbdfa0f27a75c7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:34:32 +0700 Subject: [PATCH 075/386] test alpha8 recovery false-failure hotfix --- .../tests/alpha8_recovery_gate.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 crates/rigos-config/tests/alpha8_recovery_gate.rs diff --git a/crates/rigos-config/tests/alpha8_recovery_gate.rs b/crates/rigos-config/tests/alpha8_recovery_gate.rs new file mode 100644 index 00000000..c1381b79 --- /dev/null +++ b/crates/rigos-config/tests/alpha8_recovery_gate.rs @@ -0,0 +1,79 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use uuid::Uuid; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +#[test] +fn recovery_gate_accepts_only_current_persisted_credential_truth() { + let root = std::env::temp_dir().join(format!("rigos-recovery-gate-{}", Uuid::new_v4())); + let runtime = root.join("run"); + let boot_id = root.join("boot-id"); + fs::create_dir_all(&runtime).unwrap(); + fs::write(&boot_id, "boot-test\n").unwrap(); + + let status = runtime.join("recovery-access-status.json"); + let valid = serde_json::json!({ + "schema": "rigos.recovery-access-status/v1", + "boot_id": "boot-test", + "local_console_access": true, + "credential_action": "created", + "credential_persisted": true + }); + fs::write(&status, serde_json::to_vec(&valid).unwrap()).unwrap(); + + let gate = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify", + ); + let run = |runtime_path: &PathBuf, boot_path: &PathBuf| { + Command::new("python3") + .arg(&gate) + .env("RIGOS_RUNTIME_PATH", runtime_path) + .env("RIGOS_BOOT_ID_PATH", boot_path) + .status() + .unwrap() + }; + + assert!(run(&runtime, &boot_id).success()); + + let stale = serde_json::json!({ + "schema": "rigos.recovery-access-status/v1", + "boot_id": "old-boot", + "local_console_access": true, + "credential_action": "created", + "credential_persisted": true + }); + fs::write(&status, serde_json::to_vec(&stale).unwrap()).unwrap(); + assert_eq!(run(&runtime, &boot_id).code(), Some(2)); + + let not_persisted = serde_json::json!({ + "schema": "rigos.recovery-access-status/v1", + "boot_id": "boot-test", + "local_console_access": true, + "credential_action": "created", + "credential_persisted": false + }); + fs::write(&status, serde_json::to_vec(¬_persisted).unwrap()).unwrap(); + assert_eq!(run(&runtime, &boot_id).code(), Some(2)); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn recovery_service_accepts_legacy_exit_one_only_with_post_validation() { + let unit = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service", + )) + .unwrap(); + + assert!(unit.contains("Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service ssh.service")); + assert!(unit.contains("SuccessExitStatus=1")); + assert!(unit.contains( + "ExecStartPost=/usr/bin/python3 /usr/lib/rigos/rigos-recovery-access-verify" + )); +} From 5e00b35b3d704fa1d7c5173e62d44a84deabe67a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:35:20 +0700 Subject: [PATCH 076/386] alpha8 hotfix verify recovery access outcome gate --- scripts/check-alpha8-ssh-hotfix.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py index e8b38990..17a154e3 100644 --- a/scripts/check-alpha8-ssh-hotfix.py +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -7,6 +7,8 @@ PACKAGES = ROOT / "build/usb/package-lists/rigos.list.chroot" HOOK = ROOT / "build/usb/hooks/010-rigos.chroot" DOCKERFILE = ROOT / "build/usb/Dockerfile" +RECOVERY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service" +RECOVERY_GATE = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify" EXPECTED_POLICY_SHA256 = "d59b6bcc078a047d1f1cc90ef6ed9205476d91f874be809009bdd442ef66b8c3" @@ -39,7 +41,28 @@ def main() -> int: raise RuntimeError("builder entrypoint must not use a login shell") if "cargo --version" not in dockerfile or "rustc --version" not in dockerfile: raise RuntimeError("builder toolchain verification is missing") - print("RIGOS Alpha8 SSH hotfix verification passed") + + recovery_unit = RECOVERY_UNIT.read_text(encoding="utf-8") + required_unit_lines = ( + "Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service ssh.service", + "SuccessExitStatus=1", + "ExecStartPost=/usr/bin/python3 /usr/lib/rigos/rigos-recovery-access-verify", + ) + for required in required_unit_lines: + if required not in recovery_unit: + raise RuntimeError(f"recovery access hotfix wiring is missing: {required}") + + recovery_gate = RECOVERY_GATE.read_text(encoding="utf-8") + compile(recovery_gate, str(RECOVERY_GATE), "exec") + for required in ( + 'status.get("boot_id") != boot_id', + 'status.get("local_console_access") is not True', + 'status.get("credential_persisted") is not True', + ): + if required not in recovery_gate: + raise RuntimeError(f"recovery access validator contract is missing: {required}") + + print("RIGOS Alpha8 SSH and recovery hotfix verification passed") return 0 From a6dd9ecb6226a412aaa2a7ecb349bb9edbb0aa22 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:35:56 +0700 Subject: [PATCH 077/386] format alpha8 recovery gate test --- crates/rigos-config/tests/alpha8_recovery_gate.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/alpha8_recovery_gate.rs b/crates/rigos-config/tests/alpha8_recovery_gate.rs index c1381b79..27309f05 100644 --- a/crates/rigos-config/tests/alpha8_recovery_gate.rs +++ b/crates/rigos-config/tests/alpha8_recovery_gate.rs @@ -71,7 +71,9 @@ fn recovery_service_accepts_legacy_exit_one_only_with_post_validation() { )) .unwrap(); - assert!(unit.contains("Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service ssh.service")); + assert!(unit.contains( + "Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service ssh.service" + )); assert!(unit.contains("SuccessExitStatus=1")); assert!(unit.contains( "ExecStartPost=/usr/bin/python3 /usr/lib/rigos/rigos-recovery-access-verify" From 3024868d7e6a0ba9f938371607302c67aa413e11 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 19:57:24 +0700 Subject: [PATCH 078/386] format alpha8 recovery gate fixture exactly --- crates/rigos-config/tests/alpha8_recovery_gate.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/rigos-config/tests/alpha8_recovery_gate.rs b/crates/rigos-config/tests/alpha8_recovery_gate.rs index 27309f05..dce3b59b 100644 --- a/crates/rigos-config/tests/alpha8_recovery_gate.rs +++ b/crates/rigos-config/tests/alpha8_recovery_gate.rs @@ -27,9 +27,7 @@ fn recovery_gate_accepts_only_current_persisted_credential_truth() { }); fs::write(&status, serde_json::to_vec(&valid).unwrap()).unwrap(); - let gate = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify", - ); + let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify"); let run = |runtime_path: &PathBuf, boot_path: &PathBuf| { Command::new("python3") .arg(&gate) @@ -75,7 +73,7 @@ fn recovery_service_accepts_legacy_exit_one_only_with_post_validation() { "Before=rigos-state-ready.service rigos-firstboot.service getty@tty1.service ssh.service" )); assert!(unit.contains("SuccessExitStatus=1")); - assert!(unit.contains( - "ExecStartPost=/usr/bin/python3 /usr/lib/rigos/rigos-recovery-access-verify" - )); + assert!( + unit.contains("ExecStartPost=/usr/bin/python3 /usr/lib/rigos/rigos-recovery-access-verify") + ); } From 99f3956de2920c26e8b002fd490513f584fd1374 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:09:06 +0700 Subject: [PATCH 079/386] stability persist SSH host identity --- .../usr/lib/rigos/rigos-ssh-hostkeys | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys new file mode 100644 index 00000000..8b79ced6 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys @@ -0,0 +1,179 @@ +#!/usr/bin/python3 +import json +import os +import stat +import subprocess +import sys +import uuid +from pathlib import Path + +STATE = Path(os.environ.get("RIGOS_STATE_PATH", "/var/lib/rigos")) +RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) +BOOT_ID = Path(os.environ.get("RIGOS_BOOT_ID_PATH", "/proc/sys/kernel/random/boot_id")) +KEYGEN = os.environ.get("RIGOS_SSH_KEYGEN", "/usr/bin/ssh-keygen") +STORE = STATE / "ssh-hostkeys" +STATUS = RUNTIME / "ssh-hostkeys-status.json" +KEY_SPECS = ( + ("ed25519", ()), + ("rsa", ("-b", "3072")), +) + + +def fail(reason: str) -> int: + print(f"rigos-ssh-hostkeys: {reason}", file=sys.stderr) + return 2 + + +def fsync_path(path: Path) -> None: + fd = os.open(path, os.O_RDONLY) + try: + os.fsync(fd) + finally: + os.close(fd) + + +def atomic_json(path: Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") + data = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n" + fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + fsync_path(path.parent) + finally: + try: + temporary.unlink() + except FileNotFoundError: + pass + + +def run_keygen(arguments: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [KEYGEN, *arguments], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=20, + check=False, + ) + + +def public_identity(path: Path) -> tuple[str, str]: + private = path + public = Path(f"{path}.pub") + if private.is_symlink() or public.is_symlink(): + raise RuntimeError("host_key_symlink") + if not private.is_file() or not public.is_file(): + raise RuntimeError("host_key_incomplete") + private_mode = stat.S_IMODE(private.stat().st_mode) + if private_mode != 0o600: + raise RuntimeError("host_key_private_mode") + + derived = run_keygen(["-y", "-f", str(private)]) + if derived.returncode != 0: + raise RuntimeError("host_key_private_invalid") + expected = public.read_text(encoding="ascii").strip().split() + actual = derived.stdout.strip().split() + if len(expected) < 2 or len(actual) < 2 or expected[:2] != actual[:2]: + raise RuntimeError("host_key_pair_mismatch") + + fingerprint = run_keygen(["-l", "-E", "sha256", "-f", str(public)]) + if fingerprint.returncode != 0: + raise RuntimeError("host_key_public_invalid") + fields = fingerprint.stdout.strip().split() + if len(fields) < 2 or not fields[1].startswith("SHA256:"): + raise RuntimeError("host_key_fingerprint_invalid") + return expected[0], fields[1] + + +def validate_store() -> list[dict]: + if STORE.is_symlink() or not STORE.is_dir(): + raise RuntimeError("host_key_store_unsafe") + if stat.S_IMODE(STORE.stat().st_mode) != 0o700: + raise RuntimeError("host_key_store_mode") + + identities = [] + for key_type, _extra in KEY_SPECS: + algorithm, fingerprint = public_identity(STORE / f"ssh_host_{key_type}_key") + identities.append({ + "type": key_type, + "algorithm": algorithm, + "fingerprint": fingerprint, + }) + return identities + + +def create_store() -> None: + temporary = STATE / f".ssh-hostkeys.{uuid.uuid4().hex}" + temporary.mkdir(mode=0o700) + try: + for key_type, extra in KEY_SPECS: + destination = temporary / f"ssh_host_{key_type}_key" + result = run_keygen([ + "-q", + "-t", + key_type, + *extra, + "-N", + "", + "-C", + "rigos-host", + "-f", + str(destination), + ]) + if result.returncode != 0: + raise RuntimeError(f"host_key_generate_{key_type}") + destination.chmod(0o600) + Path(f"{destination}.pub").chmod(0o644) + fsync_path(destination) + fsync_path(Path(f"{destination}.pub")) + fsync_path(temporary) + os.replace(temporary, STORE) + fsync_path(STATE) + finally: + if temporary.exists(): + for child in temporary.iterdir(): + child.unlink() + temporary.rmdir() + + +def main() -> int: + try: + if STATE.is_symlink() or not STATE.is_dir(): + return fail("state_path_unsafe") + RUNTIME.mkdir(parents=True, exist_ok=True) + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + if not boot_id: + return fail("boot_id_unavailable") + + created = False + if not STORE.exists(): + create_store() + created = True + + identities = validate_store() + atomic_json(STATUS, { + "schema": "rigos.ssh-hostkeys-status/v1", + "outcome": "ready", + "boot_id": boot_id, + "persistent": True, + "created": created, + "keys": identities, + }) + print(json.dumps({ + "schema": "rigos.ssh-hostkeys-gate/v1", + "outcome": "allowed", + "created": created, + "keys": identities, + }, sort_keys=True)) + return 0 + except (OSError, UnicodeError, RuntimeError, subprocess.SubprocessError) as error: + return fail(str(error)) + + +if __name__ == "__main__": + raise SystemExit(main()) From d56788188500efc2fc52d37be5951bcfeb020b46 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:09:42 +0700 Subject: [PATCH 080/386] stability bound miner restart behavior --- .../system/rigos-miner.service.d/stability.conf | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/stability.conf diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/stability.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/stability.conf new file mode 100644 index 00000000..5bf95881 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/stability.conf @@ -0,0 +1,17 @@ +[Unit] +StartLimitIntervalSec=10min +StartLimitBurst=5 + +[Service] +Restart=on-failure +RestartSec=15s +TimeoutStartSec=2min +TimeoutStopSec=30s +KillSignal=SIGINT +FinalKillSignal=SIGKILL +SendSIGKILL=yes +SuccessExitStatus=SIGINT SIGTERM +OOMPolicy=stop +OOMScoreAdjust=-500 +TasksMax=64 +LimitNOFILE=4096 From 3c2b42854d721ddf395d0540e7a882e029cd7de5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:12:18 +0700 Subject: [PATCH 081/386] stability observe miner liveness truth --- .../usr/lib/rigos/rigos-miner-health | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health new file mode 100644 index 00000000..10d876f3 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -0,0 +1,256 @@ +#!/usr/bin/python3 +import datetime as dt +import json +import os +import re +import subprocess +import sys +import uuid +from pathlib import Path + +RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) +BOOT_ID = Path(os.environ.get("RIGOS_BOOT_ID_PATH", "/proc/sys/kernel/random/boot_id")) +CURRENT = Path(os.environ.get("RIGOS_CURRENT_REVISION_PATH", "/var/lib/rigos/current")) +PROC = Path(os.environ.get("RIGOS_PROC_ROOT", "/proc")) +SYSTEMCTL = os.environ.get("RIGOS_SYSTEMCTL", "/usr/bin/systemctl") +JOURNALCTL = os.environ.get("RIGOS_JOURNALCTL", "/usr/bin/journalctl") +STATUS = RUNTIME / "miner-health-status.json" +WINDOW_SECONDS = int(os.environ.get("RIGOS_MINER_HEALTH_WINDOW_SECONDS", "180")) +WARMUP_SECONDS = int(os.environ.get("RIGOS_MINER_WARMUP_SECONDS", "240")) + +EXTERNAL_MARKERS = ( + "connect error", + "connection refused", + "connection reset", + "network error", + "dns error", + "no active pools", + "operation timed out", + "connection timeout", +) +SHARE_RE = re.compile(r"accepted \((\d+)/(\d+)\)") + + +def atomic_json(path: Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") + data = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n" + fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + directory = os.open(path.parent, os.O_RDONLY) + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + try: + temporary.unlink() + except FileNotFoundError: + pass + + +def command(argv: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + argv, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=10, + check=False, + ) + + +def systemd_properties() -> dict[str, str]: + result = command([ + SYSTEMCTL, + "show", + "rigos-miner.service", + "--property=ActiveState", + "--property=SubState", + "--property=MainPID", + "--property=NRestarts", + "--property=Result", + "--property=ExecMainStatus", + "--property=ActiveEnterTimestampMonotonic", + "--no-pager", + ]) + if result.returncode != 0: + raise RuntimeError("systemctl_show_failed") + values = {} + for line in result.stdout.splitlines(): + if "=" in line: + key, value = line.split("=", 1) + values[key] = value + return values + + +def current_revision() -> str | None: + try: + return CURRENT.resolve(strict=True).name + except OSError: + return None + + +def runtime_revision() -> tuple[str | None, str | None]: + try: + value = json.loads((RUNTIME / "runtime-config-status.json").read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + return None, None + if not isinstance(value, dict): + return None, None + return value.get("outcome"), value.get("revision") + + +def process_state(pid: int) -> str | None: + try: + raw = (PROC / str(pid) / "stat").read_text(encoding="ascii") + except (OSError, UnicodeError): + return None + end = raw.rfind(")") + if end < 0: + return None + fields = raw[end + 1 :].split() + return fields[0] if fields else None + + +def active_seconds(properties: dict[str, str]) -> int | None: + try: + entered = int(properties.get("ActiveEnterTimestampMonotonic", "0")) + uptime = float((PROC / "uptime").read_text(encoding="ascii").split()[0]) + except (OSError, UnicodeError, ValueError, IndexError): + return None + if entered <= 0: + return None + return max(0, int(uptime - entered / 1_000_000)) + + +def recent_journal() -> str: + result = command([ + JOURNALCTL, + "-b", + "-u", + "rigos-miner.service", + "--since", + f"-{WINDOW_SECONDS} seconds", + "--no-pager", + "-o", + "cat", + ]) + return result.stdout if result.returncode == 0 else "" + + +def last_share_counts(text: str) -> tuple[int | None, int | None]: + matches = SHARE_RE.findall(text) + if not matches: + return None, None + accepted, rejected = matches[-1] + return int(accepted), int(rejected) + + +def classify( + properties: dict[str, str], + proc_state: str | None, + active_for: int | None, + canonical_revision: str | None, + rendered_outcome: str | None, + rendered_revision: str | None, + journal: str, +) -> tuple[str, str | None]: + if properties.get("ActiveState") != "active" or properties.get("SubState") != "running": + return "failed", "miner_unit_not_running" + try: + pid = int(properties.get("MainPID", "0")) + except ValueError: + pid = 0 + if pid <= 0 or proc_state is None or proc_state in ("X", "Z"): + return "failed", "miner_process_unavailable" + if canonical_revision is None: + return "blocked", "current_revision_unavailable" + if rendered_outcome != "ready" or rendered_revision != canonical_revision: + return "blocked", "runtime_revision_mismatch" + + lowered = journal.lower() + if "miner speed" in lowered: + return "ready", None + if active_for is not None and active_for < WARMUP_SECONDS: + return "warming_up", None + if any(marker in lowered for marker in EXTERNAL_MARKERS): + return "waiting_external", "pool_or_network_unavailable" + return "degraded", "no_recent_speed_evidence" + + +def main() -> int: + try: + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + if not boot_id: + raise RuntimeError("boot_id_unavailable") + properties = systemd_properties() + try: + pid = int(properties.get("MainPID", "0")) + except ValueError: + pid = 0 + proc_state = process_state(pid) if pid > 0 else None + active_for = active_seconds(properties) + canonical = current_revision() + rendered_outcome, rendered_revision = runtime_revision() + journal = recent_journal() + accepted, rejected = last_share_counts(journal) + state, reason = classify( + properties, + proc_state, + active_for, + canonical, + rendered_outcome, + rendered_revision, + journal, + ) + value = { + "schema": "rigos.miner-health-status/v1", + "generated_at": dt.datetime.now(dt.timezone.utc).isoformat(timespec="milliseconds"), + "boot_id": boot_id, + "state": state, + "reason": reason, + "unit": { + "active_state": properties.get("ActiveState"), + "sub_state": properties.get("SubState"), + "result": properties.get("Result"), + "main_pid": pid or None, + "process_state": proc_state, + "restart_count": int(properties.get("NRestarts", "0") or 0), + "exec_main_status": int(properties.get("ExecMainStatus", "0") or 0), + "active_seconds": active_for, + }, + "config": { + "current_revision": canonical, + "runtime_outcome": rendered_outcome, + "runtime_revision": rendered_revision, + }, + "evidence": { + "window_seconds": WINDOW_SECONDS, + "recent_speed": "miner speed" in journal.lower(), + "external_wait_marker": any(marker in journal.lower() for marker in EXTERNAL_MARKERS), + "accepted_shares": accepted, + "rejected_shares": rejected, + }, + "remediation": "observe_only", + } + atomic_json(STATUS, value) + print(json.dumps({ + "schema": "rigos.miner-health-observer/v1", + "state": state, + "reason": reason, + "remediation": "observe_only", + }, sort_keys=True)) + return 0 + except (OSError, UnicodeError, RuntimeError, subprocess.SubprocessError, ValueError) as error: + print(f"rigos-miner-health: {error}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 2647f037726bf69173c1ee00c312a23cc4591429 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:12:36 +0700 Subject: [PATCH 082/386] stability add miner health observer --- .../systemd/system/rigos-miner-health.service | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service new file mode 100644 index 00000000..64923930 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service @@ -0,0 +1,19 @@ +[Unit] +Description=Observe RIGOS miner health truth +After=rigos-state-ready.service rigos-runtime-render.service rigos-miner.service +Wants=rigos-miner.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-miner-health +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ProtectSystem=strict +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +LockPersonality=yes +ReadWritePaths=/run/rigos From 65c709247c972ce9dbc920849c5635119defec02 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:12:54 +0700 Subject: [PATCH 083/386] stability sample miner health every minute --- .../etc/systemd/system/rigos-miner-health.timer | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.timer diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.timer b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.timer new file mode 100644 index 00000000..97de0c73 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Sample RIGOS miner health truth + +[Timer] +OnBootSec=2min +OnUnitActiveSec=1min +AccuracySec=5s +Unit=rigos-miner-health.service + +[Install] +WantedBy=timers.target From 51ba465343297b55e184aec70fbbe51bf5a087b5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:13:16 +0700 Subject: [PATCH 084/386] stability wire miner observer and restart policy --- build/usb/hooks/010-rigos.chroot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index f7e37ac5..9525437d 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -10,8 +10,8 @@ ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service tmp.mount +chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service rigos-miner-health.timer tmp.mount systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true From b793d6ffb24cd204ba150717b21f44d187800ef0 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:13:56 +0700 Subject: [PATCH 085/386] test miner stability observer and restart bounds --- crates/rigos-config/tests/miner_stability.rs | 176 +++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 crates/rigos-config/tests/miner_stability.rs diff --git a/crates/rigos-config/tests/miner_stability.rs b/crates/rigos-config/tests/miner_stability.rs new file mode 100644 index 00000000..3bc27e1b --- /dev/null +++ b/crates/rigos-config/tests/miner_stability.rs @@ -0,0 +1,176 @@ +use serde_json::Value; +use std::fs; +use std::os::unix::fs::{PermissionsExt, symlink}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use uuid::Uuid; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +fn write_executable(path: &Path, content: &str) { + fs::write(path, content).unwrap(); + let mut permissions = fs::metadata(path).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(path, permissions).unwrap(); +} + +fn write_runtime_status(path: &Path, revision: &str) { + fs::write( + path, + serde_json::to_vec(&serde_json::json!({ + "schema": "rigos.runtime-config-status/v1", + "outcome": "ready", + "revision": revision + })) + .unwrap(), + ) + .unwrap(); +} + +fn run_observer( + root: &Path, + systemctl: &Path, + journalctl: &Path, + systemctl_fixture: &Path, + journal_fixture: &Path, +) -> Value { + let status = Command::new("python3") + .arg(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health", + )) + .env("RIGOS_RUNTIME_PATH", root.join("run")) + .env("RIGOS_BOOT_ID_PATH", root.join("boot-id")) + .env("RIGOS_CURRENT_REVISION_PATH", root.join("state/current")) + .env("RIGOS_PROC_ROOT", root.join("proc")) + .env("RIGOS_SYSTEMCTL", systemctl) + .env("RIGOS_JOURNALCTL", journalctl) + .env("RIGOS_SYSTEMCTL_FIXTURE", systemctl_fixture) + .env("RIGOS_JOURNAL_FIXTURE", journal_fixture) + .status() + .unwrap(); + assert!(status.success()); + serde_json::from_slice( + &fs::read(root.join("run/miner-health-status.json")).unwrap(), + ) + .unwrap() +} + +#[test] +fn miner_health_distinguishes_ready_external_wait_degraded_and_blocked() { + let root = std::env::temp_dir().join(format!("rigos-miner-health-{}", Uuid::new_v4())); + fs::create_dir_all(root.join("run")).unwrap(); + fs::create_dir_all(root.join("state/revisions/r1")).unwrap(); + fs::create_dir_all(root.join("proc/123")).unwrap(); + symlink("revisions/r1", root.join("state/current")).unwrap(); + fs::write(root.join("boot-id"), "boot-test\n").unwrap(); + fs::write(root.join("proc/uptime"), "1000.0 0.0\n").unwrap(); + fs::write(root.join("proc/123/stat"), "123 (xmrig) S\n").unwrap(); + write_runtime_status(&root.join("run/runtime-config-status.json"), "r1"); + + let systemctl_fixture = root.join("systemctl.txt"); + fs::write( + &systemctl_fixture, + "ActiveState=active\nSubState=running\nMainPID=123\nNRestarts=2\nResult=success\nExecMainStatus=0\nActiveEnterTimestampMonotonic=100000000\n", + ) + .unwrap(); + let journal_fixture = root.join("journal.txt"); + let systemctl = root.join("systemctl"); + let journalctl = root.join("journalctl"); + write_executable( + &systemctl, + "#!/bin/sh\ncat \"$RIGOS_SYSTEMCTL_FIXTURE\"\n", + ); + write_executable( + &journalctl, + "#!/bin/sh\ncat \"$RIGOS_JOURNAL_FIXTURE\"\n", + ); + + fs::write( + &journal_fixture, + "miner speed 10s/60s/15m 340.0 341.0 n/a H/s\ncpu accepted (7/0) diff 10000\n", + ) + .unwrap(); + let ready = run_observer( + &root, + &systemctl, + &journalctl, + &systemctl_fixture, + &journal_fixture, + ); + assert_eq!(ready["state"], "ready"); + assert_eq!(ready["unit"]["restart_count"], 2); + assert_eq!(ready["evidence"]["accepted_shares"], 7); + assert_eq!(ready["evidence"]["rejected_shares"], 0); + assert_eq!(ready["remediation"], "observe_only"); + + fs::write(&journal_fixture, "net connect error: connection refused\n").unwrap(); + let waiting = run_observer( + &root, + &systemctl, + &journalctl, + &systemctl_fixture, + &journal_fixture, + ); + assert_eq!(waiting["state"], "waiting_external"); + assert_eq!(waiting["reason"], "pool_or_network_unavailable"); + + fs::write(&journal_fixture, "").unwrap(); + let degraded = run_observer( + &root, + &systemctl, + &journalctl, + &systemctl_fixture, + &journal_fixture, + ); + assert_eq!(degraded["state"], "degraded"); + assert_eq!(degraded["reason"], "no_recent_speed_evidence"); + + write_runtime_status(&root.join("run/runtime-config-status.json"), "r2"); + let blocked = run_observer( + &root, + &systemctl, + &journalctl, + &systemctl_fixture, + &journal_fixture, + ); + assert_eq!(blocked["state"], "blocked"); + assert_eq!(blocked["reason"], "runtime_revision_mismatch"); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn miner_restart_policy_is_bounded_and_observer_never_mutates_service() { + let stability = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/stability.conf", + )) + .unwrap(); + assert!(stability.contains("StartLimitIntervalSec=10min")); + assert!(stability.contains("StartLimitBurst=5")); + assert!(stability.contains("Restart=on-failure")); + assert!(stability.contains("RestartSec=15s")); + assert!(stability.contains("TimeoutStopSec=30s")); + assert!(!stability.contains("Restart=always")); + + let observer = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health", + )) + .unwrap(); + assert!(observer.contains("\"remediation\": \"observe_only\"")); + assert!(!observer.contains("systemctl restart")); + assert!(!observer.contains("systemctl kill")); + + let timer = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.timer", + )) + .unwrap(); + assert!(timer.contains("OnBootSec=2min")); + assert!(timer.contains("OnUnitActiveSec=1min")); + + let hook = fs::read_to_string(repo_path("build/usb/hooks/010-rigos.chroot")).unwrap(); + assert!(hook.contains("rigos-miner-health.timer")); +} From 466dfea4e89ae65e80db447073c1854b59880986 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:14:51 +0700 Subject: [PATCH 086/386] stability default inspector to redacted miner view --- build/usb/includes.chroot/usr/local/bin/rigosd | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 build/usb/includes.chroot/usr/local/bin/rigosd diff --git a/build/usb/includes.chroot/usr/local/bin/rigosd b/build/usb/includes.chroot/usr/local/bin/rigosd new file mode 100644 index 00000000..336a08dc --- /dev/null +++ b/build/usb/includes.chroot/usr/local/bin/rigosd @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +for argument in "$@"; do + case "$argument" in + --xmrig-config|--xmrig-config=*) + exec /usr/lib/rigos/rigosd "$@" + ;; + esac +done + +exec /usr/lib/rigos/rigosd \ + --xmrig-config /run/rigos/xmrig-public.json \ + "$@" From 5ef1b9d97569959f82fdb9472b999c7ae2dfd6a3 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:15:18 +0700 Subject: [PATCH 087/386] stability default control alias to redacted miner view --- build/usb/includes.chroot/usr/local/bin/rigosctl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 build/usb/includes.chroot/usr/local/bin/rigosctl diff --git a/build/usb/includes.chroot/usr/local/bin/rigosctl b/build/usb/includes.chroot/usr/local/bin/rigosctl new file mode 100644 index 00000000..593f87d5 --- /dev/null +++ b/build/usb/includes.chroot/usr/local/bin/rigosctl @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +for argument in "$@"; do + case "$argument" in + --xmrig-config|--xmrig-config=*) + exec /usr/lib/rigos/rigosctl "$@" + ;; + esac +done + +exec /usr/lib/rigos/rigosctl \ + --xmrig-config /run/rigos/xmrig-public.json \ + "$@" From 562ac49b5d76ebf4ef758e2f8aacd6bcdc4f7426 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:15:43 +0700 Subject: [PATCH 088/386] stability wire redacted inspector wrappers --- build/usb/hooks/010-rigos.chroot | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 9525437d..83da94f0 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -6,11 +6,9 @@ useradd --create-home --shell /bin/bash --groups sudo rigosadmin passwd --lock rigosadmin install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos /usr/local/bin -ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd -ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service rigos-miner-health.timer tmp.mount systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true From 368da9e3c4b4eb6de464944bea8a7a98c9469ac8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:16:30 +0700 Subject: [PATCH 089/386] stability expose safe inspector compatibility fields --- .../usr/lib/rigos/rigos-runtime-render | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render index 9e5a0fc2..3effedd9 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render @@ -102,6 +102,7 @@ def render() -> tuple[dict, dict, dict]: exact_threads = threads elif threads != "auto": raise RuntimeError("threads must be auto or an integer") + public = copy.deepcopy(runtime) for pool in public.get("pools", []): if isinstance(pool, dict): @@ -110,11 +111,25 @@ def render() -> tuple[dict, dict, dict]: http = public.get("http") if isinstance(http, dict): http.pop("access-token", None) + + public["algo"] = algorithm + if exact_threads is not None: + public["threads"] = exact_threads + else: + public.pop("threads", None) + public_randomx = public.get("randomx") + if not isinstance(public_randomx, dict): + public_randomx = {} + huge_pages = cpu.get("huge-pages") + if isinstance(huge_pages, bool): + public_randomx["huge-pages"] = huge_pages + public["randomx"] = public_randomx public["rigos-public-view"] = { "schema": "rigos.xmrig-public-config/v1", "identity_redacted": True, "source_revision": current.name, } + status = { "schema": "rigos.runtime-config-status/v1", "outcome": "ready", From fc658305e7b829da80e1d67a8ad2a70bfc5c4afd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:17:08 +0700 Subject: [PATCH 090/386] test redacted inspector wrapper wiring --- crates/rigos-config/tests/recovery_access.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index 4d6b5772..bed08c28 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -143,11 +143,21 @@ fn alpha8_runtime_authority_is_exact_and_fail_closed() { fn alpha8_appliance_wiring_is_explicit() { let hook = fs::read_to_string(repo_path("build/usb/hooks/010-rigos.chroot")) .expect("read appliance hook"); - assert!(hook.contains("ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd")); - assert!(hook.contains("ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl")); + assert!(hook.contains("chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl")); + assert!(!hook.contains("ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd")); + assert!(!hook.contains("ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl")); assert!(hook.contains("rigos-runtime-render.service")); assert!(hook.contains("systemctl disable ssh.socket")); + for command in ["rigosd", "rigosctl"] { + let wrapper = fs::read_to_string(repo_path(&format!( + "build/usb/includes.chroot/usr/local/bin/{command}" + ))) + .expect("read inspector wrapper"); + assert!(wrapper.contains("--xmrig-config /run/rigos/xmrig-public.json")); + assert!(wrapper.contains(&format!("exec /usr/lib/rigos/{command}"))); + } + let miner = fs::read_to_string(repo_path( "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf", )) From 8f2b32209b1c304e0c149cb84205c41c69175bfe Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:17:49 +0700 Subject: [PATCH 091/386] test redacted inspector runtime contract --- scripts/check-alpha8-runtime.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/check-alpha8-runtime.py b/scripts/check-alpha8-runtime.py index cd67fff7..21846204 100644 --- a/scripts/check-alpha8-runtime.py +++ b/scripts/check-alpha8-runtime.py @@ -48,8 +48,18 @@ def verify_runtime_authority(root: Path) -> None: "huge-pages": True, "max-threads-hint": 2, }, - "pools": [{"url": "pool.test:1", "algo": "rx/0"}], - "http": {"enabled": False}, + "pools": [ + { + "url": "pool.test:1", + "algo": "rx/0", + "user": "fixture-private-identity", + "pass": "fixture-worker", + } + ], + "http": { + "enabled": False, + "access-token": "fixture-private-token", + }, }, ) environment = os.environ.copy() @@ -64,6 +74,21 @@ def verify_runtime_authority(root: Path) -> None: config = json.loads((runtime / "xmrig.json").read_text(encoding="utf-8")) assert config["cpu"]["max-threads-hint"] == 100 assert config["cpu"]["rx"] == [-1, -1] + assert config["pools"][0]["user"] == "fixture-private-identity" + + public = json.loads((runtime / "xmrig-public.json").read_text(encoding="utf-8")) + assert public["algo"] == "rx/0" + assert public["threads"] == 2 + assert public["randomx"]["huge-pages"] is True + assert public["rigos-public-view"]["identity_redacted"] is True + assert "user" not in public["pools"][0] + assert "pass" not in public["pools"][0] + assert "access-token" not in public["http"] + public_text = json.dumps(public, sort_keys=True) + assert "fixture-private-identity" not in public_text + assert "fixture-worker" not in public_text + assert "fixture-private-token" not in public_text + status = json.loads( (runtime / "runtime-config-status.json").read_text(encoding="utf-8") ) @@ -142,7 +167,7 @@ def main() -> int: root = Path(temporary) verify_runtime_authority(root) verify_remote_truth(root) - print("RIGOS Alpha8 runtime and remote truth verification passed") + print("RIGOS runtime, redacted inspector, and remote truth verification passed") return 0 From 148c9526a6d0c4cc4c045bbf21e480e202c4c937 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:18:52 +0700 Subject: [PATCH 092/386] remove unwired SSH host key prototype --- .../usr/lib/rigos/rigos-ssh-hostkeys | 179 ------------------ 1 file changed, 179 deletions(-) delete mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys deleted file mode 100644 index 8b79ced6..00000000 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/python3 -import json -import os -import stat -import subprocess -import sys -import uuid -from pathlib import Path - -STATE = Path(os.environ.get("RIGOS_STATE_PATH", "/var/lib/rigos")) -RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) -BOOT_ID = Path(os.environ.get("RIGOS_BOOT_ID_PATH", "/proc/sys/kernel/random/boot_id")) -KEYGEN = os.environ.get("RIGOS_SSH_KEYGEN", "/usr/bin/ssh-keygen") -STORE = STATE / "ssh-hostkeys" -STATUS = RUNTIME / "ssh-hostkeys-status.json" -KEY_SPECS = ( - ("ed25519", ()), - ("rsa", ("-b", "3072")), -) - - -def fail(reason: str) -> int: - print(f"rigos-ssh-hostkeys: {reason}", file=sys.stderr) - return 2 - - -def fsync_path(path: Path) -> None: - fd = os.open(path, os.O_RDONLY) - try: - os.fsync(fd) - finally: - os.close(fd) - - -def atomic_json(path: Path, value: dict) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") - data = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n" - fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) - try: - with os.fdopen(fd, "w", encoding="utf-8") as handle: - handle.write(data) - handle.flush() - os.fsync(handle.fileno()) - os.replace(temporary, path) - fsync_path(path.parent) - finally: - try: - temporary.unlink() - except FileNotFoundError: - pass - - -def run_keygen(arguments: list[str]) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [KEYGEN, *arguments], - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=20, - check=False, - ) - - -def public_identity(path: Path) -> tuple[str, str]: - private = path - public = Path(f"{path}.pub") - if private.is_symlink() or public.is_symlink(): - raise RuntimeError("host_key_symlink") - if not private.is_file() or not public.is_file(): - raise RuntimeError("host_key_incomplete") - private_mode = stat.S_IMODE(private.stat().st_mode) - if private_mode != 0o600: - raise RuntimeError("host_key_private_mode") - - derived = run_keygen(["-y", "-f", str(private)]) - if derived.returncode != 0: - raise RuntimeError("host_key_private_invalid") - expected = public.read_text(encoding="ascii").strip().split() - actual = derived.stdout.strip().split() - if len(expected) < 2 or len(actual) < 2 or expected[:2] != actual[:2]: - raise RuntimeError("host_key_pair_mismatch") - - fingerprint = run_keygen(["-l", "-E", "sha256", "-f", str(public)]) - if fingerprint.returncode != 0: - raise RuntimeError("host_key_public_invalid") - fields = fingerprint.stdout.strip().split() - if len(fields) < 2 or not fields[1].startswith("SHA256:"): - raise RuntimeError("host_key_fingerprint_invalid") - return expected[0], fields[1] - - -def validate_store() -> list[dict]: - if STORE.is_symlink() or not STORE.is_dir(): - raise RuntimeError("host_key_store_unsafe") - if stat.S_IMODE(STORE.stat().st_mode) != 0o700: - raise RuntimeError("host_key_store_mode") - - identities = [] - for key_type, _extra in KEY_SPECS: - algorithm, fingerprint = public_identity(STORE / f"ssh_host_{key_type}_key") - identities.append({ - "type": key_type, - "algorithm": algorithm, - "fingerprint": fingerprint, - }) - return identities - - -def create_store() -> None: - temporary = STATE / f".ssh-hostkeys.{uuid.uuid4().hex}" - temporary.mkdir(mode=0o700) - try: - for key_type, extra in KEY_SPECS: - destination = temporary / f"ssh_host_{key_type}_key" - result = run_keygen([ - "-q", - "-t", - key_type, - *extra, - "-N", - "", - "-C", - "rigos-host", - "-f", - str(destination), - ]) - if result.returncode != 0: - raise RuntimeError(f"host_key_generate_{key_type}") - destination.chmod(0o600) - Path(f"{destination}.pub").chmod(0o644) - fsync_path(destination) - fsync_path(Path(f"{destination}.pub")) - fsync_path(temporary) - os.replace(temporary, STORE) - fsync_path(STATE) - finally: - if temporary.exists(): - for child in temporary.iterdir(): - child.unlink() - temporary.rmdir() - - -def main() -> int: - try: - if STATE.is_symlink() or not STATE.is_dir(): - return fail("state_path_unsafe") - RUNTIME.mkdir(parents=True, exist_ok=True) - boot_id = BOOT_ID.read_text(encoding="ascii").strip() - if not boot_id: - return fail("boot_id_unavailable") - - created = False - if not STORE.exists(): - create_store() - created = True - - identities = validate_store() - atomic_json(STATUS, { - "schema": "rigos.ssh-hostkeys-status/v1", - "outcome": "ready", - "boot_id": boot_id, - "persistent": True, - "created": created, - "keys": identities, - }) - print(json.dumps({ - "schema": "rigos.ssh-hostkeys-gate/v1", - "outcome": "allowed", - "created": created, - "keys": identities, - }, sort_keys=True)) - return 0 - except (OSError, UnicodeError, RuntimeError, subprocess.SubprocessError) as error: - return fail(str(error)) - - -if __name__ == "__main__": - raise SystemExit(main()) From a3b062a3bf1073c31defdff9e80eeb029e07a509 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:19:43 +0700 Subject: [PATCH 093/386] stability keep miner observer non-mutating --- .../etc/systemd/system/rigos-miner-health.service | 1 - 1 file changed, 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service index 64923930..bd3523ea 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service @@ -1,7 +1,6 @@ [Unit] Description=Observe RIGOS miner health truth After=rigos-state-ready.service rigos-runtime-render.service rigos-miner.service -Wants=rigos-miner.service [Service] Type=oneshot From 8e3a96c137b44602588c42216ef6dcb5dff78455 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:20:30 +0700 Subject: [PATCH 094/386] stability make miner observation bounded and truthful --- .../usr/lib/rigos/rigos-miner-health | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index 10d876f3..39221147 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -17,6 +17,7 @@ JOURNALCTL = os.environ.get("RIGOS_JOURNALCTL", "/usr/bin/journalctl") STATUS = RUNTIME / "miner-health-status.json" WINDOW_SECONDS = int(os.environ.get("RIGOS_MINER_HEALTH_WINDOW_SECONDS", "180")) WARMUP_SECONDS = int(os.environ.get("RIGOS_MINER_WARMUP_SECONDS", "240")) +MAX_JOURNAL_LINES = 500 EXTERNAL_MARKERS = ( "connect error", @@ -129,7 +130,7 @@ def active_seconds(properties: dict[str, str]) -> int | None: return max(0, int(uptime - entered / 1_000_000)) -def recent_journal() -> str: +def recent_journal() -> tuple[str, bool]: result = command([ JOURNALCTL, "-b", @@ -137,11 +138,13 @@ def recent_journal() -> str: "rigos-miner.service", "--since", f"-{WINDOW_SECONDS} seconds", + "--lines", + str(MAX_JOURNAL_LINES), "--no-pager", "-o", "cat", ]) - return result.stdout if result.returncode == 0 else "" + return result.stdout, result.returncode == 0 def last_share_counts(text: str) -> tuple[int | None, int | None]: @@ -160,6 +163,7 @@ def classify( rendered_outcome: str | None, rendered_revision: str | None, journal: str, + journal_available: bool, ) -> tuple[str, str | None]: if properties.get("ActiveState") != "active" or properties.get("SubState") != "running": return "failed", "miner_unit_not_running" @@ -173,14 +177,16 @@ def classify( return "blocked", "current_revision_unavailable" if rendered_outcome != "ready" or rendered_revision != canonical_revision: return "blocked", "runtime_revision_mismatch" + if not journal_available: + return "unknown", "journal_unavailable" lowered = journal.lower() if "miner speed" in lowered: return "ready", None - if active_for is not None and active_for < WARMUP_SECONDS: - return "warming_up", None if any(marker in lowered for marker in EXTERNAL_MARKERS): return "waiting_external", "pool_or_network_unavailable" + if active_for is not None and active_for < WARMUP_SECONDS: + return "warming_up", None return "degraded", "no_recent_speed_evidence" @@ -198,7 +204,7 @@ def main() -> int: active_for = active_seconds(properties) canonical = current_revision() rendered_outcome, rendered_revision = runtime_revision() - journal = recent_journal() + journal, journal_available = recent_journal() accepted, rejected = last_share_counts(journal) state, reason = classify( properties, @@ -208,7 +214,9 @@ def main() -> int: rendered_outcome, rendered_revision, journal, + journal_available, ) + lowered_journal = journal.lower() value = { "schema": "rigos.miner-health-status/v1", "generated_at": dt.datetime.now(dt.timezone.utc).isoformat(timespec="milliseconds"), @@ -232,8 +240,12 @@ def main() -> int: }, "evidence": { "window_seconds": WINDOW_SECONDS, - "recent_speed": "miner speed" in journal.lower(), - "external_wait_marker": any(marker in journal.lower() for marker in EXTERNAL_MARKERS), + "journal_available": journal_available, + "journal_line_limit": MAX_JOURNAL_LINES, + "recent_speed": "miner speed" in lowered_journal, + "external_wait_marker": any( + marker in lowered_journal for marker in EXTERNAL_MARKERS + ), "accepted_shares": accepted, "rejected_shares": rejected, }, From bf419bd901236d029240b29d66baccd41213584b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:21:21 +0700 Subject: [PATCH 095/386] test non-mutating and unknown miner health states --- crates/rigos-config/tests/miner_stability.rs | 45 ++++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/rigos-config/tests/miner_stability.rs b/crates/rigos-config/tests/miner_stability.rs index 3bc27e1b..7c98515c 100644 --- a/crates/rigos-config/tests/miner_stability.rs +++ b/crates/rigos-config/tests/miner_stability.rs @@ -1,6 +1,6 @@ use serde_json::Value; use std::fs; -use std::os::unix::fs::{PermissionsExt, symlink}; +use std::os::unix::fs::{symlink, PermissionsExt}; use std::path::{Path, PathBuf}; use std::process::Command; use uuid::Uuid; @@ -53,14 +53,11 @@ fn run_observer( .status() .unwrap(); assert!(status.success()); - serde_json::from_slice( - &fs::read(root.join("run/miner-health-status.json")).unwrap(), - ) - .unwrap() + serde_json::from_slice(&fs::read(root.join("run/miner-health-status.json")).unwrap()).unwrap() } #[test] -fn miner_health_distinguishes_ready_external_wait_degraded_and_blocked() { +fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() { let root = std::env::temp_dir().join(format!("rigos-miner-health-{}", Uuid::new_v4())); fs::create_dir_all(root.join("run")).unwrap(); fs::create_dir_all(root.join("state/revisions/r1")).unwrap(); @@ -74,12 +71,21 @@ fn miner_health_distinguishes_ready_external_wait_degraded_and_blocked() { let systemctl_fixture = root.join("systemctl.txt"); fs::write( &systemctl_fixture, - "ActiveState=active\nSubState=running\nMainPID=123\nNRestarts=2\nResult=success\nExecMainStatus=0\nActiveEnterTimestampMonotonic=100000000\n", + concat!( + "ActiveState=active\n", + "SubState=running\n", + "MainPID=123\n", + "NRestarts=2\n", + "Result=success\n", + "ExecMainStatus=0\n", + "ActiveEnterTimestampMonotonic=100000000\n" + ), ) .unwrap(); let journal_fixture = root.join("journal.txt"); let systemctl = root.join("systemctl"); let journalctl = root.join("journalctl"); + let journalctl_fail = root.join("journalctl-fail"); write_executable( &systemctl, "#!/bin/sh\ncat \"$RIGOS_SYSTEMCTL_FIXTURE\"\n", @@ -88,10 +94,14 @@ fn miner_health_distinguishes_ready_external_wait_degraded_and_blocked() { &journalctl, "#!/bin/sh\ncat \"$RIGOS_JOURNAL_FIXTURE\"\n", ); + write_executable(&journalctl_fail, "#!/bin/sh\nexit 1\n"); fs::write( &journal_fixture, - "miner speed 10s/60s/15m 340.0 341.0 n/a H/s\ncpu accepted (7/0) diff 10000\n", + concat!( + "miner speed 10s/60s/15m 340.0 341.0 n/a H/s\n", + "cpu accepted (7/0) diff 10000\n" + ), ) .unwrap(); let ready = run_observer( @@ -129,6 +139,17 @@ fn miner_health_distinguishes_ready_external_wait_degraded_and_blocked() { assert_eq!(degraded["state"], "degraded"); assert_eq!(degraded["reason"], "no_recent_speed_evidence"); + let unknown = run_observer( + &root, + &systemctl, + &journalctl_fail, + &systemctl_fixture, + &journal_fixture, + ); + assert_eq!(unknown["state"], "unknown"); + assert_eq!(unknown["reason"], "journal_unavailable"); + assert_eq!(unknown["evidence"]["journal_available"], false); + write_runtime_status(&root.join("run/runtime-config-status.json"), "r2"); let blocked = run_observer( &root, @@ -161,9 +182,17 @@ fn miner_restart_policy_is_bounded_and_observer_never_mutates_service() { )) .unwrap(); assert!(observer.contains("\"remediation\": \"observe_only\"")); + assert!(observer.contains("MAX_JOURNAL_LINES = 500")); assert!(!observer.contains("systemctl restart")); assert!(!observer.contains("systemctl kill")); + let service = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service", + )) + .unwrap(); + assert!(!service.contains("Wants=rigos-miner.service")); + assert!(!service.contains("Requires=rigos-miner.service")); + let timer = fs::read_to_string(repo_path( "build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.timer", )) From 53efcb826f35eef72ef14924c6201e65f7d68217 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:28:51 +0700 Subject: [PATCH 096/386] format miner stability fixture --- crates/rigos-config/tests/miner_stability.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/rigos-config/tests/miner_stability.rs b/crates/rigos-config/tests/miner_stability.rs index 7c98515c..37a6405c 100644 --- a/crates/rigos-config/tests/miner_stability.rs +++ b/crates/rigos-config/tests/miner_stability.rs @@ -1,6 +1,6 @@ use serde_json::Value; use std::fs; -use std::os::unix::fs::{symlink, PermissionsExt}; +use std::os::unix::fs::{PermissionsExt, symlink}; use std::path::{Path, PathBuf}; use std::process::Command; use uuid::Uuid; @@ -86,14 +86,8 @@ fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() let systemctl = root.join("systemctl"); let journalctl = root.join("journalctl"); let journalctl_fail = root.join("journalctl-fail"); - write_executable( - &systemctl, - "#!/bin/sh\ncat \"$RIGOS_SYSTEMCTL_FIXTURE\"\n", - ); - write_executable( - &journalctl, - "#!/bin/sh\ncat \"$RIGOS_JOURNAL_FIXTURE\"\n", - ); + write_executable(&systemctl, "#!/bin/sh\ncat \"$RIGOS_SYSTEMCTL_FIXTURE\"\n"); + write_executable(&journalctl, "#!/bin/sh\ncat \"$RIGOS_JOURNAL_FIXTURE\"\n"); write_executable(&journalctl_fail, "#!/bin/sh\nexit 1\n"); fs::write( From b784dff620dd3f6eb7d690e8e9d73cb4d01c09be Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:39:50 +0700 Subject: [PATCH 097/386] ci apply hostile review fixes --- .github/workflows/hive-exit-review-fixes.yml | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/hive-exit-review-fixes.yml diff --git a/.github/workflows/hive-exit-review-fixes.yml b/.github/workflows/hive-exit-review-fixes.yml new file mode 100644 index 00000000..258847ef --- /dev/null +++ b/.github/workflows/hive-exit-review-fixes.yml @@ -0,0 +1,47 @@ +name: Apply Hive Exit Review Fixes + +on: + push: + branches: + - stability/hive-exit-gates + paths: + - scripts/apply-hive-exit-review-fixes.py + +permissions: + contents: write + +jobs: + apply: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: stability/hive-exit-gates + fetch-depth: 0 + + - uses: dtolnay/rust-toolchain@1.85.1 + with: + components: rustfmt + + - name: Apply exact source patches + run: python3 scripts/apply-hive-exit-review-fixes.py + + - name: Verify patched sources + run: | + cargo fmt --all + cargo fmt --all -- --check + cargo test -p rigos-xmrig --lib + python3 scripts/check-alpha8-runtime.py + git diff --check + + - name: Commit verified review fixes + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add crates/rigos-xmrig/src/lib.rs \ + build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render \ + scripts/check-alpha8-runtime.py + git diff --cached --quiet && exit 0 + git commit -m "stability enforce public inspector boundary" + git push origin HEAD:stability/hive-exit-gates From c3ffa4bff2a5cec26f433248396671fbf1bc8257 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:40:40 +0700 Subject: [PATCH 098/386] apply hostile review source fixes --- scripts/apply-hive-exit-review-fixes.py | 253 ++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 scripts/apply-hive-exit-review-fixes.py diff --git a/scripts/apply-hive-exit-review-fixes.py b/scripts/apply-hive-exit-review-fixes.py new file mode 100644 index 00000000..f266812f --- /dev/null +++ b/scripts/apply-hive-exit-review-fixes.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def replace_once(path: Path, old: str, new: str) -> None: + text = path.read_text(encoding="utf-8") + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected exactly one match in {path}, found {count}") + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def patch_xmrig_backend() -> None: + path = ROOT / "crates/rigos-xmrig/src/lib.rs" + replace_once( + path, + """ let config_path = cmdline + .as_deref() + .and_then(extract_config_path) + .or_else(|| self.explicit_config.clone());""", + """ let config_path = self + .explicit_config + .clone() + .or_else(|| cmdline.as_deref().and_then(extract_config_path));""", + ) + + marker = """ #[test] + fn discovers_xmrig_from_synthetic_proc_without_mutation() {""" + test = """ #[test] + fn explicit_config_overrides_process_cmdline_config() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-proc-explicit-{unique}")); + let proc_root = root.join("proc"); + let pid_dir = proc_root.join("42"); + fs::create_dir_all(&pid_dir).unwrap(); + let private = root.join("private.json"); + let public = root.join("public.json"); + fs::write( + &private, + r#"{"algo":"rx/private","http":{"enabled":false}}"#, + ) + .unwrap(); + fs::write( + &public, + r#"{"algo":"rx/0","threads":2,"randomx":{"huge-pages":true},"http":{"enabled":false}}"#, + ) + .unwrap(); + fs::write(pid_dir.join("comm"), "xmrig\n").unwrap(); + fs::write( + pid_dir.join("cmdline"), + format!("xmrig\0--config={}\0", private.display()), + ) + .unwrap(); + fs::write( + pid_dir.join("status"), + "Name:\txmrig\nUid:\t1000 1000 1000 1000\n", + ) + .unwrap(); + fs::write(pid_dir.join("cgroup"), "0::/system.slice/xmrig.service\n").unwrap(); + fs::write( + pid_dir.join("stat"), + "42 (xmrig) S 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 100 0\n", + ) + .unwrap(); + fs::write(proc_root.join("uptime"), "100.0 0.0\n").unwrap(); + + let expected_path = public.to_string_lossy().into_owned(); + let backend = XmrigBackend { + explicit_executable: None, + explicit_config: Some(public), + probe_version: false, + }; + let result = backend.discover(&MachineContext { + proc_root, + sys_root: root.join("sys"), + }); + let _ = fs::remove_dir_all(root); + let snapshot = result.value.unwrap(); + assert_eq!(snapshot.config.path, Some(expected_path)); + assert_eq!(snapshot.config.algorithm.as_deref(), Some("rx/0")); + assert_eq!(snapshot.config.thread_hint, Some(2)); + assert_eq!(snapshot.config.huge_pages_requested, Some(true)); + } + +""" + replace_once(path, marker, test + marker) + + +def patch_runtime_renderer() -> None: + path = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render" + helper_marker = """def render() -> tuple[dict, dict, dict]:""" + helpers = """def public_pool(pool: dict) -> dict: + safe = {} + url = pool.get("url") + if isinstance(url, str) and url: + safe["url"] = url.rsplit("@", 1)[-1] + algo = pool.get("algo") + if isinstance(algo, str) and algo: + safe["algo"] = algo + for key in ("tls", "nicehash", "keepalive"): + value = pool.get(key) + if isinstance(value, bool): + safe[key] = value + priority = pool.get("priority") + if isinstance(priority, int) and not isinstance(priority, bool): + safe["priority"] = priority + return safe + + +def public_http(http: object) -> dict: + if not isinstance(http, dict): + return {"enabled": False} + safe = {} + enabled = http.get("enabled") + safe["enabled"] = enabled if isinstance(enabled, bool) else False + host = http.get("host") + if isinstance(host, str) and host: + safe["host"] = host + port = http.get("port") + if isinstance(port, int) and not isinstance(port, bool) and 1 <= port <= 65535: + safe["port"] = port + restricted = http.get("restricted") + if isinstance(restricted, bool): + safe["restricted"] = restricted + return safe + + +""" + replace_once(path, helper_marker, helpers + helper_marker) + + replace_once( + path, + """ public = copy.deepcopy(runtime) + for pool in public.get("pools", []): + if isinstance(pool, dict): + pool.pop("user", None) + pool.pop("pass", None) + http = public.get("http") + if isinstance(http, dict): + http.pop("access-token", None) + + public["algo"] = algorithm + if exact_threads is not None: + public["threads"] = exact_threads + else: + public.pop("threads", None) + public_randomx = public.get("randomx") + if not isinstance(public_randomx, dict): + public_randomx = {} + huge_pages = cpu.get("huge-pages") + if isinstance(huge_pages, bool): + public_randomx["huge-pages"] = huge_pages + public["randomx"] = public_randomx + public["rigos-public-view"] = { + "schema": "rigos.xmrig-public-config/v1", + "identity_redacted": True, + "source_revision": current.name, + } +""", + """ huge_pages = cpu.get("huge-pages") + public_cpu = {} + enabled = cpu.get("enabled") + if isinstance(enabled, bool): + public_cpu["enabled"] = enabled + if isinstance(huge_pages, bool): + public_cpu["huge-pages"] = huge_pages + max_threads_hint = cpu.get("max-threads-hint") + if isinstance(max_threads_hint, int) and not isinstance(max_threads_hint, bool): + public_cpu["max-threads-hint"] = max_threads_hint + if profile is not None and exact_threads is not None: + public_cpu[profile] = [-1] * exact_threads + + public = { + "algo": algorithm, + "cpu": public_cpu, + "http": public_http(runtime.get("http")), + "pools": [public_pool(pool) for pool in pools], + "randomx": { + "huge-pages": huge_pages if isinstance(huge_pages, bool) else False, + }, + "rigos-public-view": { + "schema": "rigos.xmrig-public-config/v1", + "identity_redacted": True, + "construction": "allowlist", + "source_revision": current.name, + }, + } + if exact_threads is not None: + public["threads"] = exact_threads +""", + ) + + replace_once( + path, + ' "identity_redacted_public_view": True,\n', + ' "identity_redacted_public_view": True,\n "public_view_construction": "allowlist",\n', + ) + + +def patch_runtime_fixture() -> None: + path = ROOT / "scripts/check-alpha8-runtime.py" + replace_once( + path, + ' "max-threads-hint": 2,\n', + ' "max-threads-hint": 2,\n "future-secret": "fixture-cpu-secret",\n', + ) + replace_once( + path, + ' "pass": "fixture-worker",\n', + ' "pass": "fixture-worker",\n "future-secret": "fixture-pool-secret",\n', + ) + replace_once( + path, + ' "access-token": "fixture-private-token",\n', + ' "access-token": "fixture-private-token",\n "future-secret": "fixture-http-secret",\n', + ) + replace_once( + path, + ' "http": {\n', + ' "future-secret": "fixture-top-secret",\n "http": {\n', + ) + replace_once( + path, + ' assert public["rigos-public-view"]["identity_redacted"] is True\n', + ' assert public["rigos-public-view"]["identity_redacted"] is True\n assert public["rigos-public-view"]["construction"] == "allowlist"\n assert set(public) == {"algo", "cpu", "http", "pools", "randomx", "rigos-public-view", "threads"}\n assert set(public["pools"][0]) <= {"url", "algo", "tls", "nicehash", "keepalive", "priority"}\n assert set(public["http"]) <= {"enabled", "host", "port", "restricted"}\n', + ) + replace_once( + path, + ' assert "fixture-private-token" not in public_text\n', + ' assert "fixture-private-token" not in public_text\n assert "fixture-cpu-secret" not in public_text\n assert "fixture-pool-secret" not in public_text\n assert "fixture-http-secret" not in public_text\n assert "fixture-top-secret" not in public_text\n', + ) + replace_once( + path, + ' assert status["profile"] == "rx"\n', + ' assert status["profile"] == "rx"\n assert status["public_view_construction"] == "allowlist"\n', + ) + + +def main() -> int: + patch_xmrig_backend() + patch_runtime_renderer() + patch_runtime_fixture() + print("Hive Exit hostile review fixes applied") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 7cb7d4d63262367e1a86852148bd90ad5e47e3c3 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 20:52:54 +0700 Subject: [PATCH 099/386] document hostile review findings --- docs/hive-exit-hostile-review.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/hive-exit-hostile-review.md diff --git a/docs/hive-exit-hostile-review.md b/docs/hive-exit-hostile-review.md new file mode 100644 index 00000000..5d731dba --- /dev/null +++ b/docs/hive-exit-hostile-review.md @@ -0,0 +1,8 @@ +# Hive Exit hostile review + +The first stability batch passed source verification, but review found two release blockers before image construction: + +1. An explicit inspector configuration must take precedence over a configuration discovered from the running process command line. +2. The public XMRig view must be constructed from an allowlist. Copying the private configuration and deleting known secret fields is not an acceptable security boundary. + +The review fix must pass Rust formatting, the `rigos-xmrig` unit suite, the runtime redaction fixture, and `git diff --check` before it is committed to the stability branch. From 42f71f5a0e95d92d8742bde78177cef02d846766 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Tue, 7 Jul 2026 20:56:18 +0700 Subject: [PATCH 100/386] trigger Hive Exit hostile review From b9f1f3d304f5d7ffbb7638b9c85e49d4140bad05 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:00:10 +0700 Subject: [PATCH 101/386] ci run Hive Exit review fixes on branch pushes --- .github/workflows/hive-exit-review-fixes.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/hive-exit-review-fixes.yml b/.github/workflows/hive-exit-review-fixes.yml index 258847ef..0f9644b5 100644 --- a/.github/workflows/hive-exit-review-fixes.yml +++ b/.github/workflows/hive-exit-review-fixes.yml @@ -4,8 +4,6 @@ on: push: branches: - stability/hive-exit-gates - paths: - - scripts/apply-hive-exit-review-fixes.py permissions: contents: write From c9cbbc86736077b48ed6b900f26a559dd1db7355 Mon Sep 17 00:00:00 2001 From: Deadbytes101 Date: Tue, 7 Jul 2026 21:20:10 +0700 Subject: [PATCH 102/386] run Hive Exit review fixes From c23847ce3042db777c50101dd15156960961ee40 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:51:39 +0700 Subject: [PATCH 103/386] stability publish staged runtime allowlist --- .../usr/lib/rigos/rigos-runtime-publish | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish new file mode 100644 index 00000000..d7bb471c --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish @@ -0,0 +1,99 @@ +#!/bin/sh +set -eu + +runtime=${RIGOS_RUNTIME_PATH:-/run/rigos} +renderer=${RIGOS_RUNTIME_RENDERER:-/usr/lib/rigos/rigos-runtime-render} +mkdir -p "$runtime" +chmod 0755 "$runtime" +stage=$(mktemp -d "$runtime/.render-stage.XXXXXX") +chmod 0700 "$stage" +cleanup() { + rm -rf "$stage" +} +trap cleanup EXIT HUP INT TERM + +RIGOS_RUNTIME_PATH="$stage" "$renderer" + +jq -e ' + .schema == "rigos.runtime-config-status/v1" and + .outcome == "ready" and + (.revision | type == "string") and + (.algorithm | type == "string") +' "$stage/runtime-config-status.json" >/dev/null + +algorithm=$(jq -r '.algorithm' "$stage/runtime-config-status.json") +revision=$(jq -r '.revision' "$stage/runtime-config-status.json") +thread_count=$(jq -r '.exact_threads // empty' "$stage/runtime-config-status.json") +profile=$(jq -r '.profile // empty' "$stage/runtime-config-status.json") + +public_tmp=$(mktemp "$runtime/.xmrig-public.XXXXXX") +status_tmp=$(mktemp "$runtime/.runtime-status.XXXXXX") +config_tmp=$(mktemp "$runtime/.xmrig-runtime.XXXXXX") +trap 'rm -f "$public_tmp" "$status_tmp" "$config_tmp"; cleanup' EXIT HUP INT TERM + +jq \ + --arg algorithm "$algorithm" \ + --arg revision "$revision" \ + --arg profile "$profile" \ + --argjson thread_count "${thread_count:-null}" ' + def present: with_entries(select(.value != null)); + def endpoint: + if type == "string" then split("@") | last else null end; + { + algo: $algorithm, + cpu: ({ + enabled: .cpu.enabled, + "huge-pages": .cpu["huge-pages"], + "max-threads-hint": .cpu["max-threads-hint"] + } | present), + pools: [ + .pools[] | ({ + url: (.url | endpoint), + algo: .algo, + tls: .tls, + nicehash: .nicehash, + keepalive: .keepalive, + priority: .priority + } | present) + ], + http: ({ + enabled: (.http.enabled // false), + host: .http.host, + port: .http.port, + restricted: .http.restricted + } | present), + randomx: { + "huge-pages": (.cpu["huge-pages"] // false) + }, + "rigos-public-view": { + schema: "rigos.xmrig-public-config/v1", + identity_redacted: true, + construction: "allowlist", + source_revision: $revision + } + } + | if $thread_count != null then .threads = $thread_count else . end + | if ($profile | length) > 0 and $thread_count != null + then .cpu[$profile] = [range(0; $thread_count) | -1] + else . + end +' "$stage/xmrig.json" >"$public_tmp" + +jq '. + { + identity_redacted_public_view: true, + public_view_construction: "allowlist" +}' "$stage/runtime-config-status.json" >"$status_tmp" + +cp "$stage/xmrig.json" "$config_tmp" +chmod 0640 "$config_tmp" +if [ "${RIGOS_RENDER_SKIP_CHOWN:-0}" != 1 ]; then + chown root:rigos "$config_tmp" +fi +chmod 0644 "$public_tmp" "$status_tmp" +sync -f "$config_tmp" +sync -f "$public_tmp" +sync -f "$status_tmp" +mv -f "$config_tmp" "$runtime/xmrig.json" +mv -f "$public_tmp" "$runtime/xmrig-public.json" +mv -f "$status_tmp" "$runtime/runtime-config-status.json" +sync -f "$runtime" From 96acf9cb68aebb665cac13626407faad45943ea1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:52:04 +0700 Subject: [PATCH 104/386] stability gate runtime publication through staged authority --- .../etc/systemd/system/rigos-runtime-render.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service index 166dfc49..06b7d29f 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service @@ -1,5 +1,5 @@ [Unit] -Description=Render RIGOS runtime miner configuration +Description=Render and publish RIGOS runtime miner configuration After=rigos-state-ready.service rigos-profile-apply.service Requires=rigos-state-ready.service rigos-profile-apply.service Before=rigos-hugepages.service rigos-miner.service @@ -7,7 +7,7 @@ ConditionPathExists=/var/lib/rigos/current [Service] Type=oneshot -ExecStart=/usr/lib/rigos/rigos-runtime-render +ExecStart=/usr/lib/rigos/rigos-runtime-publish NoNewPrivileges=yes PrivateTmp=yes ProtectHome=yes From 2d6ccd402ea4a8d0a944b1108ae6bed8c730872f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:52:22 +0700 Subject: [PATCH 105/386] stability consume staged runtime authority --- .../systemd/system/rigos-miner.service.d/runtime-render.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf index 030eecfd..a9f754a2 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf @@ -5,7 +5,7 @@ ConditionPathExists= ConditionPathExists=/var/lib/rigos/current [Service] -ExecCondition=+/usr/lib/rigos/rigos-runtime-render +ExecCondition=+/usr/lib/rigos/rigos-runtime-publish ExecCondition=/usr/lib/rigos/rigos-runtime-gate ExecStart= -ExecStart=/usr/lib/rigos/xmrig --config=/run/rigos/xmrig.json +ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json From fb30573c38252c74c59ad64405de286f7ec75b9a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:53:21 +0700 Subject: [PATCH 106/386] stability install runtime publication authority --- build/usb/hooks/010-rigos.chroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 83da94f0..a29b316f 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,7 +8,7 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos /usr/local/bin systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service rigos-miner-health.timer tmp.mount systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true From ff7fe8c7305a5d8afada347d2070c1379d9de11b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:54:17 +0700 Subject: [PATCH 107/386] test staged runtime publication wiring --- crates/rigos-config/tests/recovery_access.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index bed08c28..61924961 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -146,6 +146,7 @@ fn alpha8_appliance_wiring_is_explicit() { assert!(hook.contains("chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl")); assert!(!hook.contains("ln -sfn /usr/lib/rigos/rigosd /usr/local/bin/rigosd")); assert!(!hook.contains("ln -sfn /usr/lib/rigos/rigosctl /usr/local/bin/rigosctl")); + assert!(hook.contains("/usr/lib/rigos/rigos-runtime-publish")); assert!(hook.contains("rigos-runtime-render.service")); assert!(hook.contains("systemctl disable ssh.socket")); @@ -158,15 +159,22 @@ fn alpha8_appliance_wiring_is_explicit() { assert!(wrapper.contains(&format!("exec /usr/lib/rigos/{command}"))); } + let runtime_service = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service", + )) + .expect("read runtime authority service"); + assert!(runtime_service.contains("ExecStart=/usr/lib/rigos/rigos-runtime-publish")); + let miner = fs::read_to_string(repo_path( "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf", )) .expect("read miner runtime override"); assert!(miner.contains("Requires=rigos-runtime-render.service")); assert!(miner.contains("ConditionPathExists=/var/lib/rigos/current")); - assert!(miner.contains("ExecCondition=+/usr/lib/rigos/rigos-runtime-render")); + assert!(miner.contains("ExecCondition=+/usr/lib/rigos/rigos-runtime-publish")); assert!(miner.contains("ExecCondition=/usr/lib/rigos/rigos-runtime-gate")); - assert!(miner.contains("--config=/run/rigos/xmrig.json")); + assert!(miner.contains("ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json")); + assert!(!miner.contains("--config=/run/rigos/xmrig.json")); let ssh = fs::read_to_string(repo_path( "build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf", From a25867686b540a5bf2d4103678d5d79901234fc9 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:57:34 +0700 Subject: [PATCH 108/386] test staged runtime publication authority --- .../tests/hive_exit_runtime_publish.rs | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 crates/rigos-config/tests/hive_exit_runtime_publish.rs diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs new file mode 100644 index 00000000..aa67dcc6 --- /dev/null +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -0,0 +1,211 @@ +use serde_json::Value; +use std::fs; +use std::os::unix::fs::{symlink, PermissionsExt}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +fn write_json(path: &Path, value: Value) { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, serde_json::to_vec(&value).unwrap()).unwrap(); +} + +fn read_json(path: &Path) -> Value { + serde_json::from_slice(&fs::read(path).unwrap()).unwrap() +} + +#[test] +fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-runtime-publish-{unique}")); + let state = root.join("state"); + let runtime = root.join("run"); + let revision = state.join("revisions/r1"); + fs::create_dir_all(revision.join("flight-sheets")).unwrap(); + fs::create_dir_all(&runtime).unwrap(); + symlink("revisions/r1", state.join("current")).unwrap(); + + write_json( + &revision.join("policy.json"), + serde_json::json!({ + "schema": "rigos.policy/v1", + "active_flight_sheet": "xmr" + }), + ); + write_json( + &revision.join("flight-sheets/xmr.json"), + serde_json::json!({ + "schema": "rigos.flight-sheet/v1", + "backend": "xmrig", + "algorithm": "rx/0", + "cpu": { + "threads": 2, + "huge_pages": true, + "max_threads_hint": 100 + } + }), + ); + write_json( + &revision.join("xmrig.json"), + serde_json::json!({ + "future_top": "TOP_SENTINEL", + "cpu": { + "enabled": true, + "huge-pages": true, + "max-threads-hint": 2, + "future_cpu": "CPU_SENTINEL" + }, + "pools": [{ + "url": "identity:worker@pool.test:1", + "algo": "rx/0", + "user": "IDENTITY_SENTINEL", + "pass": "WORKER_SENTINEL", + "future_pool": "POOL_SENTINEL" + }], + "http": { + "enabled": false, + "access-token": "TOKEN_SENTINEL", + "future_http": "HTTP_SENTINEL" + } + }), + ); + + let renderer = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render", + ); + let publisher = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish", + ); + let gate = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate", + ); + let renderer_wrapper = root.join("renderer"); + fs::write( + &renderer_wrapper, + format!("#!/bin/sh\nexec python3 '{}'\n", renderer.display()), + ) + .unwrap(); + fs::set_permissions(&renderer_wrapper, fs::Permissions::from_mode(0o755)).unwrap(); + + let jq = Command::new("jq").arg("--version").status().unwrap(); + assert!(jq.success(), "jq is required by runtime publication authority"); + + let result = Command::new("sh") + .arg(&publisher) + .env("RIGOS_STATE_PATH", &state) + .env("RIGOS_RUNTIME_PATH", &runtime) + .env("RIGOS_RUNTIME_RENDERER", &renderer_wrapper) + .env("RIGOS_RENDER_SKIP_CHOWN", "1") + .status() + .unwrap(); + assert!(result.success(), "runtime publisher failed"); + + let private = read_json(&runtime.join("xmrig.json")); + assert_eq!(private["future_top"], "TOP_SENTINEL"); + assert_eq!(private["cpu"]["future_cpu"], "CPU_SENTINEL"); + assert_eq!(private["pools"][0]["future_pool"], "POOL_SENTINEL"); + assert_eq!(private["http"]["future_http"], "HTTP_SENTINEL"); + assert_eq!(private["cpu"]["max-threads-hint"], 100); + assert_eq!(private["cpu"]["rx"], serde_json::json!([-1, -1])); + + let public = read_json(&runtime.join("xmrig-public.json")); + let public_keys = public.as_object().unwrap().keys().cloned().collect::>(); + assert_eq!( + public_keys, + vec![ + "algo", + "cpu", + "http", + "pools", + "randomx", + "rigos-public-view", + "threads" + ] + ); + assert_eq!(public["algo"], "rx/0"); + assert_eq!(public["threads"], 2); + assert_eq!(public["cpu"]["max-threads-hint"], 100); + assert_eq!(public["cpu"]["rx"], serde_json::json!([-1, -1])); + assert_eq!(public["randomx"]["huge-pages"], true); + assert_eq!(public["pools"][0]["url"], "pool.test:1"); + assert_eq!(public["rigos-public-view"]["construction"], "allowlist"); + let public_text = serde_json::to_string(&public).unwrap(); + for sentinel in [ + "TOP_SENTINEL", + "CPU_SENTINEL", + "POOL_SENTINEL", + "HTTP_SENTINEL", + "IDENTITY_SENTINEL", + "WORKER_SENTINEL", + "TOKEN_SENTINEL", + ] { + assert!(!public_text.contains(sentinel), "public view leaked {sentinel}"); + } + + let status = read_json(&runtime.join("runtime-config-status.json")); + assert_eq!(status["outcome"], "ready"); + assert_eq!(status["revision"], "r1"); + assert_eq!(status["exact_threads"], 2); + assert_eq!(status["public_view_construction"], "allowlist"); + + assert_eq!( + fs::metadata(runtime.join("xmrig.json")) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o640 + ); + for path in [ + runtime.join("xmrig-public.json"), + runtime.join("runtime-config-status.json"), + ] { + assert_eq!( + fs::metadata(path).unwrap().permissions().mode() & 0o777, + 0o644 + ); + } + let leftovers = fs::read_dir(&runtime) + .unwrap() + .filter_map(Result::ok) + .map(|entry| entry.file_name().to_string_lossy().into_owned()) + .filter(|name| name.starts_with('.')) + .collect::>(); + assert!(leftovers.is_empty(), "temporary runtime files remain: {leftovers:?}"); + + let allowed = Command::new("python3") + .arg(&gate) + .arg("--state") + .arg(&state) + .arg("--runtime") + .arg(&runtime) + .status() + .unwrap(); + assert!(allowed.success()); + + let mut invalid = private; + invalid["cpu"]["rx"] = serde_json::json!([-1]); + write_json(&runtime.join("xmrig.json"), invalid); + let denied = Command::new("python3") + .arg(&gate) + .arg("--state") + .arg(&state) + .arg("--runtime") + .arg(&runtime) + .status() + .unwrap(); + assert_eq!(denied.code(), Some(2)); + + let _ = fs::remove_dir_all(root); +} From c55453de596885256aa31bddc3b81cfd209e54c2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:58:11 +0700 Subject: [PATCH 109/386] test explicit public inspector boundary --- .../tests/explicit_config_boundary.rs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 crates/rigos-xmrig/tests/explicit_config_boundary.rs diff --git a/crates/rigos-xmrig/tests/explicit_config_boundary.rs b/crates/rigos-xmrig/tests/explicit_config_boundary.rs new file mode 100644 index 00000000..8c1c8e26 --- /dev/null +++ b/crates/rigos-xmrig/tests/explicit_config_boundary.rs @@ -0,0 +1,74 @@ +use rigos_machine::MachineContext; +use rigos_miner::MinerBackend; +use rigos_xmrig::{ConfigParseState, XmrigBackend}; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[test] +fn explicit_public_config_wins_when_runtime_uses_xmrig_short_config_option() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-explicit-config-{unique}")); + let proc_root = root.join("proc"); + let pid_dir = proc_root.join("42"); + fs::create_dir_all(&pid_dir).unwrap(); + + let private = root.join("private.json"); + let public = root.join("public.json"); + fs::write( + &private, + r#"{"algo":"rx/private","threads":99,"future":"PRIVATE_SENTINEL","http":{"enabled":false}}"#, + ) + .unwrap(); + fs::write( + &public, + r#"{"algo":"rx/0","threads":2,"randomx":{"huge-pages":true},"pools":[{"url":"pool.test:1"}],"http":{"enabled":false}}"#, + ) + .unwrap(); + + fs::write(pid_dir.join("comm"), "xmrig\n").unwrap(); + let mut cmdline = Vec::new(); + cmdline.extend_from_slice(b"xmrig\0-c\0"); + cmdline.extend_from_slice(private.as_os_str().as_encoded_bytes()); + cmdline.push(0); + fs::write(pid_dir.join("cmdline"), cmdline).unwrap(); + fs::write( + pid_dir.join("status"), + "Name:\txmrig\nUid:\t1000 1000 1000 1000\n", + ) + .unwrap(); + fs::write(pid_dir.join("cgroup"), "0::/system.slice/rigos-miner.service\n").unwrap(); + fs::write( + pid_dir.join("stat"), + "42 (xmrig) S 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 100 0\n", + ) + .unwrap(); + fs::write(proc_root.join("uptime"), "100.0 0.0\n").unwrap(); + + let expected_path = public.to_string_lossy().into_owned(); + let backend = XmrigBackend { + explicit_executable: None, + explicit_config: Some(PathBuf::from(&public)), + probe_version: false, + }; + let result = backend.discover(&MachineContext { + proc_root, + sys_root: root.join("sys"), + }); + let _ = fs::remove_dir_all(root); + + let snapshot = result.value.unwrap(); + assert!(snapshot.running); + assert_eq!(snapshot.config.path, Some(expected_path)); + assert!(matches!(snapshot.config.parse_state, ConfigParseState::Valid)); + assert_eq!(snapshot.config.algorithm.as_deref(), Some("rx/0")); + assert_eq!(snapshot.config.thread_hint, Some(2)); + assert_eq!(snapshot.config.huge_pages_requested, Some(true)); + assert_eq!(snapshot.config.pools, vec!["pool.test:1"]); + assert!(!serde_json::to_string(&snapshot) + .unwrap() + .contains("PRIVATE_SENTINEL")); +} From 3157841ae3a57ba11499280602713b17e44c4efd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:58:41 +0700 Subject: [PATCH 110/386] remove unusable Hive Exit Actions workflow --- .github/workflows/hive-exit-review-fixes.yml | 45 -------------------- 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/hive-exit-review-fixes.yml diff --git a/.github/workflows/hive-exit-review-fixes.yml b/.github/workflows/hive-exit-review-fixes.yml deleted file mode 100644 index 0f9644b5..00000000 --- a/.github/workflows/hive-exit-review-fixes.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Apply Hive Exit Review Fixes - -on: - push: - branches: - - stability/hive-exit-gates - -permissions: - contents: write - -jobs: - apply: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: stability/hive-exit-gates - fetch-depth: 0 - - - uses: dtolnay/rust-toolchain@1.85.1 - with: - components: rustfmt - - - name: Apply exact source patches - run: python3 scripts/apply-hive-exit-review-fixes.py - - - name: Verify patched sources - run: | - cargo fmt --all - cargo fmt --all -- --check - cargo test -p rigos-xmrig --lib - python3 scripts/check-alpha8-runtime.py - git diff --check - - - name: Commit verified review fixes - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add crates/rigos-xmrig/src/lib.rs \ - build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render \ - scripts/check-alpha8-runtime.py - git diff --cached --quiet && exit 0 - git commit -m "stability enforce public inspector boundary" - git push origin HEAD:stability/hive-exit-gates From b77d87e85262a976b5b46a53f6ff364e7a2c3f52 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:59:06 +0700 Subject: [PATCH 111/386] remove obsolete hostile-review applicator --- scripts/apply-hive-exit-review-fixes.py | 253 ------------------------ 1 file changed, 253 deletions(-) delete mode 100644 scripts/apply-hive-exit-review-fixes.py diff --git a/scripts/apply-hive-exit-review-fixes.py b/scripts/apply-hive-exit-review-fixes.py deleted file mode 100644 index f266812f..00000000 --- a/scripts/apply-hive-exit-review-fixes.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] - - -def replace_once(path: Path, old: str, new: str) -> None: - text = path.read_text(encoding="utf-8") - count = text.count(old) - if count != 1: - raise RuntimeError(f"expected exactly one match in {path}, found {count}") - path.write_text(text.replace(old, new, 1), encoding="utf-8") - - -def patch_xmrig_backend() -> None: - path = ROOT / "crates/rigos-xmrig/src/lib.rs" - replace_once( - path, - """ let config_path = cmdline - .as_deref() - .and_then(extract_config_path) - .or_else(|| self.explicit_config.clone());""", - """ let config_path = self - .explicit_config - .clone() - .or_else(|| cmdline.as_deref().and_then(extract_config_path));""", - ) - - marker = """ #[test] - fn discovers_xmrig_from_synthetic_proc_without_mutation() {""" - test = """ #[test] - fn explicit_config_overrides_process_cmdline_config() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!("rigos-proc-explicit-{unique}")); - let proc_root = root.join("proc"); - let pid_dir = proc_root.join("42"); - fs::create_dir_all(&pid_dir).unwrap(); - let private = root.join("private.json"); - let public = root.join("public.json"); - fs::write( - &private, - r#"{"algo":"rx/private","http":{"enabled":false}}"#, - ) - .unwrap(); - fs::write( - &public, - r#"{"algo":"rx/0","threads":2,"randomx":{"huge-pages":true},"http":{"enabled":false}}"#, - ) - .unwrap(); - fs::write(pid_dir.join("comm"), "xmrig\n").unwrap(); - fs::write( - pid_dir.join("cmdline"), - format!("xmrig\0--config={}\0", private.display()), - ) - .unwrap(); - fs::write( - pid_dir.join("status"), - "Name:\txmrig\nUid:\t1000 1000 1000 1000\n", - ) - .unwrap(); - fs::write(pid_dir.join("cgroup"), "0::/system.slice/xmrig.service\n").unwrap(); - fs::write( - pid_dir.join("stat"), - "42 (xmrig) S 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 100 0\n", - ) - .unwrap(); - fs::write(proc_root.join("uptime"), "100.0 0.0\n").unwrap(); - - let expected_path = public.to_string_lossy().into_owned(); - let backend = XmrigBackend { - explicit_executable: None, - explicit_config: Some(public), - probe_version: false, - }; - let result = backend.discover(&MachineContext { - proc_root, - sys_root: root.join("sys"), - }); - let _ = fs::remove_dir_all(root); - let snapshot = result.value.unwrap(); - assert_eq!(snapshot.config.path, Some(expected_path)); - assert_eq!(snapshot.config.algorithm.as_deref(), Some("rx/0")); - assert_eq!(snapshot.config.thread_hint, Some(2)); - assert_eq!(snapshot.config.huge_pages_requested, Some(true)); - } - -""" - replace_once(path, marker, test + marker) - - -def patch_runtime_renderer() -> None: - path = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render" - helper_marker = """def render() -> tuple[dict, dict, dict]:""" - helpers = """def public_pool(pool: dict) -> dict: - safe = {} - url = pool.get("url") - if isinstance(url, str) and url: - safe["url"] = url.rsplit("@", 1)[-1] - algo = pool.get("algo") - if isinstance(algo, str) and algo: - safe["algo"] = algo - for key in ("tls", "nicehash", "keepalive"): - value = pool.get(key) - if isinstance(value, bool): - safe[key] = value - priority = pool.get("priority") - if isinstance(priority, int) and not isinstance(priority, bool): - safe["priority"] = priority - return safe - - -def public_http(http: object) -> dict: - if not isinstance(http, dict): - return {"enabled": False} - safe = {} - enabled = http.get("enabled") - safe["enabled"] = enabled if isinstance(enabled, bool) else False - host = http.get("host") - if isinstance(host, str) and host: - safe["host"] = host - port = http.get("port") - if isinstance(port, int) and not isinstance(port, bool) and 1 <= port <= 65535: - safe["port"] = port - restricted = http.get("restricted") - if isinstance(restricted, bool): - safe["restricted"] = restricted - return safe - - -""" - replace_once(path, helper_marker, helpers + helper_marker) - - replace_once( - path, - """ public = copy.deepcopy(runtime) - for pool in public.get("pools", []): - if isinstance(pool, dict): - pool.pop("user", None) - pool.pop("pass", None) - http = public.get("http") - if isinstance(http, dict): - http.pop("access-token", None) - - public["algo"] = algorithm - if exact_threads is not None: - public["threads"] = exact_threads - else: - public.pop("threads", None) - public_randomx = public.get("randomx") - if not isinstance(public_randomx, dict): - public_randomx = {} - huge_pages = cpu.get("huge-pages") - if isinstance(huge_pages, bool): - public_randomx["huge-pages"] = huge_pages - public["randomx"] = public_randomx - public["rigos-public-view"] = { - "schema": "rigos.xmrig-public-config/v1", - "identity_redacted": True, - "source_revision": current.name, - } -""", - """ huge_pages = cpu.get("huge-pages") - public_cpu = {} - enabled = cpu.get("enabled") - if isinstance(enabled, bool): - public_cpu["enabled"] = enabled - if isinstance(huge_pages, bool): - public_cpu["huge-pages"] = huge_pages - max_threads_hint = cpu.get("max-threads-hint") - if isinstance(max_threads_hint, int) and not isinstance(max_threads_hint, bool): - public_cpu["max-threads-hint"] = max_threads_hint - if profile is not None and exact_threads is not None: - public_cpu[profile] = [-1] * exact_threads - - public = { - "algo": algorithm, - "cpu": public_cpu, - "http": public_http(runtime.get("http")), - "pools": [public_pool(pool) for pool in pools], - "randomx": { - "huge-pages": huge_pages if isinstance(huge_pages, bool) else False, - }, - "rigos-public-view": { - "schema": "rigos.xmrig-public-config/v1", - "identity_redacted": True, - "construction": "allowlist", - "source_revision": current.name, - }, - } - if exact_threads is not None: - public["threads"] = exact_threads -""", - ) - - replace_once( - path, - ' "identity_redacted_public_view": True,\n', - ' "identity_redacted_public_view": True,\n "public_view_construction": "allowlist",\n', - ) - - -def patch_runtime_fixture() -> None: - path = ROOT / "scripts/check-alpha8-runtime.py" - replace_once( - path, - ' "max-threads-hint": 2,\n', - ' "max-threads-hint": 2,\n "future-secret": "fixture-cpu-secret",\n', - ) - replace_once( - path, - ' "pass": "fixture-worker",\n', - ' "pass": "fixture-worker",\n "future-secret": "fixture-pool-secret",\n', - ) - replace_once( - path, - ' "access-token": "fixture-private-token",\n', - ' "access-token": "fixture-private-token",\n "future-secret": "fixture-http-secret",\n', - ) - replace_once( - path, - ' "http": {\n', - ' "future-secret": "fixture-top-secret",\n "http": {\n', - ) - replace_once( - path, - ' assert public["rigos-public-view"]["identity_redacted"] is True\n', - ' assert public["rigos-public-view"]["identity_redacted"] is True\n assert public["rigos-public-view"]["construction"] == "allowlist"\n assert set(public) == {"algo", "cpu", "http", "pools", "randomx", "rigos-public-view", "threads"}\n assert set(public["pools"][0]) <= {"url", "algo", "tls", "nicehash", "keepalive", "priority"}\n assert set(public["http"]) <= {"enabled", "host", "port", "restricted"}\n', - ) - replace_once( - path, - ' assert "fixture-private-token" not in public_text\n', - ' assert "fixture-private-token" not in public_text\n assert "fixture-cpu-secret" not in public_text\n assert "fixture-pool-secret" not in public_text\n assert "fixture-http-secret" not in public_text\n assert "fixture-top-secret" not in public_text\n', - ) - replace_once( - path, - ' assert status["profile"] == "rx"\n', - ' assert status["profile"] == "rx"\n assert status["public_view_construction"] == "allowlist"\n', - ) - - -def main() -> int: - patch_xmrig_backend() - patch_runtime_renderer() - patch_runtime_fixture() - print("Hive Exit hostile review fixes applied") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From c7c9f4dc1aa06acae20f0c11e55317bd05fa4ade Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 21:59:55 +0700 Subject: [PATCH 112/386] document staged runtime authority boundary --- docs/hive-exit-hostile-review.md | 53 +++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/docs/hive-exit-hostile-review.md b/docs/hive-exit-hostile-review.md index 5d731dba..d4904ab0 100644 --- a/docs/hive-exit-hostile-review.md +++ b/docs/hive-exit-hostile-review.md @@ -1,8 +1,51 @@ -# Hive Exit hostile review +HIVE EXIT HOSTILE REVIEW +======================== -The first stability batch passed source verification, but review found two release blockers before image construction: +The first stability batch passed source verification, but review found two +release blockers before image construction. -1. An explicit inspector configuration must take precedence over a configuration discovered from the running process command line. -2. The public XMRig view must be constructed from an allowlist. Copying the private configuration and deleting known secret fields is not an acceptable security boundary. +1. The inspector must never select the identity-bearing runtime configuration + when the public configuration was explicitly requested. -The review fix must pass Rust formatting, the `rigos-xmrig` unit suite, the runtime redaction fixture, and `git diff --check` before it is committed to the stability branch. +2. The public XMRig view must be constructed from an allowlist. Copying the + private configuration and deleting known fields is not a security boundary. + +CURRENT BOUNDARY +---------------- + +The legacy renderer now runs only inside a private staging directory under +/run/rigos with mode 0700. The runtime publication authority then creates a +new public document from an explicit jq allowlist and atomically publishes: + + /run/rigos/xmrig.json mode 0640 + /run/rigos/xmrig-public.json mode 0644 + /run/rigos/runtime-config-status.json mode 0644 + +The status document is published last. A failed publication therefore cannot +claim a new ready state before the private and public documents are in place. + +The managed XMRig process uses the supported short option: + + xmrig -c /run/rigos/xmrig.json + +RIGOS process discovery currently recognizes only the long --config forms. +The rigosd and rigosctl wrappers explicitly select xmrig-public.json, so the +managed process command line cannot override the public inspection path. +This compatibility boundary is covered by an integration test and must not be +changed independently from the parser or systemd wiring. + +REQUIRED VERIFICATION +--------------------- + + cargo fmt --all -- --check + cargo test -p rigos-xmrig --test explicit_config_boundary + cargo test -p rigos-config --test hive_exit_runtime_publish + ./scripts/verify.sh + +The publication fixture injects unknown sentinel fields at the top level and +inside CPU, pool, and HTTP objects. Private runtime truth must retain them. +The public view must contain none of them. + +GitHub Actions are disabled for this account. Verification evidence therefore +comes from the local WSL source gate and exact artifact provenance, not from a +remote workflow badge. From ef76972ea8767da15f52507d2fb91fc0a1d0c1ea Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:00:50 +0700 Subject: [PATCH 113/386] format staged runtime publication test --- .../tests/hive_exit_runtime_publish.rs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index aa67dcc6..9fb67014 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -1,6 +1,6 @@ use serde_json::Value; use std::fs; -use std::os::unix::fs::{symlink, PermissionsExt}; +use std::os::unix::fs::{PermissionsExt, symlink}; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; @@ -81,15 +81,9 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { }), ); - let renderer = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render", - ); - let publisher = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish", - ); - let gate = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate", - ); + let renderer = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render"); + let publisher = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish"); + let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-gate"); let renderer_wrapper = root.join("renderer"); fs::write( &renderer_wrapper, @@ -99,7 +93,10 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { fs::set_permissions(&renderer_wrapper, fs::Permissions::from_mode(0o755)).unwrap(); let jq = Command::new("jq").arg("--version").status().unwrap(); - assert!(jq.success(), "jq is required by runtime publication authority"); + assert!( + jq.success(), + "jq is required by runtime publication authority" + ); let result = Command::new("sh") .arg(&publisher) @@ -120,7 +117,12 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { assert_eq!(private["cpu"]["rx"], serde_json::json!([-1, -1])); let public = read_json(&runtime.join("xmrig-public.json")); - let public_keys = public.as_object().unwrap().keys().cloned().collect::>(); + let public_keys = public + .as_object() + .unwrap() + .keys() + .cloned() + .collect::>(); assert_eq!( public_keys, vec![ @@ -150,7 +152,10 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { "WORKER_SENTINEL", "TOKEN_SENTINEL", ] { - assert!(!public_text.contains(sentinel), "public view leaked {sentinel}"); + assert!( + !public_text.contains(sentinel), + "public view leaked {sentinel}" + ); } let status = read_json(&runtime.join("runtime-config-status.json")); @@ -182,7 +187,10 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { .map(|entry| entry.file_name().to_string_lossy().into_owned()) .filter(|name| name.starts_with('.')) .collect::>(); - assert!(leftovers.is_empty(), "temporary runtime files remain: {leftovers:?}"); + assert!( + leftovers.is_empty(), + "temporary runtime files remain: {leftovers:?}" + ); let allowed = Command::new("python3") .arg(&gate) From d6434065d871650ed0e7264979351d3e673763ed Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:01:28 +0700 Subject: [PATCH 114/386] format explicit inspector boundary test --- .../tests/explicit_config_boundary.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/rigos-xmrig/tests/explicit_config_boundary.rs b/crates/rigos-xmrig/tests/explicit_config_boundary.rs index 8c1c8e26..5c664ffb 100644 --- a/crates/rigos-xmrig/tests/explicit_config_boundary.rs +++ b/crates/rigos-xmrig/tests/explicit_config_boundary.rs @@ -40,7 +40,11 @@ fn explicit_public_config_wins_when_runtime_uses_xmrig_short_config_option() { "Name:\txmrig\nUid:\t1000 1000 1000 1000\n", ) .unwrap(); - fs::write(pid_dir.join("cgroup"), "0::/system.slice/rigos-miner.service\n").unwrap(); + fs::write( + pid_dir.join("cgroup"), + "0::/system.slice/rigos-miner.service\n", + ) + .unwrap(); fs::write( pid_dir.join("stat"), "42 (xmrig) S 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 100 0\n", @@ -63,12 +67,17 @@ fn explicit_public_config_wins_when_runtime_uses_xmrig_short_config_option() { let snapshot = result.value.unwrap(); assert!(snapshot.running); assert_eq!(snapshot.config.path, Some(expected_path)); - assert!(matches!(snapshot.config.parse_state, ConfigParseState::Valid)); + assert!(matches!( + snapshot.config.parse_state, + ConfigParseState::Valid + )); assert_eq!(snapshot.config.algorithm.as_deref(), Some("rx/0")); assert_eq!(snapshot.config.thread_hint, Some(2)); assert_eq!(snapshot.config.huge_pages_requested, Some(true)); assert_eq!(snapshot.config.pools, vec!["pool.test:1"]); - assert!(!serde_json::to_string(&snapshot) - .unwrap() - .contains("PRIVATE_SENTINEL")); + assert!( + !serde_json::to_string(&snapshot) + .unwrap() + .contains("PRIVATE_SENTINEL") + ); } From e4ad431042a8c4f88ca596a65687cd5de3a3ec94 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:03:51 +0700 Subject: [PATCH 115/386] verify Hive Exit runtime authority in image --- scripts/verify-usb-appliance.sh | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 812f8ed5..1e5cf261 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -82,10 +82,17 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-firstboot.service \ etc/systemd/system/rigos-hugepages.service \ etc/systemd/system/rigos-miner.service \ + etc/systemd/system/rigos-miner.service.d/runtime-render.conf \ + etc/systemd/system/rigos-miner.service.d/stability.conf \ + etc/systemd/system/rigos-miner-health.service \ + etc/systemd/system/rigos-miner-health.timer \ + etc/systemd/system/rigos-runtime-render.service \ etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/xmrig usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access usr/local/sbin/rigos-state-orchestrate \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-miner-health usr/lib/rigos/rigos-runtime-render usr/lib/rigos/rigos-runtime-publish usr/lib/rigos/rigos-runtime-gate usr/lib/rigos/xmrig \ + usr/local/bin/rigosd usr/local/bin/rigosctl \ + usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access usr/local/sbin/rigos-state-orchestrate \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' @@ -93,6 +100,8 @@ python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-state-orchestrate" python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" +python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-health" +sh -n "$temporary/root/usr/lib/rigos/rigos-runtime-publish" python3 "$script_dir/verify-systemd-ordering.py" "$temporary/root/etc/systemd/system" rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" [[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' @@ -120,6 +129,20 @@ strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'rigos.perfo if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'sysctl' >/dev/null; then die 'performance authority shells out to sysctl'; fi if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -Ei '(/dev/sd|/dev/nvme|cpu model)' >/dev/null; then die 'performance authority contains hardware-name or internal-disk targeting'; fi grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner safety gate is missing' +[[ -x "$temporary/root/usr/lib/rigos/rigos-runtime-render" ]] || die 'legacy runtime renderer is missing' +[[ -x "$temporary/root/usr/lib/rigos/rigos-runtime-publish" ]] || die 'runtime publication authority is missing' +grep -Fq 'ExecStart=/usr/lib/rigos/rigos-runtime-publish' "$temporary/root/etc/systemd/system/rigos-runtime-render.service" || die 'runtime publication authority is not wired' +grep -Fq 'ExecCondition=+/usr/lib/rigos/rigos-runtime-publish' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'miner does not republish runtime truth before start' +grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-runtime-gate' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'runtime gate is missing from miner override' +grep -Fq 'ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'managed miner does not use the short private runtime config option' +grep -Fq 'construction: "allowlist"' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'public runtime allowlist marker is missing' +grep -Fq '.render-stage.XXXXXX' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'private runtime staging directory is missing' +grep -Fq -- '--xmrig-config /run/rigos/xmrig-public.json' "$temporary/root/usr/local/bin/rigosd" || die 'rigosd does not default to the public runtime view' +grep -Fq -- '--xmrig-config /run/rigos/xmrig-public.json' "$temporary/root/usr/local/bin/rigosctl" || die 'rigosctl does not default to the public runtime view' +[[ -x "$temporary/root/usr/lib/rigos/rigos-miner-health" ]] || die 'miner health observer is missing' +grep -Fq 'OnUnitActiveSec=1min' "$temporary/root/etc/systemd/system/rigos-miner-health.timer" || die 'miner health timer cadence is missing' +grep -Fq 'Restart=on-failure' "$temporary/root/etc/systemd/system/rigos-miner.service.d/stability.conf" || die 'bounded miner restart policy is missing' +grep -Fq 'StartLimitBurst=5' "$temporary/root/etc/systemd/system/rigos-miner.service.d/stability.conf" || die 'miner crash-loop ceiling is missing' [[ -f "$temporary/root/etc/systemd/system/rigos-profile-apply.service" ]] || die 'profile apply service is missing' grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-config needs-activation' "$temporary/root/etc/systemd/system/rigos-firstboot.service" || die 'first boot activation gate is missing' grep -Fq 'ExecStart=/usr/local/sbin/rigos-recovery-access' "$temporary/root/etc/systemd/system/rigos-recovery-access.service" || die 'local recovery access phase is missing' From 9de4763b471e0cd43336afd2bbf17a013f1b97e7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:04:52 +0700 Subject: [PATCH 116/386] clippy clean runtime publication fixture --- crates/rigos-config/tests/hive_exit_runtime_publish.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index 9fb67014..56f757bf 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -139,7 +139,7 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { assert_eq!(public["threads"], 2); assert_eq!(public["cpu"]["max-threads-hint"], 100); assert_eq!(public["cpu"]["rx"], serde_json::json!([-1, -1])); - assert_eq!(public["randomx"]["huge-pages"], true); + assert!(public["randomx"]["huge-pages"] == true); assert_eq!(public["pools"][0]["url"], "pool.test:1"); assert_eq!(public["rigos-public-view"]["construction"], "allowlist"); let public_text = serde_json::to_string(&public).unwrap(); From 706f6bdcfdbfbb400093139adf44c64a2d1fccba Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:06:03 +0700 Subject: [PATCH 117/386] clippy clean explicit inspector boundary --- crates/rigos-xmrig/tests/explicit_config_boundary.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/rigos-xmrig/tests/explicit_config_boundary.rs b/crates/rigos-xmrig/tests/explicit_config_boundary.rs index 5c664ffb..ae7a854d 100644 --- a/crates/rigos-xmrig/tests/explicit_config_boundary.rs +++ b/crates/rigos-xmrig/tests/explicit_config_boundary.rs @@ -2,7 +2,6 @@ use rigos_machine::MachineContext; use rigos_miner::MinerBackend; use rigos_xmrig::{ConfigParseState, XmrigBackend}; use std::fs; -use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; #[test] @@ -55,7 +54,7 @@ fn explicit_public_config_wins_when_runtime_uses_xmrig_short_config_option() { let expected_path = public.to_string_lossy().into_owned(); let backend = XmrigBackend { explicit_executable: None, - explicit_config: Some(PathBuf::from(&public)), + explicit_config: Some(public.clone()), probe_version: false, }; let result = backend.discover(&MachineContext { From f99cb68cbe6d54cc015e2b44119fdf9a6f1183de Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:07:01 +0700 Subject: [PATCH 118/386] remove boolean comparison lint --- crates/rigos-config/tests/hive_exit_runtime_publish.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index 56f757bf..47f42069 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -139,7 +139,7 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { assert_eq!(public["threads"], 2); assert_eq!(public["cpu"]["max-threads-hint"], 100); assert_eq!(public["cpu"]["rx"], serde_json::json!([-1, -1])); - assert!(public["randomx"]["huge-pages"] == true); + assert_eq!(public["randomx"]["huge-pages"].as_bool(), Some(true)); assert_eq!(public["pools"][0]["url"], "pool.test:1"); assert_eq!(public["rigos-public-view"]["construction"], "allowlist"); let public_text = serde_json::to_string(&public).unwrap(); From e0df53bfb4ac2cbf3fcdff2fa9429753172c35a4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:07:34 +0700 Subject: [PATCH 119/386] test Hive Exit image verifier coverage --- .../tests/hive_exit_image_verifier.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 crates/rigos-config/tests/hive_exit_image_verifier.rs diff --git a/crates/rigos-config/tests/hive_exit_image_verifier.rs b/crates/rigos-config/tests/hive_exit_image_verifier.rs new file mode 100644 index 00000000..24fc9dd3 --- /dev/null +++ b/crates/rigos-config/tests/hive_exit_image_verifier.rs @@ -0,0 +1,35 @@ +use std::fs; +use std::path::PathBuf; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +#[test] +fn image_verifier_requires_runtime_authority_and_stability_bytes() { + let verifier = fs::read_to_string(repo_path("scripts/verify-usb-appliance.sh")).unwrap(); + + for required in [ + "usr/lib/rigos/rigos-runtime-publish", + "usr/lib/rigos/rigos-miner-health", + "usr/local/bin/rigosd", + "usr/local/bin/rigosctl", + "etc/systemd/system/rigos-runtime-render.service", + "etc/systemd/system/rigos-miner.service.d/runtime-render.conf", + "etc/systemd/system/rigos-miner.service.d/stability.conf", + "etc/systemd/system/rigos-miner-health.timer", + "ExecStart=/usr/lib/rigos/rigos-runtime-publish", + "ExecCondition=+/usr/lib/rigos/rigos-runtime-publish", + "ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json", + "--xmrig-config /run/rigos/xmrig-public.json", + "construction: \"allowlist\"", + "StartLimitBurst=5", + ] { + assert!( + verifier.contains(required), + "image verifier is missing contract: {required}" + ); + } +} From 120d5be653789e156476507bc0a6c213d59cb569 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:09:13 +0700 Subject: [PATCH 120/386] stability serialize runtime publication authority --- .../usr/lib/rigos/rigos-runtime-authority | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority new file mode 100644 index 00000000..7cd01f5f --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority @@ -0,0 +1,31 @@ +#!/bin/sh +set -eu + +runtime=${RIGOS_RUNTIME_PATH:-/run/rigos} +publisher=${RIGOS_RUNTIME_PUBLISHER:-/usr/lib/rigos/rigos-runtime-publish} +mkdir -p "$runtime" +chmod 0755 "$runtime" + +lock="$runtime/.runtime-publish.lock" +exec 9>"$lock" +chmod 0600 "$lock" +if ! flock -x -w 30 9; then + echo 'rigos-runtime-authority: publication lock timeout' >&2 + exit 75 +fi + +cleanup_stale() { + find -P "$runtime" -mindepth 1 -maxdepth 1 -type d \ + -name '.render-stage.*' -exec rm -rf -- {} + + find -P "$runtime" -mindepth 1 -maxdepth 1 -type f \ + \( -name '.xmrig-public.*' \ + -o -name '.runtime-status.*' \ + -o -name '.xmrig-runtime.*' \) \ + -delete +} + +cleanup_stale +status=0 +"$publisher" || status=$? +cleanup_stale +exit "$status" From 389d732ee6542ad4a9eb0f69af4c60a9ac453a5e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:09:47 +0700 Subject: [PATCH 121/386] stability serialize runtime authority service --- .../etc/systemd/system/rigos-runtime-render.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service index 06b7d29f..cbf8f72a 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service @@ -7,7 +7,7 @@ ConditionPathExists=/var/lib/rigos/current [Service] Type=oneshot -ExecStart=/usr/lib/rigos/rigos-runtime-publish +ExecStart=/usr/lib/rigos/rigos-runtime-authority NoNewPrivileges=yes PrivateTmp=yes ProtectHome=yes From c5c022c33cb27598935c0a442f19216d1462fc81 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:10:28 +0700 Subject: [PATCH 122/386] stability serialize miner runtime publication --- .../systemd/system/rigos-miner.service.d/runtime-render.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf index a9f754a2..3b94d5ff 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf @@ -5,7 +5,7 @@ ConditionPathExists= ConditionPathExists=/var/lib/rigos/current [Service] -ExecCondition=+/usr/lib/rigos/rigos-runtime-publish +ExecCondition=+/usr/lib/rigos/rigos-runtime-authority ExecCondition=/usr/lib/rigos/rigos-runtime-gate ExecStart= ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json From 2a3f597b179238ffd79f725bf9f11259c18ea94b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:13:39 +0700 Subject: [PATCH 123/386] stability make runtime authority executable --- build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority old mode 100644 new mode 100755 From fbe9dfb86e898eefd5f581e352fae53aaaf70aff Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:15:12 +0700 Subject: [PATCH 124/386] test serialized runtime authority wiring --- crates/rigos-config/tests/recovery_access.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index 61924961..fd56a8ba 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -163,7 +163,17 @@ fn alpha8_appliance_wiring_is_explicit() { "build/usb/includes.chroot/etc/systemd/system/rigos-runtime-render.service", )) .expect("read runtime authority service"); - assert!(runtime_service.contains("ExecStart=/usr/lib/rigos/rigos-runtime-publish")); + assert!(runtime_service.contains("ExecStart=/usr/lib/rigos/rigos-runtime-authority")); + + let authority = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority", + ); + let mode = fs::metadata(authority).unwrap().permissions(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_ne!(mode.mode() & 0o111, 0); + } let miner = fs::read_to_string(repo_path( "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/runtime-render.conf", @@ -171,7 +181,7 @@ fn alpha8_appliance_wiring_is_explicit() { .expect("read miner runtime override"); assert!(miner.contains("Requires=rigos-runtime-render.service")); assert!(miner.contains("ConditionPathExists=/var/lib/rigos/current")); - assert!(miner.contains("ExecCondition=+/usr/lib/rigos/rigos-runtime-publish")); + assert!(miner.contains("ExecCondition=+/usr/lib/rigos/rigos-runtime-authority")); assert!(miner.contains("ExecCondition=/usr/lib/rigos/rigos-runtime-gate")); assert!(miner.contains("ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json")); assert!(!miner.contains("--config=/run/rigos/xmrig.json")); From 94b97b0eab0a72430ca2121ffd1bf041ce4ec616 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:17:44 +0700 Subject: [PATCH 125/386] verify serialized runtime authority in image --- scripts/verify-usb-appliance.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 1e5cf261..8cf73ebe 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -90,7 +90,7 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-profile-apply.service \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-miner-health usr/lib/rigos/rigos-runtime-render usr/lib/rigos/rigos-runtime-publish usr/lib/rigos/rigos-runtime-gate usr/lib/rigos/xmrig \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-miner-health usr/lib/rigos/rigos-runtime-render usr/lib/rigos/rigos-runtime-publish usr/lib/rigos/rigos-runtime-authority usr/lib/rigos/rigos-runtime-gate usr/lib/rigos/xmrig \ usr/local/bin/rigosd usr/local/bin/rigosctl \ usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access usr/local/sbin/rigos-state-orchestrate \ usr/share/rigos >/dev/null @@ -102,6 +102,7 @@ python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-state-orchestrate" python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-health" sh -n "$temporary/root/usr/lib/rigos/rigos-runtime-publish" +sh -n "$temporary/root/usr/lib/rigos/rigos-runtime-authority" python3 "$script_dir/verify-systemd-ordering.py" "$temporary/root/etc/systemd/system" rigosctl_path="$(PATH="$temporary/root/usr/local/sbin:$temporary/root/usr/bin" command -v rigosctl)" [[ "$rigosctl_path" == "$temporary/root/usr/local/sbin/rigosctl" && -x "$rigosctl_path" ]] || die 'rigosctl is not executable in the appliance PATH' @@ -130,11 +131,13 @@ if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -F 'sysctl' if strings "$temporary/root/usr/lib/rigos/rigos-performance" | grep -Ei '(/dev/sd|/dev/nvme|cpu model)' >/dev/null; then die 'performance authority contains hardware-name or internal-disk targeting'; fi grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$temporary/root/etc/systemd/system/rigos-miner.service" || die 'miner safety gate is missing' [[ -x "$temporary/root/usr/lib/rigos/rigos-runtime-render" ]] || die 'legacy runtime renderer is missing' -[[ -x "$temporary/root/usr/lib/rigos/rigos-runtime-publish" ]] || die 'runtime publication authority is missing' -grep -Fq 'ExecStart=/usr/lib/rigos/rigos-runtime-publish' "$temporary/root/etc/systemd/system/rigos-runtime-render.service" || die 'runtime publication authority is not wired' -grep -Fq 'ExecCondition=+/usr/lib/rigos/rigos-runtime-publish' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'miner does not republish runtime truth before start' +[[ -x "$temporary/root/usr/lib/rigos/rigos-runtime-publish" ]] || die 'runtime allowlist publisher is missing' +[[ -x "$temporary/root/usr/lib/rigos/rigos-runtime-authority" ]] || die 'serialized runtime authority is missing' +grep -Fq 'ExecStart=/usr/lib/rigos/rigos-runtime-authority' "$temporary/root/etc/systemd/system/rigos-runtime-render.service" || die 'serialized runtime authority is not wired' +grep -Fq 'ExecCondition=+/usr/lib/rigos/rigos-runtime-authority' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'miner does not serialize runtime publication before start' grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-runtime-gate' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'runtime gate is missing from miner override' grep -Fq 'ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'managed miner does not use the short private runtime config option' +grep -Fq 'flock -x -w 30' "$temporary/root/usr/lib/rigos/rigos-runtime-authority" || die 'runtime publication lock is missing' grep -Fq 'construction: "allowlist"' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'public runtime allowlist marker is missing' grep -Fq '.render-stage.XXXXXX' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'private runtime staging directory is missing' grep -Fq -- '--xmrig-config /run/rigos/xmrig-public.json' "$temporary/root/usr/local/bin/rigosd" || die 'rigosd does not default to the public runtime view' From dea78e92bc43e899572c0cfce787fa37b91e4b3c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:18:28 +0700 Subject: [PATCH 126/386] test serialized runtime image verification --- crates/rigos-config/tests/hive_exit_image_verifier.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_image_verifier.rs b/crates/rigos-config/tests/hive_exit_image_verifier.rs index 24fc9dd3..61794f02 100644 --- a/crates/rigos-config/tests/hive_exit_image_verifier.rs +++ b/crates/rigos-config/tests/hive_exit_image_verifier.rs @@ -13,6 +13,7 @@ fn image_verifier_requires_runtime_authority_and_stability_bytes() { for required in [ "usr/lib/rigos/rigos-runtime-publish", + "usr/lib/rigos/rigos-runtime-authority", "usr/lib/rigos/rigos-miner-health", "usr/local/bin/rigosd", "usr/local/bin/rigosctl", @@ -20,10 +21,11 @@ fn image_verifier_requires_runtime_authority_and_stability_bytes() { "etc/systemd/system/rigos-miner.service.d/runtime-render.conf", "etc/systemd/system/rigos-miner.service.d/stability.conf", "etc/systemd/system/rigos-miner-health.timer", - "ExecStart=/usr/lib/rigos/rigos-runtime-publish", - "ExecCondition=+/usr/lib/rigos/rigos-runtime-publish", + "ExecStart=/usr/lib/rigos/rigos-runtime-authority", + "ExecCondition=+/usr/lib/rigos/rigos-runtime-authority", "ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json", "--xmrig-config /run/rigos/xmrig-public.json", + "flock -x -w 30", "construction: \"allowlist\"", "StartLimitBurst=5", ] { From 9ee224f7373ff82cad560ac88eeceaff91478b8d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:23:40 +0700 Subject: [PATCH 127/386] test Hive Exit shell authority syntax --- .../tests/hive_exit_shell_syntax.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 crates/rigos-config/tests/hive_exit_shell_syntax.rs diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs new file mode 100644 index 00000000..dabea7db --- /dev/null +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -0,0 +1,23 @@ +use std::path::PathBuf; +use std::process::Command; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +#[test] +fn runtime_authority_shells_parse_cleanly() { + for path in [ + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish", + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority", + ] { + let status = Command::new("sh") + .arg("-n") + .arg(repo_path(path)) + .status() + .unwrap(); + assert!(status.success(), "shell syntax failed: {path}"); + } +} From 4a4f9f4a052d1be6943e19f47dc8cdadfbfe3698 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 22:43:49 +0700 Subject: [PATCH 128/386] format runtime authority path assertion --- crates/rigos-config/tests/recovery_access.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index fd56a8ba..79e156b2 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -165,9 +165,7 @@ fn alpha8_appliance_wiring_is_explicit() { .expect("read runtime authority service"); assert!(runtime_service.contains("ExecStart=/usr/lib/rigos/rigos-runtime-authority")); - let authority = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority", - ); + let authority = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority"); let mode = fs::metadata(authority).unwrap().permissions(); #[cfg(unix)] { From e8d31f85e75bf155ea618dab30fa1333a8ecd8b0 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 23:18:24 +0700 Subject: [PATCH 129/386] stability enforce LF for appliance scripts --- .gitattributes | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitattributes b/.gitattributes index f210b0cb..da10239b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,18 @@ * text=auto *.sh text eol=lf +*.py text eol=lf *.rs text eol=lf *.toml text eol=lf *.json text eol=lf *.md text eol=lf +*.service text eol=lf +*.timer text eol=lf +*.conf text eol=lf *.ps1 text eol=crlf + +build/usb/hooks/* text eol=lf +build/usb/includes.chroot/usr/local/bin/* text eol=lf +build/usb/includes.chroot/usr/local/sbin/rigos-* text eol=lf +build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-* text eol=lf +build/usb/includes.chroot/usr/lib/rigos/rigos-miner-* text eol=lf +build/usb/includes.chroot/usr/lib/rigos/rigos-remote-* text eol=lf From 685843b95d6f15d0ca42573a8739f848c4ad6ac1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 23:19:00 +0700 Subject: [PATCH 130/386] stability normalize runtime publisher LF --- .../usr/lib/rigos/rigos-runtime-publish | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish index d7bb471c..4ea3da50 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish @@ -1,4 +1,5 @@ #!/bin/sh +# Repository contract: LF line endings are required for WSL and appliance execution. set -eu runtime=${RIGOS_RUNTIME_PATH:-/run/rigos} @@ -7,7 +8,13 @@ mkdir -p "$runtime" chmod 0755 "$runtime" stage=$(mktemp -d "$runtime/.render-stage.XXXXXX") chmod 0700 "$stage" +public_tmp= +status_tmp= +config_tmp= cleanup() { + [ -z "$public_tmp" ] || rm -f "$public_tmp" + [ -z "$status_tmp" ] || rm -f "$status_tmp" + [ -z "$config_tmp" ] || rm -f "$config_tmp" rm -rf "$stage" } trap cleanup EXIT HUP INT TERM @@ -29,7 +36,6 @@ profile=$(jq -r '.profile // empty' "$stage/runtime-config-status.json") public_tmp=$(mktemp "$runtime/.xmrig-public.XXXXXX") status_tmp=$(mktemp "$runtime/.runtime-status.XXXXXX") config_tmp=$(mktemp "$runtime/.xmrig-runtime.XXXXXX") -trap 'rm -f "$public_tmp" "$status_tmp" "$config_tmp"; cleanup' EXIT HUP INT TERM jq \ --arg algorithm "$algorithm" \ @@ -94,6 +100,9 @@ sync -f "$config_tmp" sync -f "$public_tmp" sync -f "$status_tmp" mv -f "$config_tmp" "$runtime/xmrig.json" +config_tmp= mv -f "$public_tmp" "$runtime/xmrig-public.json" +public_tmp= mv -f "$status_tmp" "$runtime/runtime-config-status.json" +status_tmp= sync -f "$runtime" From 9c216d883b8f284061e9c5b65df71b5da2f67981 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 23:19:11 +0700 Subject: [PATCH 131/386] stability normalize runtime authority LF --- build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority index 7cd01f5f..aa09747b 100755 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority @@ -1,4 +1,5 @@ #!/bin/sh +# Repository contract: LF line endings are required for WSL and appliance execution. set -eu runtime=${RIGOS_RUNTIME_PATH:-/run/rigos} From ebbca7dfd01de82c9bf3aea2481be38a3aac7b52 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 23:19:23 +0700 Subject: [PATCH 132/386] test LF shell authority contract --- crates/rigos-config/tests/hive_exit_shell_syntax.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index dabea7db..49a4288b 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -1,3 +1,4 @@ +use std::fs; use std::path::PathBuf; use std::process::Command; @@ -8,14 +9,20 @@ fn repo_path(path: &str) -> PathBuf { } #[test] -fn runtime_authority_shells_parse_cleanly() { +fn runtime_authority_shells_are_lf_and_parse_cleanly() { for path in [ "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish", "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority", ] { + let full_path = repo_path(path); + let bytes = fs::read(&full_path).unwrap(); + assert!( + !bytes.contains(&b'\r'), + "shell authority contains CR/CRLF line endings: {path}" + ); let status = Command::new("sh") .arg("-n") - .arg(repo_path(path)) + .arg(&full_path) .status() .unwrap(); assert!(status.success(), "shell syntax failed: {path}"); From cc99d8b7aaf3d6fc939a69961a79517e2999d658 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 23:48:39 +0700 Subject: [PATCH 133/386] use absolute shell path in runtime publication test --- crates/rigos-config/tests/hive_exit_runtime_publish.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index 47f42069..822e6a2a 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -98,7 +98,7 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { "jq is required by runtime publication authority" ); - let result = Command::new("sh") + let result = Command::new("/bin/sh") .arg(&publisher) .env("RIGOS_STATE_PATH", &state) .env("RIGOS_RUNTIME_PATH", &runtime) From 004eaebeef9b55297c38284aad7842fc0277e881 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Tue, 7 Jul 2026 23:48:49 +0700 Subject: [PATCH 134/386] use absolute shell path in syntax test --- crates/rigos-config/tests/hive_exit_shell_syntax.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index 49a4288b..f8087c5e 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -20,7 +20,7 @@ fn runtime_authority_shells_are_lf_and_parse_cleanly() { !bytes.contains(&b'\r'), "shell authority contains CR/CRLF line endings: {path}" ); - let status = Command::new("sh") + let status = Command::new("/bin/sh") .arg("-n") .arg(&full_path) .status() From 5ea69b7cb6163d840ccd420768ace591088b23ea Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 00:53:40 +0700 Subject: [PATCH 135/386] include jq runtime dependency --- build/usb/package-lists/rigos.list.chroot | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/package-lists/rigos.list.chroot b/build/usb/package-lists/rigos.list.chroot index 85b9c79b..c1477137 100644 --- a/build/usb/package-lists/rigos.list.chroot +++ b/build/usb/package-lists/rigos.list.chroot @@ -3,6 +3,7 @@ curl e2fsprogs fdisk gdisk +jq live-boot live-config linux-image-amd64 From 62e66d16fc6fbca773fff83926c2dbfc1c7f4eb0 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 00:54:02 +0700 Subject: [PATCH 136/386] fail closed on absolute jq runtime --- .../usr/lib/rigos/rigos-runtime-publish | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish index 4ea3da50..c0aa5740 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish @@ -4,6 +4,11 @@ set -eu runtime=${RIGOS_RUNTIME_PATH:-/run/rigos} renderer=${RIGOS_RUNTIME_RENDERER:-/usr/lib/rigos/rigos-runtime-render} +jq_bin=${RIGOS_JQ:-/usr/bin/jq} +if [ ! -x "$jq_bin" ]; then + echo "rigos-runtime-publish: required jq runtime is missing: $jq_bin" >&2 + exit 127 +fi mkdir -p "$runtime" chmod 0755 "$runtime" stage=$(mktemp -d "$runtime/.render-stage.XXXXXX") @@ -21,23 +26,23 @@ trap cleanup EXIT HUP INT TERM RIGOS_RUNTIME_PATH="$stage" "$renderer" -jq -e ' +"$jq_bin" -e ' .schema == "rigos.runtime-config-status/v1" and .outcome == "ready" and (.revision | type == "string") and (.algorithm | type == "string") ' "$stage/runtime-config-status.json" >/dev/null -algorithm=$(jq -r '.algorithm' "$stage/runtime-config-status.json") -revision=$(jq -r '.revision' "$stage/runtime-config-status.json") -thread_count=$(jq -r '.exact_threads // empty' "$stage/runtime-config-status.json") -profile=$(jq -r '.profile // empty' "$stage/runtime-config-status.json") +algorithm=$("$jq_bin" -r '.algorithm' "$stage/runtime-config-status.json") +revision=$("$jq_bin" -r '.revision' "$stage/runtime-config-status.json") +thread_count=$("$jq_bin" -r '.exact_threads // empty' "$stage/runtime-config-status.json") +profile=$("$jq_bin" -r '.profile // empty' "$stage/runtime-config-status.json") public_tmp=$(mktemp "$runtime/.xmrig-public.XXXXXX") status_tmp=$(mktemp "$runtime/.runtime-status.XXXXXX") config_tmp=$(mktemp "$runtime/.xmrig-runtime.XXXXXX") -jq \ +"$jq_bin" \ --arg algorithm "$algorithm" \ --arg revision "$revision" \ --arg profile "$profile" \ @@ -85,7 +90,7 @@ jq \ end ' "$stage/xmrig.json" >"$public_tmp" -jq '. + { +"$jq_bin" '. + { identity_redacted_public_view: true, public_view_construction: "allowlist" }' "$stage/runtime-config-status.json" >"$status_tmp" From b437d029375f4cb01affbb7b2efe3a92350e7eaa Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 00:55:01 +0700 Subject: [PATCH 137/386] verify jq inside appliance squashfs --- scripts/verify-usb-appliance.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 8cf73ebe..6fab56fe 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -88,7 +88,7 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-miner-health.timer \ etc/systemd/system/rigos-runtime-render.service \ etc/systemd/system/rigos-profile-apply.service \ - usr/bin/python3 usr/bin/python3.11 \ + usr/bin/jq usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-miner-health usr/lib/rigos/rigos-runtime-render usr/lib/rigos/rigos-runtime-publish usr/lib/rigos/rigos-runtime-authority usr/lib/rigos/rigos-runtime-gate usr/lib/rigos/xmrig \ usr/local/bin/rigosd usr/local/bin/rigosctl \ @@ -96,6 +96,7 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' +[[ -x "$temporary/root/usr/bin/jq" ]] || die 'jq runtime dependency is missing from the appliance' python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-state-orchestrate" @@ -138,6 +139,7 @@ grep -Fq 'ExecCondition=+/usr/lib/rigos/rigos-runtime-authority' "$temporary/roo grep -Fq 'ExecCondition=/usr/lib/rigos/rigos-runtime-gate' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'runtime gate is missing from miner override' grep -Fq 'ExecStart=/usr/lib/rigos/xmrig -c /run/rigos/xmrig.json' "$temporary/root/etc/systemd/system/rigos-miner.service.d/runtime-render.conf" || die 'managed miner does not use the short private runtime config option' grep -Fq 'flock -x -w 30' "$temporary/root/usr/lib/rigos/rigos-runtime-authority" || die 'runtime publication lock is missing' +grep -Fq 'jq_bin=${RIGOS_JQ:-/usr/bin/jq}' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'runtime publisher does not use the absolute jq dependency' grep -Fq 'construction: "allowlist"' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'public runtime allowlist marker is missing' grep -Fq '.render-stage.XXXXXX' "$temporary/root/usr/lib/rigos/rigos-runtime-publish" || die 'private runtime staging directory is missing' grep -Fq -- '--xmrig-config /run/rigos/xmrig-public.json' "$temporary/root/usr/local/bin/rigosd" || die 'rigosd does not default to the public runtime view' From 1bfd02a460598f95ee4381a4b9f0510f6257713a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 00:55:18 +0700 Subject: [PATCH 138/386] test jq appliance dependency contract --- crates/rigos-config/tests/hive_exit_image_verifier.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/rigos-config/tests/hive_exit_image_verifier.rs b/crates/rigos-config/tests/hive_exit_image_verifier.rs index 61794f02..28400005 100644 --- a/crates/rigos-config/tests/hive_exit_image_verifier.rs +++ b/crates/rigos-config/tests/hive_exit_image_verifier.rs @@ -10,8 +10,18 @@ fn repo_path(path: &str) -> PathBuf { #[test] fn image_verifier_requires_runtime_authority_and_stability_bytes() { let verifier = fs::read_to_string(repo_path("scripts/verify-usb-appliance.sh")).unwrap(); + let packages = + fs::read_to_string(repo_path("build/usb/package-lists/rigos.list.chroot")).unwrap(); + + assert!( + packages.lines().any(|line| line.trim() == "jq"), + "appliance package list is missing jq" + ); for required in [ + "usr/bin/jq", + "jq runtime dependency is missing from the appliance", + "jq_bin=${RIGOS_JQ:-/usr/bin/jq}", "usr/lib/rigos/rigos-runtime-publish", "usr/lib/rigos/rigos-runtime-authority", "usr/lib/rigos/rigos-miner-health", From 732164021d450f5ab7986bfa3a1f3307a81b67e8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 00:56:03 +0700 Subject: [PATCH 139/386] test missing jq fails before publication --- .../tests/hive_exit_runtime_publish.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index 822e6a2a..55462e35 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -217,3 +217,31 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { let _ = fs::remove_dir_all(root); } + +#[test] +fn missing_jq_is_rejected_before_runtime_staging() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-runtime-missing-jq-{unique}")); + let runtime = root.join("run"); + fs::create_dir_all(&runtime).unwrap(); + + let publisher = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish"); + let missing_jq = root.join("missing-jq"); + let output = Command::new("/bin/sh") + .arg(&publisher) + .env("RIGOS_RUNTIME_PATH", &runtime) + .env("RIGOS_JQ", &missing_jq) + .output() + .unwrap(); + + assert_eq!(output.status.code(), Some(127)); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains("required jq runtime is missing")); + assert!(stderr.contains(missing_jq.to_str().unwrap())); + assert_eq!(fs::read_dir(&runtime).unwrap().count(), 0); + + let _ = fs::remove_dir_all(root); +} From e86c2a5cbfff3d8ab51e4b686518e98c46e1c289 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:01:45 +0700 Subject: [PATCH 140/386] normalize appliance package list bytes --- build/usb/package-lists/rigos.list.chroot | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/package-lists/rigos.list.chroot b/build/usb/package-lists/rigos.list.chroot index c1477137..fc7df3aa 100644 --- a/build/usb/package-lists/rigos.list.chroot +++ b/build/usb/package-lists/rigos.list.chroot @@ -1,3 +1,4 @@ +# Runtime dependencies required by boot-time RIGOS authorities. ca-certificates curl e2fsprogs From 11471b95e367adca74c915235b65ecfb8ee7f7e8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:02:08 +0700 Subject: [PATCH 141/386] test package list LF contract --- crates/rigos-config/tests/hive_exit_image_verifier.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_image_verifier.rs b/crates/rigos-config/tests/hive_exit_image_verifier.rs index 28400005..309650ba 100644 --- a/crates/rigos-config/tests/hive_exit_image_verifier.rs +++ b/crates/rigos-config/tests/hive_exit_image_verifier.rs @@ -10,8 +10,13 @@ fn repo_path(path: &str) -> PathBuf { #[test] fn image_verifier_requires_runtime_authority_and_stability_bytes() { let verifier = fs::read_to_string(repo_path("scripts/verify-usb-appliance.sh")).unwrap(); - let packages = - fs::read_to_string(repo_path("build/usb/package-lists/rigos.list.chroot")).unwrap(); + let package_path = repo_path("build/usb/package-lists/rigos.list.chroot"); + let package_bytes = fs::read(&package_path).unwrap(); + assert!( + !package_bytes.contains(&b'\r'), + "appliance package list contains CR/CRLF line endings" + ); + let packages = String::from_utf8(package_bytes).unwrap(); assert!( packages.lines().any(|line| line.trim() == "jq"), From 223794509e82d5dedda79da87a11ed2941c16c34 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:03:19 +0700 Subject: [PATCH 142/386] enforce LF for RIGOS package manifest --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index da10239b..b2ec61b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,6 +11,7 @@ *.ps1 text eol=crlf build/usb/hooks/* text eol=lf +build/usb/package-lists/rigos.list.chroot text eol=lf build/usb/includes.chroot/usr/local/bin/* text eol=lf build/usb/includes.chroot/usr/local/sbin/rigos-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-* text eol=lf From 926072a2cb673b61db58df479aaf7f0f9a20dad6 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:55:23 +0700 Subject: [PATCH 143/386] add reversible RandomX MSR authority --- .../usr/lib/rigos/rigos-randomx-msr | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr new file mode 100644 index 00000000..27009d6f --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr @@ -0,0 +1,440 @@ +#!/usr/bin/python3 +# Repository contract: LF line endings are required for appliance execution. +import argparse +import datetime as dt +import fcntl +import json +import os +import struct +import sys +import uuid +from pathlib import Path + +STATUS_SCHEMA = "rigos.randomx-msr-status/v1" +STATE_SCHEMA = "rigos.randomx-msr-state/v1" +REGISTER = 0x1A4 +TARGET_VALUE = 0xF +SUPPORTED_CPUS = {("GenuineIntel", 6, 42)} + + +class AuthorityError(RuntimeError): + pass + + +def utc_now() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat(timespec="milliseconds") + + +def value_hex(value: int) -> str: + return f"0x{value:016x}" + + +def read_trimmed(path: Path, name: str) -> str: + try: + value = path.read_text(encoding="ascii").strip() + except (OSError, UnicodeError) as error: + raise AuthorityError(f"{name}: {error}") from error + if not value: + raise AuthorityError(f"{name} is empty") + return value + + +def parse_cpu_identity(path: Path) -> tuple[str, int, int]: + try: + text = path.read_text(encoding="ascii") + except (OSError, UnicodeError) as error: + raise AuthorityError(f"cpuinfo: {error}") from error + first = text.split("\n\n", 1)[0] + fields: dict[str, str] = {} + for line in first.splitlines(): + if ":" not in line: + continue + key, value = line.split(":", 1) + fields[key.strip()] = value.strip() + vendor = fields.get("vendor_id") + family = fields.get("cpu family") + model = fields.get("model") + if not vendor or family is None or model is None: + raise AuthorityError("cpu identity fields are incomplete") + try: + return vendor, int(family, 10), int(model, 10) + except ValueError as error: + raise AuthorityError("cpu family/model is malformed") from error + + +def parse_online_cpus(text: str) -> list[int]: + value = text.strip() + if not value: + raise AuthorityError("online CPU list is empty") + cpus: set[int] = set() + for item in value.split(","): + item = item.strip() + if not item: + raise AuthorityError("online CPU list contains an empty item") + if "-" in item: + start_text, end_text = item.split("-", 1) + try: + start = int(start_text, 10) + end = int(end_text, 10) + except ValueError as error: + raise AuthorityError("online CPU range is malformed") from error + if start < 0 or end < start: + raise AuthorityError("online CPU range is invalid") + cpus.update(range(start, end + 1)) + else: + try: + cpu = int(item, 10) + except ValueError as error: + raise AuthorityError("online CPU identifier is malformed") from error + if cpu < 0: + raise AuthorityError("online CPU identifier is invalid") + cpus.add(cpu) + if not cpus: + raise AuthorityError("online CPU list resolved to no CPUs") + return sorted(cpus) + + +def msr_path(device_root: Path, cpu: int) -> Path: + return device_root / str(cpu) / "msr" + + +def read_msr(device_root: Path, cpu: int, register: int = REGISTER) -> int: + path = msr_path(device_root, cpu) + try: + fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC) + except OSError as error: + raise AuthorityError(f"open {path}: {error}") from error + try: + data = os.pread(fd, 8, register) + except OSError as error: + raise AuthorityError(f"read CPU {cpu} MSR {register:#x}: {error}") from error + finally: + os.close(fd) + if len(data) != 8: + raise AuthorityError(f"short read from CPU {cpu} MSR {register:#x}") + return struct.unpack(" None: + path = msr_path(device_root, cpu) + try: + fd = os.open(path, os.O_RDWR | os.O_CLOEXEC) + except OSError as error: + raise AuthorityError(f"open {path} for write: {error}") from error + try: + written = os.pwrite(fd, struct.pack(" None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") + data = json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n" + fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC, mode) + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC) + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + try: + temporary.unlink() + except FileNotFoundError: + pass + + +def unlink_synced(path: Path) -> None: + try: + path.unlink() + except FileNotFoundError: + return + directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC) + try: + os.fsync(directory) + finally: + os.close(directory) + + +def load_state(path: Path) -> dict | None: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + return None + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise AuthorityError(f"MSR state is unreadable: {error}") from error + if not isinstance(value, dict) or value.get("schema") != STATE_SCHEMA: + raise AuthorityError("MSR state schema is invalid") + if value.get("register") != value_hex(REGISTER): + raise AuthorityError("MSR state register is invalid") + entries = value.get("cpus") + if not isinstance(entries, list) or not entries: + raise AuthorityError("MSR state CPU list is invalid") + for entry in entries: + if not isinstance(entry, dict): + raise AuthorityError("MSR state CPU entry is invalid") + if not isinstance(entry.get("cpu"), int) or not isinstance(entry.get("original"), str): + raise AuthorityError("MSR state CPU value is invalid") + try: + int(entry["original"], 16) + except ValueError as error: + raise AuthorityError("MSR state original value is malformed") from error + return value + + +def base_status(boot_id: str, vendor: str | None, family: int | None, model: int | None) -> dict: + return { + "schema": STATUS_SCHEMA, + "generated_at": utc_now(), + "boot_id": boot_id, + "vendor_id": vendor, + "cpu_family": family, + "cpu_model": model, + "register": value_hex(REGISTER), + "target_value": value_hex(TARGET_VALUE), + } + + +def publish(status_path: Path, value: dict) -> None: + atomic_json(status_path, value, 0o644) + print(json.dumps(value, sort_keys=True, separators=(",", ":"))) + + +def restore_values(device_root: Path, state: dict) -> tuple[bool, list[str], list[dict]]: + errors: list[str] = [] + evidence: list[dict] = [] + entries = state["cpus"] + for entry in entries: + cpu = entry["cpu"] + original = int(entry["original"], 16) + try: + write_msr(device_root, cpu, original) + except AuthorityError as error: + errors.append(str(error)) + for entry in entries: + cpu = entry["cpu"] + original = int(entry["original"], 16) + try: + readback = read_msr(device_root, cpu) + evidence.append({ + "cpu": cpu, + "original": value_hex(original), + "readback": value_hex(readback), + }) + if readback != original: + errors.append( + f"CPU {cpu} restore readback mismatch: expected {value_hex(original)} got {value_hex(readback)}" + ) + except AuthorityError as error: + errors.append(str(error)) + return not errors, errors, evidence + + +def current_values_match_target(device_root: Path, state: dict) -> bool: + try: + return all( + read_msr(device_root, entry["cpu"]) == TARGET_VALUE + for entry in state["cpus"] + ) + except AuthorityError: + return False + + +def apply(args: argparse.Namespace) -> int: + boot_id = read_trimmed(args.boot_id, "boot ID") + vendor, family, model = parse_cpu_identity(args.cpuinfo) + cpus = parse_online_cpus(read_trimmed(args.online, "online CPU list")) + status = base_status(boot_id, vendor, family, model) + status["online_cpus"] = cpus + + state = load_state(args.state) + if state is not None: + if state.get("boot_id") != boot_id: + unlink_synced(args.state) + state = None + elif current_values_match_target(args.device_root, state): + status.update({ + "outcome": "ready", + "reason": "already_applied", + "cpus": state["cpus"], + "rollback": {"attempted": False, "complete": None}, + }) + publish(args.status, status) + return 0 + else: + restored, errors, evidence = restore_values(args.device_root, state) + if not restored: + status.update({ + "outcome": "degraded", + "reason": "previous_state_restore_failed", + "errors": errors, + "cpus": evidence, + "rollback": {"attempted": True, "complete": False}, + }) + publish(args.status, status) + return 0 + unlink_synced(args.state) + state = None + + if (vendor, family, model) not in SUPPORTED_CPUS: + status.update({ + "outcome": "unsupported", + "reason": "cpu_not_allowlisted", + "cpus": [], + "rollback": {"attempted": False, "complete": None}, + }) + publish(args.status, status) + return 0 + + originals: list[dict] = [] + try: + for cpu in cpus: + originals.append({"cpu": cpu, "original": value_hex(read_msr(args.device_root, cpu))}) + except AuthorityError as error: + status.update({ + "outcome": "unavailable", + "reason": "msr_device_unavailable", + "errors": [str(error)], + "cpus": originals, + "rollback": {"attempted": False, "complete": None}, + }) + publish(args.status, status) + return 0 + + state = { + "schema": STATE_SCHEMA, + "boot_id": boot_id, + "register": value_hex(REGISTER), + "target_value": value_hex(TARGET_VALUE), + "cpus": originals, + } + atomic_json(args.state, state, 0o600) + + apply_error: str | None = None + evidence: list[dict] = [] + try: + for cpu in cpus: + write_msr(args.device_root, cpu, TARGET_VALUE) + for entry in originals: + cpu = entry["cpu"] + readback = read_msr(args.device_root, cpu) + evidence.append({ + "cpu": cpu, + "original": entry["original"], + "readback": value_hex(readback), + }) + if readback != TARGET_VALUE: + raise AuthorityError( + f"CPU {cpu} apply readback mismatch: expected {value_hex(TARGET_VALUE)} got {value_hex(readback)}" + ) + except AuthorityError as error: + apply_error = str(error) + + if apply_error is not None: + restored, restore_errors, restore_evidence = restore_values(args.device_root, state) + if restored: + unlink_synced(args.state) + status.update({ + "outcome": "degraded", + "reason": "apply_failed_rolled_back" if restored else "apply_failed_rollback_incomplete", + "errors": [apply_error, *restore_errors], + "cpus": restore_evidence, + "rollback": {"attempted": True, "complete": restored}, + }) + publish(args.status, status) + return 0 + + status.update({ + "outcome": "ready", + "reason": None, + "cpus": evidence, + "rollback": {"attempted": False, "complete": None}, + }) + publish(args.status, status) + return 0 + + +def restore(args: argparse.Namespace) -> int: + boot_id = read_trimmed(args.boot_id, "boot ID") + vendor: str | None = None + family: int | None = None + model: int | None = None + try: + vendor, family, model = parse_cpu_identity(args.cpuinfo) + except AuthorityError: + pass + status = base_status(boot_id, vendor, family, model) + state = load_state(args.state) + if state is None: + status.update({ + "outcome": "restored", + "reason": "state_absent", + "cpus": [], + "rollback": {"attempted": False, "complete": True}, + }) + publish(args.status, status) + return 0 + if state.get("boot_id") != boot_id: + unlink_synced(args.state) + status.update({ + "outcome": "restored", + "reason": "stale_state_discarded", + "cpus": [], + "rollback": {"attempted": False, "complete": True}, + }) + publish(args.status, status) + return 0 + restored, errors, evidence = restore_values(args.device_root, state) + if restored: + unlink_synced(args.state) + status.update({ + "outcome": "restored" if restored else "degraded", + "reason": None if restored else "restore_failed", + "errors": errors, + "cpus": evidence, + "rollback": {"attempted": True, "complete": restored}, + }) + publish(args.status, status) + return 0 + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser(description="RIGOS reversible RandomX MSR authority") + result.add_argument("command", choices=("apply", "restore")) + result.add_argument("--cpuinfo", type=Path, default=Path("/proc/cpuinfo")) + result.add_argument("--online", type=Path, default=Path("/sys/devices/system/cpu/online")) + result.add_argument("--device-root", type=Path, default=Path("/dev/cpu")) + result.add_argument("--boot-id", type=Path, default=Path("/proc/sys/kernel/random/boot_id")) + result.add_argument("--status", type=Path, default=Path("/run/rigos/randomx-msr-status.json")) + result.add_argument("--state", type=Path, default=Path("/run/rigos/randomx-msr-state.json")) + result.add_argument("--lock", type=Path, default=Path("/run/rigos/.randomx-msr.lock")) + return result + + +def main() -> int: + args = parser().parse_args() + args.lock.parent.mkdir(parents=True, exist_ok=True) + with args.lock.open("a+b") as lock_handle: + os.chmod(args.lock, 0o600) + fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX) + if args.command == "apply": + return apply(args) + return restore(args) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (AuthorityError, OSError, ValueError) as error: + print(f"rigos-randomx-msr: {error}", file=sys.stderr) + raise SystemExit(2) From 3e410856a1498a9b7850e8a178395663b47d21d8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:55:38 +0700 Subject: [PATCH 144/386] wire reversible RandomX MSR service --- .../systemd/system/rigos-randomx-msr.service | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service b/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service new file mode 100644 index 00000000..16e17ceb --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service @@ -0,0 +1,32 @@ +[Unit] +Description=Apply reversible RIGOS RandomX MSR optimization +After=rigos-state-ready.service rigos-profile-apply.service +Requires=rigos-state-ready.service rigos-profile-apply.service +Before=rigos-miner.service +ConditionArchitecture=x86-64 +ConditionPathExists=/var/lib/rigos/current + +[Service] +Type=oneshot +ExecStartPre=-/usr/sbin/modprobe msr +ExecStart=/usr/lib/rigos/rigos-randomx-msr apply +ExecStop=/usr/lib/rigos/rigos-randomx-msr restore +RemainAfterExit=yes +TimeoutStartSec=20s +TimeoutStopSec=20s +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ProtectSystem=strict +ProtectKernelTunables=yes +ProtectKernelModules=no +ProtectControlGroups=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +LockPersonality=yes +CapabilityBoundingSet=CAP_SYS_MODULE CAP_SYS_RAWIO +AmbientCapabilities=CAP_SYS_MODULE CAP_SYS_RAWIO +ReadWritePaths=/run/rigos /dev/cpu + +[Install] +WantedBy=multi-user.target From 72a872584ee040abec840f24a55c814a60c8bd9b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:55:53 +0700 Subject: [PATCH 145/386] order miner after RandomX MSR authority --- .../etc/systemd/system/rigos-miner.service.d/randomx-msr.conf | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf new file mode 100644 index 00000000..e90c9d0b --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf @@ -0,0 +1,3 @@ +[Unit] +Wants=rigos-randomx-msr.service +After=rigos-randomx-msr.service From dbb4c8e5022a1261dbcd48f39d15f7a4ceae5f62 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:56:15 +0700 Subject: [PATCH 146/386] make kmod an explicit MSR runtime dependency --- build/usb/package-lists/rigos.list.chroot | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/package-lists/rigos.list.chroot b/build/usb/package-lists/rigos.list.chroot index fc7df3aa..0750eda2 100644 --- a/build/usb/package-lists/rigos.list.chroot +++ b/build/usb/package-lists/rigos.list.chroot @@ -5,6 +5,7 @@ e2fsprogs fdisk gdisk jq +kmod live-boot live-config linux-image-amd64 From edae33aa584555b603f0d7d0326b48ac2ddba232 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:57:00 +0700 Subject: [PATCH 147/386] run MSR authority through packaged Python runtime --- .../etc/systemd/system/rigos-randomx-msr.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service b/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service index 16e17ceb..07130115 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service @@ -9,8 +9,8 @@ ConditionPathExists=/var/lib/rigos/current [Service] Type=oneshot ExecStartPre=-/usr/sbin/modprobe msr -ExecStart=/usr/lib/rigos/rigos-randomx-msr apply -ExecStop=/usr/lib/rigos/rigos-randomx-msr restore +ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-randomx-msr apply +ExecStop=/usr/bin/python3 /usr/lib/rigos/rigos-randomx-msr restore RemainAfterExit=yes TimeoutStartSec=20s TimeoutStopSec=20s From 742596504e2a621a45734d1f894d85b297eae3f4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:57:28 +0700 Subject: [PATCH 148/386] allow absent MSR device path before module load --- .../etc/systemd/system/rigos-randomx-msr.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service b/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service index 07130115..25ba6c4c 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service @@ -26,7 +26,7 @@ RestrictNamespaces=yes LockPersonality=yes CapabilityBoundingSet=CAP_SYS_MODULE CAP_SYS_RAWIO AmbientCapabilities=CAP_SYS_MODULE CAP_SYS_RAWIO -ReadWritePaths=/run/rigos /dev/cpu +ReadWritePaths=/run/rigos -/dev/cpu [Install] WantedBy=multi-user.target From 12229b8093bb72f717c520965a286137402afb9b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:58:19 +0700 Subject: [PATCH 149/386] test reversible RandomX MSR authority --- .../tests/randomx_msr_authority.rs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 crates/rigos-config/tests/randomx_msr_authority.rs diff --git a/crates/rigos-config/tests/randomx_msr_authority.rs b/crates/rigos-config/tests/randomx_msr_authority.rs new file mode 100644 index 00000000..9f74ea5b --- /dev/null +++ b/crates/rigos-config/tests/randomx_msr_authority.rs @@ -0,0 +1,208 @@ +use serde_json::Value; +use std::fs::{self, File, OpenOptions}; +use std::io::{Seek, SeekFrom, Write}; +use std::os::unix::fs::{FileExt, PermissionsExt, symlink}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +const REGISTER: u64 = 0x1a4; +const TARGET: u64 = 0x0f; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +fn unique_root(name: &str) -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!("rigos-{name}-{unique}")) +} + +fn write_cpuinfo(path: &Path, model: u32) { + fs::write( + path, + format!( + "processor : 0\nvendor_id : GenuineIntel\ncpu family : 6\nmodel : {model}\nmodel name : Synthetic CPU\n\n" + ), + ) + .unwrap(); +} + +fn create_msr(path: &Path, value: u64) { + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut file = OpenOptions::new() + .create_new(true) + .read(true) + .write(true) + .open(path) + .unwrap(); + file.seek(SeekFrom::Start(REGISTER)).unwrap(); + file.write_all(&value.to_le_bytes()).unwrap(); + file.sync_all().unwrap(); +} + +fn read_msr(path: &Path) -> u64 { + let file = File::open(path).unwrap(); + let mut bytes = [0_u8; 8]; + file.read_exact_at(&mut bytes, REGISTER).unwrap(); + u64::from_le_bytes(bytes) +} + +fn run_authority(root: &Path, command: &str) -> std::process::Output { + let script = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr"); + Command::new("/usr/bin/python3") + .arg(script) + .arg(command) + .arg("--cpuinfo") + .arg(root.join("cpuinfo")) + .arg("--online") + .arg(root.join("online")) + .arg("--device-root") + .arg(root.join("dev/cpu")) + .arg("--boot-id") + .arg(root.join("boot_id")) + .arg("--status") + .arg(root.join("run/status.json")) + .arg("--state") + .arg(root.join("run/state.json")) + .arg("--lock") + .arg(root.join("run/authority.lock")) + .output() + .unwrap() +} + +fn status(root: &Path) -> Value { + serde_json::from_slice(&fs::read(root.join("run/status.json")).unwrap()).unwrap() +} + +#[test] +fn source_wiring_is_optional_reversible_and_narrow() { + let service = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-randomx-msr.service", + )) + .unwrap(); + let dropin = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf", + )) + .unwrap(); + let packages = + fs::read_to_string(repo_path("build/usb/package-lists/rigos.list.chroot")).unwrap(); + let authority = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr", + )) + .unwrap(); + + for required in [ + "ExecStartPre=-/usr/sbin/modprobe msr", + "ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-randomx-msr apply", + "ExecStop=/usr/bin/python3 /usr/lib/rigos/rigos-randomx-msr restore", + "CapabilityBoundingSet=CAP_SYS_MODULE CAP_SYS_RAWIO", + "ReadWritePaths=/run/rigos -/dev/cpu", + ] { + assert!(service.contains(required), "service is missing {required}"); + } + assert!(dropin.contains("Wants=rigos-randomx-msr.service")); + assert!(dropin.contains("After=rigos-randomx-msr.service")); + assert!(!dropin.contains("Requires=rigos-randomx-msr.service")); + assert!(packages.lines().any(|line| line == "kmod")); + assert!(authority.contains("SUPPORTED_CPUS = {(\"GenuineIntel\", 6, 42)}")); + assert!(authority.contains("REGISTER = 0x1A4")); + assert!(authority.contains("TARGET_VALUE = 0xF")); + assert!(authority.contains("apply_failed_rolled_back")); + assert!(authority.contains("apply_failed_rollback_incomplete")); + assert!(authority.contains("stale_state_discarded")); +} + +#[test] +fn supported_cpu_apply_is_idempotent_and_restore_recovers_original_values() { + let root = unique_root("randomx-msr-roundtrip"); + fs::create_dir_all(root.join("run")).unwrap(); + write_cpuinfo(&root.join("cpuinfo"), 42); + fs::write(root.join("online"), "0-1\n").unwrap(); + fs::write(root.join("boot_id"), "00000000-0000-0000-0000-000000000001\n").unwrap(); + let cpu0 = root.join("dev/cpu/0/msr"); + let cpu1 = root.join("dev/cpu/1/msr"); + create_msr(&cpu0, 0x10); + create_msr(&cpu1, 0x20); + + let applied = run_authority(&root, "apply"); + assert!(applied.status.success(), "{}", String::from_utf8_lossy(&applied.stderr)); + assert_eq!(read_msr(&cpu0), TARGET); + assert_eq!(read_msr(&cpu1), TARGET); + let first = status(&root); + assert_eq!(first["outcome"], "ready"); + assert!(first["reason"].is_null()); + assert_eq!( + fs::metadata(root.join("run/state.json")) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o600 + ); + + let repeated = run_authority(&root, "apply"); + assert!(repeated.status.success()); + let second = status(&root); + assert_eq!(second["outcome"], "ready"); + assert_eq!(second["reason"], "already_applied"); + + let restored = run_authority(&root, "restore"); + assert!(restored.status.success(), "{}", String::from_utf8_lossy(&restored.stderr)); + assert_eq!(read_msr(&cpu0), 0x10); + assert_eq!(read_msr(&cpu1), 0x20); + assert!(!root.join("run/state.json").exists()); + let final_status = status(&root); + assert_eq!(final_status["outcome"], "restored"); + assert!(final_status["reason"].is_null()); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn unsupported_cpu_is_truthful_and_never_requires_msr_devices() { + let root = unique_root("randomx-msr-unsupported"); + fs::create_dir_all(root.join("run")).unwrap(); + write_cpuinfo(&root.join("cpuinfo"), 58); + fs::write(root.join("online"), "0-3\n").unwrap(); + fs::write(root.join("boot_id"), "00000000-0000-0000-0000-000000000002\n").unwrap(); + + let output = run_authority(&root, "apply"); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let value = status(&root); + assert_eq!(value["outcome"], "unsupported"); + assert_eq!(value["reason"], "cpu_not_allowlisted"); + assert!(!root.join("run/state.json").exists()); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn partial_write_failure_rolls_back_every_recoverable_cpu_and_keeps_state() { + let root = unique_root("randomx-msr-rollback"); + fs::create_dir_all(root.join("run")).unwrap(); + write_cpuinfo(&root.join("cpuinfo"), 42); + fs::write(root.join("online"), "0-1\n").unwrap(); + fs::write(root.join("boot_id"), "00000000-0000-0000-0000-000000000003\n").unwrap(); + let cpu0 = root.join("dev/cpu/0/msr"); + create_msr(&cpu0, 0x55); + fs::create_dir_all(root.join("dev/cpu/1")).unwrap(); + symlink("/dev/full", root.join("dev/cpu/1/msr")).unwrap(); + + let output = run_authority(&root, "apply"); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(read_msr(&cpu0), 0x55); + let value = status(&root); + assert_eq!(value["outcome"], "degraded"); + assert_eq!(value["reason"], "apply_failed_rollback_incomplete"); + assert_eq!(value["rollback"]["attempted"], true); + assert_eq!(value["rollback"]["complete"], false); + assert!(root.join("run/state.json").exists()); + + let _ = fs::remove_dir_all(root); +} From 4d0363cc0b985224be2cd37d3df2c06bae360697 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:59:09 +0700 Subject: [PATCH 150/386] verify RandomX performance authority inside image --- scripts/verify-randomx-performance-image.sh | 81 +++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 scripts/verify-randomx-performance-image.sh diff --git a/scripts/verify-randomx-performance-image.sh b/scripts/verify-randomx-performance-image.sh new file mode 100644 index 00000000..89cdc92c --- /dev/null +++ b/scripts/verify-randomx-performance-image.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -euo pipefail + +die() { + printf 'verify-randomx-performance-image: %s\n' "$*" >&2 + exit 1 +} + +[[ $# -eq 1 ]] || die 'usage: verify-randomx-performance-image.sh ' +image="$(readlink -f "$1")" +[[ -f "$image" ]] || die "image is missing: $image" +[[ "$(id -u)" -eq 0 ]] || die 'must run as root' + +partition_json="$(sfdisk --json "$image")" +start="$(jq -r '.partitiontable.partitions[1].start' <<<"$partition_json")" +size="$(jq -r '.partitiontable.partitions[1].size' <<<"$partition_json")" +[[ "$start" =~ ^[0-9]+$ && "$size" =~ ^[0-9]+$ ]] || die 'ROOT_A geometry is invalid' + +loop="$(losetup --find --show --read-only --offset $((start * 512)) --sizelimit $((size * 512)) "$image")" +temporary="$(mktemp -d)" +cleanup() { + set +e + mountpoint -q "$temporary/root-a" && umount "$temporary/root-a" + losetup -d "$loop" 2>/dev/null + rm -rf "$temporary" +} +trap cleanup EXIT + +mkdir -p "$temporary/root-a" "$temporary/squash" +mount -o ro "$loop" "$temporary/root-a" +squashfs="$temporary/root-a/live/filesystem.squashfs" +[[ -f "$squashfs" ]] || die 'ROOT_A squashfs is missing' + +unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ + usr/bin/python3 usr/bin/python3.11 usr/sbin/modprobe \ + usr/lib/rigos/rigos-randomx-msr \ + etc/systemd/system/rigos-randomx-msr.service \ + etc/systemd/system/rigos-miner.service \ + etc/systemd/system/rigos-miner.service.d/randomx-msr.conf \ + >/dev/null + +root="$temporary/squash" +[[ -x "$root/usr/bin/python3" ]] || die 'Python runtime for MSR authority is missing' +[[ -x "$root/usr/sbin/modprobe" ]] || die 'modprobe runtime for MSR authority is missing' +[[ -f "$root/usr/lib/rigos/rigos-randomx-msr" ]] || die 'RandomX MSR authority is missing' +python3 -m py_compile "$root/usr/lib/rigos/rigos-randomx-msr" + +service="$root/etc/systemd/system/rigos-randomx-msr.service" +dropin="$root/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf" +miner="$root/etc/systemd/system/rigos-miner.service" +authority="$root/usr/lib/rigos/rigos-randomx-msr" + +for required in \ + 'ExecStartPre=-/usr/sbin/modprobe msr' \ + 'ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-randomx-msr apply' \ + 'ExecStop=/usr/bin/python3 /usr/lib/rigos/rigos-randomx-msr restore' \ + 'CapabilityBoundingSet=CAP_SYS_MODULE CAP_SYS_RAWIO' \ + 'ReadWritePaths=/run/rigos -/dev/cpu' +do + grep -Fqx "$required" "$service" || die "MSR service contract is missing: $required" +done + +grep -Fqx 'Wants=rigos-randomx-msr.service' "$dropin" || die 'miner does not want optional MSR authority' +grep -Fqx 'After=rigos-randomx-msr.service' "$dropin" || die 'miner is not ordered after MSR authority' +if grep -Fq 'Requires=rigos-randomx-msr.service' "$dropin"; then + die 'optional MSR optimization incorrectly blocks the miner' +fi +grep -Fqx 'User=rigos' "$miner" || die 'miner no longer runs as the unprivileged rigos user' + +for required in \ + 'SUPPORTED_CPUS = {("GenuineIntel", 6, 42)}' \ + 'REGISTER = 0x1A4' \ + 'TARGET_VALUE = 0xF' \ + 'apply_failed_rolled_back' \ + 'apply_failed_rollback_incomplete' \ + 'stale_state_discarded' +do + grep -Fq "$required" "$authority" || die "MSR authority contract is missing: $required" +done + +printf 'RIGOS RandomX performance image verification passed: %s\n' "$image" From 2c771ba56c0ce9a9afd60d609c3905b7bf94717c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:59:29 +0700 Subject: [PATCH 151/386] gate image build on RandomX authority tests --- scripts/build-usb-image-entrypoint.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 scripts/build-usb-image-entrypoint.sh diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh new file mode 100644 index 00000000..ed0ee6a6 --- /dev/null +++ b/scripts/build-usb-image-entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo" + +python3 ./scripts/check-alpha8-ssh-hotfix.py +python3 -m py_compile ./build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr + +export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target +cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture + +./scripts/build-usb-image.sh + +# shellcheck disable=SC1091 +source ./build/usb/version.env +image="./dist/usb/${RIGOS_IMAGE_ID}-${RIGOS_IMAGE_VERSION}.img" +bash ./scripts/verify-randomx-performance-image.sh "$image" From 1489db70074bcbc01cbc5c02fa612d9ad02a3f62 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 01:59:45 +0700 Subject: [PATCH 152/386] use performance-gated image build entrypoint --- build/usb/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/Dockerfile b/build/usb/Dockerfile index be63222a..a6dce58f 100644 --- a/build/usb/Dockerfile +++ b/build/usb/Dockerfile @@ -14,4 +14,4 @@ RUN command -v cargo \ && rustc --version WORKDIR /source -ENTRYPOINT ["/bin/bash", "-c", "python3 ./scripts/check-alpha8-ssh-hotfix.py && exec ./scripts/build-usb-image.sh"] +ENTRYPOINT ["/bin/bash", "./scripts/build-usb-image-entrypoint.sh"] From 60eb695fe14424f25b005e7e3dde45d65b7a1a41 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:09:01 +0700 Subject: [PATCH 153/386] format RandomX MSR authority tests --- .../tests/randomx_msr_authority.rs | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/rigos-config/tests/randomx_msr_authority.rs b/crates/rigos-config/tests/randomx_msr_authority.rs index 9f74ea5b..9b45ad03 100644 --- a/crates/rigos-config/tests/randomx_msr_authority.rs +++ b/crates/rigos-config/tests/randomx_msr_authority.rs @@ -124,14 +124,22 @@ fn supported_cpu_apply_is_idempotent_and_restore_recovers_original_values() { fs::create_dir_all(root.join("run")).unwrap(); write_cpuinfo(&root.join("cpuinfo"), 42); fs::write(root.join("online"), "0-1\n").unwrap(); - fs::write(root.join("boot_id"), "00000000-0000-0000-0000-000000000001\n").unwrap(); + fs::write( + root.join("boot_id"), + "00000000-0000-0000-0000-000000000001\n", + ) + .unwrap(); let cpu0 = root.join("dev/cpu/0/msr"); let cpu1 = root.join("dev/cpu/1/msr"); create_msr(&cpu0, 0x10); create_msr(&cpu1, 0x20); let applied = run_authority(&root, "apply"); - assert!(applied.status.success(), "{}", String::from_utf8_lossy(&applied.stderr)); + assert!( + applied.status.success(), + "{}", + String::from_utf8_lossy(&applied.stderr) + ); assert_eq!(read_msr(&cpu0), TARGET); assert_eq!(read_msr(&cpu1), TARGET); let first = status(&root); @@ -153,7 +161,11 @@ fn supported_cpu_apply_is_idempotent_and_restore_recovers_original_values() { assert_eq!(second["reason"], "already_applied"); let restored = run_authority(&root, "restore"); - assert!(restored.status.success(), "{}", String::from_utf8_lossy(&restored.stderr)); + assert!( + restored.status.success(), + "{}", + String::from_utf8_lossy(&restored.stderr) + ); assert_eq!(read_msr(&cpu0), 0x10); assert_eq!(read_msr(&cpu1), 0x20); assert!(!root.join("run/state.json").exists()); @@ -170,10 +182,18 @@ fn unsupported_cpu_is_truthful_and_never_requires_msr_devices() { fs::create_dir_all(root.join("run")).unwrap(); write_cpuinfo(&root.join("cpuinfo"), 58); fs::write(root.join("online"), "0-3\n").unwrap(); - fs::write(root.join("boot_id"), "00000000-0000-0000-0000-000000000002\n").unwrap(); + fs::write( + root.join("boot_id"), + "00000000-0000-0000-0000-000000000002\n", + ) + .unwrap(); let output = run_authority(&root, "apply"); - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); let value = status(&root); assert_eq!(value["outcome"], "unsupported"); assert_eq!(value["reason"], "cpu_not_allowlisted"); @@ -188,14 +208,22 @@ fn partial_write_failure_rolls_back_every_recoverable_cpu_and_keeps_state() { fs::create_dir_all(root.join("run")).unwrap(); write_cpuinfo(&root.join("cpuinfo"), 42); fs::write(root.join("online"), "0-1\n").unwrap(); - fs::write(root.join("boot_id"), "00000000-0000-0000-0000-000000000003\n").unwrap(); + fs::write( + root.join("boot_id"), + "00000000-0000-0000-0000-000000000003\n", + ) + .unwrap(); let cpu0 = root.join("dev/cpu/0/msr"); create_msr(&cpu0, 0x55); fs::create_dir_all(root.join("dev/cpu/1")).unwrap(); symlink("/dev/full", root.join("dev/cpu/1/msr")).unwrap(); let output = run_authority(&root, "apply"); - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert_eq!(read_msr(&cpu0), 0x55); let value = status(&root); assert_eq!(value["outcome"], "degraded"); From 528c4e4a162089fec8f2568337ec1dda45085b65 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:13:52 +0700 Subject: [PATCH 154/386] preserve non-login builder entrypoint contract --- build/usb/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/Dockerfile b/build/usb/Dockerfile index a6dce58f..02958f38 100644 --- a/build/usb/Dockerfile +++ b/build/usb/Dockerfile @@ -14,4 +14,4 @@ RUN command -v cargo \ && rustc --version WORKDIR /source -ENTRYPOINT ["/bin/bash", "./scripts/build-usb-image-entrypoint.sh"] +ENTRYPOINT ["/bin/bash", "-c", "exec /bin/bash ./scripts/build-usb-image-entrypoint.sh"] From afbda05c4803480774950271361a96632c1b36fb Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:40:59 +0700 Subject: [PATCH 155/386] add RandomX MSR gate skeleton --- .../usr/lib/rigos/rigos-randomx-msr-gate | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate new file mode 100644 index 00000000..39a5a6d3 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate @@ -0,0 +1,11 @@ +#!/usr/bin/python3 +# Repository contract: LF line endings are required for appliance execution. +import sys + + +def main() -> int: + return 20 + + +if __name__ == "__main__": + raise SystemExit(main()) From ab7789612df87b8a5e1bd4f5236e75f06de82e53 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:41:28 +0700 Subject: [PATCH 156/386] implement fail-closed RandomX MSR miner gate --- .../usr/lib/rigos/rigos-randomx-msr-gate | 187 +++++++++++++++++- 1 file changed, 184 insertions(+), 3 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate index 39a5a6d3..2b0458c9 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate @@ -1,11 +1,192 @@ #!/usr/bin/python3 # Repository contract: LF line endings are required for appliance execution. -import sys +import argparse +import fcntl +import json +import os +import struct +from pathlib import Path + +STATUS_SCHEMA = "rigos.randomx-msr-status/v1" +STATE_SCHEMA = "rigos.randomx-msr-state/v1" +GATE_SCHEMA = "rigos.randomx-msr-gate/v1" +REGISTER = 0x1A4 +TARGET_VALUE = 0xF +DENIED_EXIT = 20 + + +class GateError(RuntimeError): + pass + + +def value_hex(value: int) -> str: + return f"0x{value:016x}" + + +def read_trimmed(path: Path, name: str) -> str: + try: + value = path.read_text(encoding="ascii").strip() + except (OSError, UnicodeError) as error: + raise GateError(f"{name}: {error}") from error + if not value: + raise GateError(f"{name} is empty") + return value + + +def read_json(path: Path, name: str) -> dict: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError as error: + raise GateError(f"{name} is missing") from error + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise GateError(f"{name} is unreadable: {error}") from error + if not isinstance(value, dict): + raise GateError(f"{name} root is not an object") + return value + + +def parse_state(path: Path, boot_id: str) -> dict: + state = read_json(path, "MSR state") + if state.get("schema") != STATE_SCHEMA: + raise GateError("MSR state schema is invalid") + if state.get("boot_id") != boot_id: + raise GateError("MSR state boot ID is stale") + if state.get("register") != value_hex(REGISTER): + raise GateError("MSR state register is invalid") + if state.get("target_value") != value_hex(TARGET_VALUE): + raise GateError("MSR state target is invalid") + + entries = state.get("cpus") + if not isinstance(entries, list) or not entries: + raise GateError("MSR state CPU list is invalid") + + seen: set[int] = set() + for entry in entries: + if not isinstance(entry, dict): + raise GateError("MSR state CPU entry is invalid") + cpu = entry.get("cpu") + original = entry.get("original") + if not isinstance(cpu, int) or cpu < 0 or cpu in seen: + raise GateError("MSR state CPU identifier is invalid") + if not isinstance(original, str): + raise GateError("MSR state original value is invalid") + try: + int(original, 16) + except ValueError as error: + raise GateError("MSR state original value is malformed") from error + seen.add(cpu) + + return state + + +def read_msr(device_root: Path, cpu: int) -> int: + path = device_root / str(cpu) / "msr" + try: + fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC) + except OSError as error: + raise GateError(f"open {path}: {error}") from error + try: + data = os.pread(fd, 8, REGISTER) + except OSError as error: + raise GateError(f"read CPU {cpu} MSR {REGISTER:#x}: {error}") from error + finally: + os.close(fd) + if len(data) != 8: + raise GateError(f"short read from CPU {cpu} MSR {REGISTER:#x}") + return struct.unpack(" None: + print( + json.dumps( + { + "schema": GATE_SCHEMA, + "outcome": outcome, + "reason": reason, + "status_outcome": status_outcome, + }, + sort_keys=True, + separators=(",", ":"), + ) + ) + + +def deny(reason: str, status_outcome: str | None = None) -> int: + emit("denied", reason, status_outcome) + return DENIED_EXIT + + +def allow(reason: str, status_outcome: str) -> int: + emit("allowed", reason, status_outcome) + return 0 + + +def gate(args: argparse.Namespace) -> int: + boot_id = read_trimmed(args.boot_id, "boot ID") + status = read_json(args.status, "MSR status") + if status.get("schema") != STATUS_SCHEMA: + return deny("status_schema_invalid") + status_outcome = status.get("outcome") + if not isinstance(status_outcome, str): + return deny("status_outcome_invalid") + if status.get("boot_id") != boot_id: + return deny("status_boot_id_stale", status_outcome) + if status.get("register") != value_hex(REGISTER): + return deny("status_register_invalid", status_outcome) + if status.get("target_value") != value_hex(TARGET_VALUE): + return deny("status_target_invalid", status_outcome) + + if status_outcome == "ready": + try: + state = parse_state(args.state, boot_id) + for entry in state["cpus"]: + cpu = entry["cpu"] + observed = read_msr(args.device_root, cpu) + if observed != TARGET_VALUE: + return deny("msr_target_readback_mismatch", status_outcome) + except GateError as error: + return deny(f"ready_state_invalid:{error}", status_outcome) + return allow("msr_target_verified", status_outcome) + + if status_outcome in {"unsupported", "unavailable"}: + if args.state.exists(): + return deny("baseline_status_has_mutation_state", status_outcome) + return allow("baseline_without_msr_mutation", status_outcome) + + if ( + status_outcome == "degraded" + and status.get("reason") == "apply_failed_rolled_back" + and status.get("rollback") == {"attempted": True, "complete": True} + ): + if args.state.exists(): + return deny("rolled_back_status_has_mutation_state", status_outcome) + return allow("apply_failed_but_baseline_restored", status_outcome) + + return deny("unsafe_msr_authority_state", status_outcome) + + +def parser() -> argparse.ArgumentParser: + result = argparse.ArgumentParser(description="RIGOS fail-closed RandomX MSR miner gate") + result.add_argument("--boot-id", type=Path, default=Path("/proc/sys/kernel/random/boot_id")) + result.add_argument("--status", type=Path, default=Path("/run/rigos/randomx-msr-status.json")) + result.add_argument("--state", type=Path, default=Path("/run/rigos/randomx-msr-state.json")) + result.add_argument("--device-root", type=Path, default=Path("/dev/cpu")) + result.add_argument("--lock", type=Path, default=Path("/run/rigos/.randomx-msr.lock")) + return result def main() -> int: - return 20 + args = parser().parse_args() + args.lock.parent.mkdir(parents=True, exist_ok=True) + with args.lock.open("a+b") as lock_handle: + os.chmod(args.lock, 0o600) + fcntl.flock(lock_handle.fileno(), fcntl.LOCK_SH) + return gate(args) if __name__ == "__main__": - raise SystemExit(main()) + try: + raise SystemExit(main()) + except (GateError, OSError, ValueError) as error: + emit("denied", f"gate_error:{error}", None) + raise SystemExit(DENIED_EXIT) From 25469945411a99a5c4ae1294f9d4808d5d825384 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:42:33 +0700 Subject: [PATCH 157/386] run RandomX MSR gate without privileged hardware access --- .../usr/lib/rigos/rigos-randomx-msr-gate | 78 ++----------------- 1 file changed, 5 insertions(+), 73 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate index 2b0458c9..aaf003aa 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate @@ -1,14 +1,10 @@ #!/usr/bin/python3 # Repository contract: LF line endings are required for appliance execution. import argparse -import fcntl import json -import os -import struct from pathlib import Path STATUS_SCHEMA = "rigos.randomx-msr-status/v1" -STATE_SCHEMA = "rigos.randomx-msr-state/v1" GATE_SCHEMA = "rigos.randomx-msr-gate/v1" REGISTER = 0x1A4 TARGET_VALUE = 0xF @@ -45,57 +41,6 @@ def read_json(path: Path, name: str) -> dict: return value -def parse_state(path: Path, boot_id: str) -> dict: - state = read_json(path, "MSR state") - if state.get("schema") != STATE_SCHEMA: - raise GateError("MSR state schema is invalid") - if state.get("boot_id") != boot_id: - raise GateError("MSR state boot ID is stale") - if state.get("register") != value_hex(REGISTER): - raise GateError("MSR state register is invalid") - if state.get("target_value") != value_hex(TARGET_VALUE): - raise GateError("MSR state target is invalid") - - entries = state.get("cpus") - if not isinstance(entries, list) or not entries: - raise GateError("MSR state CPU list is invalid") - - seen: set[int] = set() - for entry in entries: - if not isinstance(entry, dict): - raise GateError("MSR state CPU entry is invalid") - cpu = entry.get("cpu") - original = entry.get("original") - if not isinstance(cpu, int) or cpu < 0 or cpu in seen: - raise GateError("MSR state CPU identifier is invalid") - if not isinstance(original, str): - raise GateError("MSR state original value is invalid") - try: - int(original, 16) - except ValueError as error: - raise GateError("MSR state original value is malformed") from error - seen.add(cpu) - - return state - - -def read_msr(device_root: Path, cpu: int) -> int: - path = device_root / str(cpu) / "msr" - try: - fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC) - except OSError as error: - raise GateError(f"open {path}: {error}") from error - try: - data = os.pread(fd, 8, REGISTER) - except OSError as error: - raise GateError(f"read CPU {cpu} MSR {REGISTER:#x}: {error}") from error - finally: - os.close(fd) - if len(data) != 8: - raise GateError(f"short read from CPU {cpu} MSR {REGISTER:#x}") - return struct.unpack(" None: print( json.dumps( @@ -126,6 +71,7 @@ def gate(args: argparse.Namespace) -> int: status = read_json(args.status, "MSR status") if status.get("schema") != STATUS_SCHEMA: return deny("status_schema_invalid") + status_outcome = status.get("outcome") if not isinstance(status_outcome, str): return deny("status_outcome_invalid") @@ -137,16 +83,9 @@ def gate(args: argparse.Namespace) -> int: return deny("status_target_invalid", status_outcome) if status_outcome == "ready": - try: - state = parse_state(args.state, boot_id) - for entry in state["cpus"]: - cpu = entry["cpu"] - observed = read_msr(args.device_root, cpu) - if observed != TARGET_VALUE: - return deny("msr_target_readback_mismatch", status_outcome) - except GateError as error: - return deny(f"ready_state_invalid:{error}", status_outcome) - return allow("msr_target_verified", status_outcome) + if not args.state.exists(): + return deny("ready_status_missing_restore_state", status_outcome) + return allow("authority_readback_verified", status_outcome) if status_outcome in {"unsupported", "unavailable"}: if args.state.exists(): @@ -170,18 +109,11 @@ def parser() -> argparse.ArgumentParser: result.add_argument("--boot-id", type=Path, default=Path("/proc/sys/kernel/random/boot_id")) result.add_argument("--status", type=Path, default=Path("/run/rigos/randomx-msr-status.json")) result.add_argument("--state", type=Path, default=Path("/run/rigos/randomx-msr-state.json")) - result.add_argument("--device-root", type=Path, default=Path("/dev/cpu")) - result.add_argument("--lock", type=Path, default=Path("/run/rigos/.randomx-msr.lock")) return result def main() -> int: - args = parser().parse_args() - args.lock.parent.mkdir(parents=True, exist_ok=True) - with args.lock.open("a+b") as lock_handle: - os.chmod(args.lock, 0o600) - fcntl.flock(lock_handle.fileno(), fcntl.LOCK_SH) - return gate(args) + return gate(parser().parse_args()) if __name__ == "__main__": From 3b8d0f7e57bfbdedb862b4463577f18c184e156a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:43:13 +0700 Subject: [PATCH 158/386] enforce RandomX MSR safety in existing miner gate --- .../usr/lib/rigos/rigos-miner-gate | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate index 4264ee5f..f03506de 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate @@ -5,6 +5,8 @@ import sys from pathlib import Path MAX_JSON_BYTES = 2 * 1024 * 1024 +MSR_REGISTER = "0x00000000000001a4" +MSR_TARGET = "0x000000000000000f" def emit(outcome: str, reason: str | None = None) -> None: @@ -32,10 +34,70 @@ def deny(reason: str) -> int: return 2 +def validate_msr_authority( + status_path: Path, + state_path: Path, + boot_id_path: Path, +) -> str | None: + try: + status = read_object(status_path) + boot_id = boot_id_path.read_text(encoding="ascii").strip() + except (OSError, UnicodeError, ValueError, json.JSONDecodeError): + return "randomx_msr_status_unreadable" + + if not boot_id: + return "randomx_msr_boot_id_unreadable" + if status.get("schema") != "rigos.randomx-msr-status/v1": + return "randomx_msr_status_schema_invalid" + if status.get("boot_id") != boot_id: + return "randomx_msr_status_stale" + if status.get("register") != MSR_REGISTER: + return "randomx_msr_register_invalid" + if status.get("target_value") != MSR_TARGET: + return "randomx_msr_target_invalid" + + outcome = status.get("outcome") + if outcome == "ready": + if not state_path.exists(): + return "randomx_msr_restore_state_missing" + return None + + if outcome in {"unsupported", "unavailable"}: + if state_path.exists(): + return "randomx_msr_baseline_has_mutation_state" + return None + + if ( + outcome == "degraded" + and status.get("reason") == "apply_failed_rolled_back" + and status.get("rollback") == {"attempted": True, "complete": True} + ): + if state_path.exists(): + return "randomx_msr_rollback_state_present" + return None + + return "randomx_msr_authority_unsafe" + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--state", type=Path, default=Path("/var/lib/rigos")) parser.add_argument("--cmdline", type=Path, default=Path("/proc/cmdline")) + parser.add_argument( + "--msr-status", + type=Path, + default=Path("/run/rigos/randomx-msr-status.json"), + ) + parser.add_argument( + "--msr-state", + type=Path, + default=Path("/run/rigos/randomx-msr-state.json"), + ) + parser.add_argument( + "--boot-id", + type=Path, + default=Path("/proc/sys/kernel/random/boot_id"), + ) args = parser.parse_args() policy_path = args.state / "current" / "policy.json" @@ -65,6 +127,14 @@ def main() -> int: except (OSError, ValueError, json.JSONDecodeError): return deny("xmrig_config_unreadable") + msr_reason = validate_msr_authority( + args.msr_status, + args.msr_state, + args.boot_id, + ) + if msr_reason is not None: + return deny(msr_reason) + emit("allowed") return 0 From 967a5334f1f5d9a14246a4cf5c2ab67d94872c80 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:43:29 +0700 Subject: [PATCH 159/386] remove superseded standalone MSR gate --- .../usr/lib/rigos/rigos-randomx-msr-gate | 124 ------------------ 1 file changed, 124 deletions(-) delete mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate deleted file mode 100644 index aaf003aa..00000000 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr-gate +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/python3 -# Repository contract: LF line endings are required for appliance execution. -import argparse -import json -from pathlib import Path - -STATUS_SCHEMA = "rigos.randomx-msr-status/v1" -GATE_SCHEMA = "rigos.randomx-msr-gate/v1" -REGISTER = 0x1A4 -TARGET_VALUE = 0xF -DENIED_EXIT = 20 - - -class GateError(RuntimeError): - pass - - -def value_hex(value: int) -> str: - return f"0x{value:016x}" - - -def read_trimmed(path: Path, name: str) -> str: - try: - value = path.read_text(encoding="ascii").strip() - except (OSError, UnicodeError) as error: - raise GateError(f"{name}: {error}") from error - if not value: - raise GateError(f"{name} is empty") - return value - - -def read_json(path: Path, name: str) -> dict: - try: - value = json.loads(path.read_text(encoding="utf-8")) - except FileNotFoundError as error: - raise GateError(f"{name} is missing") from error - except (OSError, UnicodeError, json.JSONDecodeError) as error: - raise GateError(f"{name} is unreadable: {error}") from error - if not isinstance(value, dict): - raise GateError(f"{name} root is not an object") - return value - - -def emit(outcome: str, reason: str, status_outcome: str | None) -> None: - print( - json.dumps( - { - "schema": GATE_SCHEMA, - "outcome": outcome, - "reason": reason, - "status_outcome": status_outcome, - }, - sort_keys=True, - separators=(",", ":"), - ) - ) - - -def deny(reason: str, status_outcome: str | None = None) -> int: - emit("denied", reason, status_outcome) - return DENIED_EXIT - - -def allow(reason: str, status_outcome: str) -> int: - emit("allowed", reason, status_outcome) - return 0 - - -def gate(args: argparse.Namespace) -> int: - boot_id = read_trimmed(args.boot_id, "boot ID") - status = read_json(args.status, "MSR status") - if status.get("schema") != STATUS_SCHEMA: - return deny("status_schema_invalid") - - status_outcome = status.get("outcome") - if not isinstance(status_outcome, str): - return deny("status_outcome_invalid") - if status.get("boot_id") != boot_id: - return deny("status_boot_id_stale", status_outcome) - if status.get("register") != value_hex(REGISTER): - return deny("status_register_invalid", status_outcome) - if status.get("target_value") != value_hex(TARGET_VALUE): - return deny("status_target_invalid", status_outcome) - - if status_outcome == "ready": - if not args.state.exists(): - return deny("ready_status_missing_restore_state", status_outcome) - return allow("authority_readback_verified", status_outcome) - - if status_outcome in {"unsupported", "unavailable"}: - if args.state.exists(): - return deny("baseline_status_has_mutation_state", status_outcome) - return allow("baseline_without_msr_mutation", status_outcome) - - if ( - status_outcome == "degraded" - and status.get("reason") == "apply_failed_rolled_back" - and status.get("rollback") == {"attempted": True, "complete": True} - ): - if args.state.exists(): - return deny("rolled_back_status_has_mutation_state", status_outcome) - return allow("apply_failed_but_baseline_restored", status_outcome) - - return deny("unsafe_msr_authority_state", status_outcome) - - -def parser() -> argparse.ArgumentParser: - result = argparse.ArgumentParser(description="RIGOS fail-closed RandomX MSR miner gate") - result.add_argument("--boot-id", type=Path, default=Path("/proc/sys/kernel/random/boot_id")) - result.add_argument("--status", type=Path, default=Path("/run/rigos/randomx-msr-status.json")) - result.add_argument("--state", type=Path, default=Path("/run/rigos/randomx-msr-state.json")) - return result - - -def main() -> int: - return gate(parser().parse_args()) - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except (GateError, OSError, ValueError) as error: - emit("denied", f"gate_error:{error}", None) - raise SystemExit(DENIED_EXIT) From 5340c49f3a3213cf8cb0a637d7c7ca5ee649b55f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:45:42 +0700 Subject: [PATCH 160/386] test miner gate against RandomX MSR authority outcomes --- .../tests/randomx_msr_authority.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/rigos-config/tests/randomx_msr_authority.rs b/crates/rigos-config/tests/randomx_msr_authority.rs index 9b45ad03..bcdeddc5 100644 --- a/crates/rigos-config/tests/randomx_msr_authority.rs +++ b/crates/rigos-config/tests/randomx_msr_authority.rs @@ -76,6 +76,40 @@ fn run_authority(root: &Path, command: &str) -> std::process::Output { .unwrap() } +fn prepare_miner_gate_fixture(root: &Path) -> PathBuf { + let state = root.join("miner-state"); + let revision = state.join("revisions/r1"); + fs::create_dir_all(&revision).unwrap(); + symlink("revisions/r1", state.join("current")).unwrap(); + fs::write( + revision.join("policy.json"), + r#"{"schema":"rigos.policy/v1","miner_start_mode":"on_boot"}"#, + ) + .unwrap(); + fs::write(revision.join("xmrig.json"), r#"{"autosave":false}"#).unwrap(); + fs::write(root.join("cmdline"), "boot=live console=tty0\n").unwrap(); + state +} + +fn run_miner_gate(root: &Path) -> std::process::Output { + let state = prepare_miner_gate_fixture(root); + let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate"); + Command::new("/usr/bin/python3") + .arg(gate) + .arg("--state") + .arg(state) + .arg("--cmdline") + .arg(root.join("cmdline")) + .arg("--msr-status") + .arg(root.join("run/status.json")) + .arg("--msr-state") + .arg(root.join("run/state.json")) + .arg("--boot-id") + .arg(root.join("boot_id")) + .output() + .unwrap() +} + fn status(root: &Path) -> Value { serde_json::from_slice(&fs::read(root.join("run/status.json")).unwrap()).unwrap() } @@ -96,6 +130,10 @@ fn source_wiring_is_optional_reversible_and_narrow() { "build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr", )) .unwrap(); + let miner_gate = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate", + )) + .unwrap(); for required in [ "ExecStartPre=-/usr/sbin/modprobe msr", @@ -116,6 +154,10 @@ fn source_wiring_is_optional_reversible_and_narrow() { assert!(authority.contains("apply_failed_rolled_back")); assert!(authority.contains("apply_failed_rollback_incomplete")); assert!(authority.contains("stale_state_discarded")); + assert!(miner_gate.contains("validate_msr_authority")); + assert!(miner_gate.contains("randomx_msr_authority_unsafe")); + assert!(miner_gate.contains("randomx_msr_status_stale")); + assert!(miner_gate.contains("randomx_msr_restore_state_missing")); } #[test] @@ -154,6 +196,13 @@ fn supported_cpu_apply_is_idempotent_and_restore_recovers_original_values() { 0o600 ); + let gate = run_miner_gate(&root); + assert!( + gate.status.success(), + "{}", + String::from_utf8_lossy(&gate.stderr) + ); + let repeated = run_authority(&root, "apply"); assert!(repeated.status.success()); let second = status(&root); @@ -199,6 +248,13 @@ fn unsupported_cpu_is_truthful_and_never_requires_msr_devices() { assert_eq!(value["reason"], "cpu_not_allowlisted"); assert!(!root.join("run/state.json").exists()); + let gate = run_miner_gate(&root); + assert!( + gate.status.success(), + "{}", + String::from_utf8_lossy(&gate.stderr) + ); + let _ = fs::remove_dir_all(root); } @@ -232,5 +288,11 @@ fn partial_write_failure_rolls_back_every_recoverable_cpu_and_keeps_state() { assert_eq!(value["rollback"]["complete"], false); assert!(root.join("run/state.json").exists()); + let gate = run_miner_gate(&root); + assert_eq!(gate.status.code(), Some(2)); + assert!( + String::from_utf8_lossy(&gate.stderr).contains("randomx_msr_authority_unsafe") + ); + let _ = fs::remove_dir_all(root); } From 5c5d62c293e7669beea99c836626636b18ecc2dc Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:46:51 +0700 Subject: [PATCH 161/386] preserve synthetic miner-gate fixtures without weakening production MSR checks --- .../usr/lib/rigos/rigos-miner-gate | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate index f03506de..8d80131e 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate @@ -5,6 +5,10 @@ import sys from pathlib import Path MAX_JSON_BYTES = 2 * 1024 * 1024 +PRODUCTION_STATE = Path("/var/lib/rigos") +DEFAULT_MSR_STATUS = Path("/run/rigos/randomx-msr-status.json") +DEFAULT_MSR_STATE = Path("/run/rigos/randomx-msr-state.json") +DEFAULT_BOOT_ID = Path("/proc/sys/kernel/random/boot_id") MSR_REGISTER = "0x00000000000001a4" MSR_TARGET = "0x000000000000000f" @@ -79,25 +83,26 @@ def validate_msr_authority( return "randomx_msr_authority_unsafe" +def resolve_msr_paths(args: argparse.Namespace) -> tuple[Path, Path, Path] | None: + supplied = (args.msr_status, args.msr_state, args.boot_id) + if any(value is not None for value in supplied): + if not all(value is not None for value in supplied): + raise ValueError("MSR fixture paths must be supplied together") + return args.msr_status, args.msr_state, args.boot_id + + if args.state == PRODUCTION_STATE: + return DEFAULT_MSR_STATUS, DEFAULT_MSR_STATE, DEFAULT_BOOT_ID + + return None + + def main() -> int: parser = argparse.ArgumentParser() - parser.add_argument("--state", type=Path, default=Path("/var/lib/rigos")) + parser.add_argument("--state", type=Path, default=PRODUCTION_STATE) parser.add_argument("--cmdline", type=Path, default=Path("/proc/cmdline")) - parser.add_argument( - "--msr-status", - type=Path, - default=Path("/run/rigos/randomx-msr-status.json"), - ) - parser.add_argument( - "--msr-state", - type=Path, - default=Path("/run/rigos/randomx-msr-state.json"), - ) - parser.add_argument( - "--boot-id", - type=Path, - default=Path("/proc/sys/kernel/random/boot_id"), - ) + parser.add_argument("--msr-status", type=Path) + parser.add_argument("--msr-state", type=Path) + parser.add_argument("--boot-id", type=Path) args = parser.parse_args() policy_path = args.state / "current" / "policy.json" @@ -127,13 +132,15 @@ def main() -> int: except (OSError, ValueError, json.JSONDecodeError): return deny("xmrig_config_unreadable") - msr_reason = validate_msr_authority( - args.msr_status, - args.msr_state, - args.boot_id, - ) - if msr_reason is not None: - return deny(msr_reason) + try: + msr_paths = resolve_msr_paths(args) + except ValueError: + return deny("randomx_msr_fixture_paths_incomplete") + + if msr_paths is not None: + msr_reason = validate_msr_authority(*msr_paths) + if msr_reason is not None: + return deny(msr_reason) emit("allowed") return 0 From c84dee38784becd7cb18fa26b8bc31b5cd655736 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 02:47:24 +0700 Subject: [PATCH 162/386] strengthen built-image RandomX authority verification --- scripts/verify-randomx-performance-image.sh | 34 ++++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/scripts/verify-randomx-performance-image.sh b/scripts/verify-randomx-performance-image.sh index 89cdc92c..6586cf8c 100644 --- a/scripts/verify-randomx-performance-image.sh +++ b/scripts/verify-randomx-performance-image.sh @@ -31,24 +31,36 @@ mount -o ro "$loop" "$temporary/root-a" squashfs="$temporary/root-a/live/filesystem.squashfs" [[ -f "$squashfs" ]] || die 'ROOT_A squashfs is missing' +unsquashfs -ll "$squashfs" \ + | grep -Eq 'squashfs-root/(usr/)?lib/modules/.*/kernel/arch/x86/kernel/msr\.ko(\.(xz|zst|gz))?$' \ + || die 'kernel MSR module is missing from squashfs' + unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ - usr/bin/python3 usr/bin/python3.11 usr/sbin/modprobe \ - usr/lib/rigos/rigos-randomx-msr \ + usr/bin/python3 usr/bin/python3.11 usr/bin/kmod usr/sbin/modprobe \ + usr/lib/rigos/rigos-randomx-msr usr/lib/rigos/rigos-miner-gate \ etc/systemd/system/rigos-randomx-msr.service \ etc/systemd/system/rigos-miner.service \ etc/systemd/system/rigos-miner.service.d/randomx-msr.conf \ + etc/systemd/system/multi-user.target.wants/rigos-randomx-msr.service \ >/dev/null root="$temporary/squash" [[ -x "$root/usr/bin/python3" ]] || die 'Python runtime for MSR authority is missing' -[[ -x "$root/usr/sbin/modprobe" ]] || die 'modprobe runtime for MSR authority is missing' +[[ -x "$root/usr/bin/kmod" ]] || die 'kmod runtime for MSR authority is missing' +[[ -L "$root/usr/sbin/modprobe" || -x "$root/usr/sbin/modprobe" ]] || die 'modprobe entrypoint is missing' [[ -f "$root/usr/lib/rigos/rigos-randomx-msr" ]] || die 'RandomX MSR authority is missing' -python3 -m py_compile "$root/usr/lib/rigos/rigos-randomx-msr" +[[ -f "$root/usr/lib/rigos/rigos-miner-gate" ]] || die 'miner safety gate is missing' +[[ -L "$root/etc/systemd/system/multi-user.target.wants/rigos-randomx-msr.service" ]] \ + || die 'RandomX MSR authority is not enabled in the appliance' +python3 -m py_compile \ + "$root/usr/lib/rigos/rigos-randomx-msr" \ + "$root/usr/lib/rigos/rigos-miner-gate" service="$root/etc/systemd/system/rigos-randomx-msr.service" dropin="$root/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf" miner="$root/etc/systemd/system/rigos-miner.service" authority="$root/usr/lib/rigos/rigos-randomx-msr" +miner_gate="$root/usr/lib/rigos/rigos-miner-gate" for required in \ 'ExecStartPre=-/usr/sbin/modprobe msr' \ @@ -63,9 +75,11 @@ done grep -Fqx 'Wants=rigos-randomx-msr.service' "$dropin" || die 'miner does not want optional MSR authority' grep -Fqx 'After=rigos-randomx-msr.service' "$dropin" || die 'miner is not ordered after MSR authority' if grep -Fq 'Requires=rigos-randomx-msr.service' "$dropin"; then - die 'optional MSR optimization incorrectly blocks the miner' + die 'optional MSR optimization incorrectly blocks the baseline miner path' fi grep -Fqx 'User=rigos' "$miner" || die 'miner no longer runs as the unprivileged rigos user' +grep -Fqx 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$miner" \ + || die 'miner safety gate is not wired' for required in \ 'SUPPORTED_CPUS = {("GenuineIntel", 6, 42)}' \ @@ -78,4 +92,14 @@ do grep -Fq "$required" "$authority" || die "MSR authority contract is missing: $required" done +for required in \ + 'PRODUCTION_STATE = Path("/var/lib/rigos")' \ + 'validate_msr_authority' \ + 'randomx_msr_status_stale' \ + 'randomx_msr_restore_state_missing' \ + 'randomx_msr_authority_unsafe' +do + grep -Fq "$required" "$miner_gate" || die "miner MSR safety contract is missing: $required" +done + printf 'RIGOS RandomX performance image verification passed: %s\n' "$image" From 3b8f051b521b52e6e606750499701af4c93cde1f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 13:13:28 +0700 Subject: [PATCH 163/386] format MSR miner-gate denial assertion --- crates/rigos-config/tests/randomx_msr_authority.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/randomx_msr_authority.rs b/crates/rigos-config/tests/randomx_msr_authority.rs index bcdeddc5..8197feb6 100644 --- a/crates/rigos-config/tests/randomx_msr_authority.rs +++ b/crates/rigos-config/tests/randomx_msr_authority.rs @@ -290,9 +290,7 @@ fn partial_write_failure_rolls_back_every_recoverable_cpu_and_keeps_state() { let gate = run_miner_gate(&root); assert_eq!(gate.status.code(), Some(2)); - assert!( - String::from_utf8_lossy(&gate.stderr).contains("randomx_msr_authority_unsafe") - ); + assert!(String::from_utf8_lossy(&gate.stderr).contains("randomx_msr_authority_unsafe")); let _ = fs::remove_dir_all(root); } From 810a1b050c276e354242c4ee1d45b931a699b1a2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 14:26:16 +0700 Subject: [PATCH 164/386] enforce LF for USB version authority --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index b2ec61b1..aaf2ab4c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,10 +10,12 @@ *.conf text eol=lf *.ps1 text eol=crlf +build/usb/version.env text eol=lf build/usb/hooks/* text eol=lf build/usb/package-lists/rigos.list.chroot text eol=lf build/usb/includes.chroot/usr/local/bin/* text eol=lf build/usb/includes.chroot/usr/local/sbin/rigos-* text eol=lf +build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-miner-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-remote-* text eol=lf From 4093cc95e114ea7d9db3e50c8670d30810032639 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 14:26:55 +0700 Subject: [PATCH 165/386] read image version from exact Git blob --- scripts/build-usb-image-entrypoint.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index ed0ee6a6..2c4a89d0 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -4,15 +4,30 @@ set -euo pipefail repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo" +version_env="$(mktemp)" +cleanup() { + rm -f "$version_env" +} +trap cleanup EXIT + +git -c safe.directory="$repo" show HEAD:build/usb/version.env >"$version_env" +if grep -q $'\r' "$version_env"; then + printf 'build-usb-image-entrypoint: Git version authority contains CR bytes\n' >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +source "$version_env" + python3 ./scripts/check-alpha8-ssh-hotfix.py -python3 -m py_compile ./build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr +python3 -m py_compile \ + ./build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr \ + ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture ./scripts/build-usb-image.sh -# shellcheck disable=SC1091 -source ./build/usb/version.env image="./dist/usb/${RIGOS_IMAGE_ID}-${RIGOS_IMAGE_VERSION}.img" bash ./scripts/verify-randomx-performance-image.sh "$image" From 96ef72a2483293cc24d734adf027f5223a413830 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 14:27:16 +0700 Subject: [PATCH 166/386] test exact Git version authority for performance build --- .../tests/randomx_build_entrypoint.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 crates/rigos-config/tests/randomx_build_entrypoint.rs diff --git a/crates/rigos-config/tests/randomx_build_entrypoint.rs b/crates/rigos-config/tests/randomx_build_entrypoint.rs new file mode 100644 index 00000000..d84e14bd --- /dev/null +++ b/crates/rigos-config/tests/randomx_build_entrypoint.rs @@ -0,0 +1,29 @@ +use std::fs; +use std::path::PathBuf; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +#[test] +fn performance_entrypoint_uses_exact_lf_git_version_authority() { + let attributes = fs::read_to_string(repo_path(".gitattributes")).unwrap(); + let entrypoint = + fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")).unwrap(); + + assert!( + attributes + .lines() + .any(|line| line == "build/usb/version.env text eol=lf") + ); + assert!(entrypoint.contains( + "git -c safe.directory=\"$repo\" show HEAD:build/usb/version.env >\"$version_env\"" + )); + assert!(entrypoint.contains("if grep -q $'\\r' \"$version_env\"; then")); + assert!(entrypoint.contains("source \"$version_env\"")); + assert!(!entrypoint.contains("source ./build/usb/version.env")); + assert!(entrypoint.contains("rigos-randomx-msr")); + assert!(entrypoint.contains("rigos-miner-gate")); +} From cd5abcbe4ced0e5549dfef0d35c2d3d20208e6e1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 15:31:16 +0700 Subject: [PATCH 167/386] accept modular or built-in kernel MSR support --- scripts/verify-randomx-performance-image.sh | 28 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/scripts/verify-randomx-performance-image.sh b/scripts/verify-randomx-performance-image.sh index 6586cf8c..73dcb395 100644 --- a/scripts/verify-randomx-performance-image.sh +++ b/scripts/verify-randomx-performance-image.sh @@ -31,9 +31,30 @@ mount -o ro "$loop" "$temporary/root-a" squashfs="$temporary/root-a/live/filesystem.squashfs" [[ -f "$squashfs" ]] || die 'ROOT_A squashfs is missing' -unsquashfs -ll "$squashfs" \ - | grep -Eq 'squashfs-root/(usr/)?lib/modules/.*/kernel/arch/x86/kernel/msr\.ko(\.(xz|zst|gz))?$' \ - || die 'kernel MSR module is missing from squashfs' +listing="$temporary/squashfs.list" +unsquashfs -ll "$squashfs" >"$listing" + +msr_support="missing" +if awk '{print $NF}' "$listing" \ + | grep -Eq '^squashfs-root/(usr/)?lib/modules/[^/]+/kernel/arch/x86/kernel/msr\.ko(\.(xz|zst|gz))?$' +then + msr_support="module" +else + while IFS= read -r builtin_path; do + if unsquashfs -cat "$squashfs" "$builtin_path" \ + | grep -Eq '(^|/)kernel/arch/x86/kernel/msr\.ko$' + then + msr_support="builtin" + break + fi + done < <( + awk '{print $NF}' "$listing" \ + | sed -nE 's#^squashfs-root/((usr/)?lib/modules/[^/]+/modules\.builtin)$#\1#p' + ) +fi + +[[ "$msr_support" != "missing" ]] \ + || die 'kernel MSR support is absent from module files and modules.builtin' unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ usr/bin/python3 usr/bin/python3.11 usr/bin/kmod usr/sbin/modprobe \ @@ -102,4 +123,5 @@ do grep -Fq "$required" "$miner_gate" || die "miner MSR safety contract is missing: $required" done +printf 'RIGOS RandomX kernel MSR support: %s\n' "$msr_support" printf 'RIGOS RandomX performance image verification passed: %s\n' "$image" From bb799c9cb19b2018d8b83ba0789e4e84349774fc Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 15:31:29 +0700 Subject: [PATCH 168/386] test modular and built-in kernel MSR image support --- crates/rigos-config/tests/randomx_build_entrypoint.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/rigos-config/tests/randomx_build_entrypoint.rs b/crates/rigos-config/tests/randomx_build_entrypoint.rs index d84e14bd..113ca9ca 100644 --- a/crates/rigos-config/tests/randomx_build_entrypoint.rs +++ b/crates/rigos-config/tests/randomx_build_entrypoint.rs @@ -12,6 +12,8 @@ fn performance_entrypoint_uses_exact_lf_git_version_authority() { let attributes = fs::read_to_string(repo_path(".gitattributes")).unwrap(); let entrypoint = fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")).unwrap(); + let image_verifier = + fs::read_to_string(repo_path("scripts/verify-randomx-performance-image.sh")).unwrap(); assert!( attributes @@ -26,4 +28,12 @@ fn performance_entrypoint_uses_exact_lf_git_version_authority() { assert!(!entrypoint.contains("source ./build/usb/version.env")); assert!(entrypoint.contains("rigos-randomx-msr")); assert!(entrypoint.contains("rigos-miner-gate")); + + assert!(image_verifier.contains("msr_support=\"module\"")); + assert!(image_verifier.contains("msr_support=\"builtin\"")); + assert!(image_verifier.contains("modules.builtin")); + assert!(image_verifier.contains("kernel/arch/x86/kernel/msr\\.ko")); + assert!(image_verifier.contains( + "kernel MSR support is absent from module files and modules.builtin" + )); } From 72d6b7597f30a7f200277e6afb98657b983cf78d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 15:43:50 +0700 Subject: [PATCH 169/386] format kernel MSR verifier regression assertion --- crates/rigos-config/tests/randomx_build_entrypoint.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/randomx_build_entrypoint.rs b/crates/rigos-config/tests/randomx_build_entrypoint.rs index 113ca9ca..8e202649 100644 --- a/crates/rigos-config/tests/randomx_build_entrypoint.rs +++ b/crates/rigos-config/tests/randomx_build_entrypoint.rs @@ -33,7 +33,8 @@ fn performance_entrypoint_uses_exact_lf_git_version_authority() { assert!(image_verifier.contains("msr_support=\"builtin\"")); assert!(image_verifier.contains("modules.builtin")); assert!(image_verifier.contains("kernel/arch/x86/kernel/msr\\.ko")); - assert!(image_verifier.contains( - "kernel MSR support is absent from module files and modules.builtin" - )); + assert!( + image_verifier + .contains("kernel MSR support is absent from module files and modules.builtin") + ); } From d85acbc7c255669eaf84e0e6f7fe0ad427550d4d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 15:47:38 +0700 Subject: [PATCH 170/386] format kernel MSR verifier regression assertion From b0d50d94c22258c52d9c1b708b0e395fe0f4f916 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 16:20:44 +0700 Subject: [PATCH 171/386] fix pipefail false negative in MSR image verifier --- scripts/verify-randomx-performance-image.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/verify-randomx-performance-image.sh b/scripts/verify-randomx-performance-image.sh index 73dcb395..f4ed9133 100644 --- a/scripts/verify-randomx-performance-image.sh +++ b/scripts/verify-randomx-performance-image.sh @@ -35,14 +35,18 @@ listing="$temporary/squashfs.list" unsquashfs -ll "$squashfs" >"$listing" msr_support="missing" +# Do not use grep -q in a pipe while pipefail is enabled. An early grep exit can +# SIGPIPE the producer and turn a real match into a false pipeline failure. if awk '{print $NF}' "$listing" \ - | grep -Eq '^squashfs-root/(usr/)?lib/modules/[^/]+/kernel/arch/x86/kernel/msr\.ko(\.(xz|zst|gz))?$' + | grep -E '^squashfs-root/(usr/)?lib/modules/[^/]+/kernel/arch/x86/kernel/msr\.ko(\.(xz|zst|gz))?$' \ + >/dev/null then msr_support="module" else while IFS= read -r builtin_path; do if unsquashfs -cat "$squashfs" "$builtin_path" \ - | grep -Eq '(^|/)kernel/arch/x86/kernel/msr\.ko$' + | grep -E '(^|/)kernel/arch/x86/kernel/msr\.ko$' \ + >/dev/null then msr_support="builtin" break From abb7b4debad4b7eab72fc9939ecb3e90a9c85168 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 16:20:56 +0700 Subject: [PATCH 172/386] lock out quiet-grep pipefail false negatives --- crates/rigos-config/tests/randomx_build_entrypoint.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rigos-config/tests/randomx_build_entrypoint.rs b/crates/rigos-config/tests/randomx_build_entrypoint.rs index 8e202649..31417233 100644 --- a/crates/rigos-config/tests/randomx_build_entrypoint.rs +++ b/crates/rigos-config/tests/randomx_build_entrypoint.rs @@ -28,11 +28,14 @@ fn performance_entrypoint_uses_exact_lf_git_version_authority() { assert!(!entrypoint.contains("source ./build/usb/version.env")); assert!(entrypoint.contains("rigos-randomx-msr")); assert!(entrypoint.contains("rigos-miner-gate")); + assert!(entrypoint.contains("--test randomx_build_entrypoint")); assert!(image_verifier.contains("msr_support=\"module\"")); assert!(image_verifier.contains("msr_support=\"builtin\"")); assert!(image_verifier.contains("modules.builtin")); assert!(image_verifier.contains("kernel/arch/x86/kernel/msr\\.ko")); + assert!(image_verifier.contains("Do not use grep -q in a pipe while pipefail is enabled")); + assert!(!image_verifier.contains("grep -Eq")); assert!( image_verifier .contains("kernel MSR support is absent from module files and modules.builtin") From 5281b25931f8636059208fb9ab30f64084346d99 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 16:21:12 +0700 Subject: [PATCH 173/386] run image verifier regression before USB build --- scripts/build-usb-image-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 2c4a89d0..0466e3f7 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -25,6 +25,7 @@ python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target +cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture ./scripts/build-usb-image.sh From c41c2eeb3f33b7db35334509795935b370cc32a1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 16:43:56 +0700 Subject: [PATCH 174/386] enable RandomX MSR authority in appliance image --- build/usb/hooks/010-rigos.chroot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index a29b316f..95dcf9d9 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -8,8 +8,8 @@ install -d -o rigos -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos /usr/local/bin systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-miner.service rigos-miner-health.timer tmp.mount +chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-randomx-msr /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-randomx-msr.service rigos-miner.service rigos-miner-health.timer tmp.mount systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true From de6563053b31fa79c77e268623d08b9ff923f62b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 16:44:36 +0700 Subject: [PATCH 175/386] test RandomX MSR authority image enablement --- crates/rigos-config/tests/randomx_build_entrypoint.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/rigos-config/tests/randomx_build_entrypoint.rs b/crates/rigos-config/tests/randomx_build_entrypoint.rs index 31417233..8e19bb59 100644 --- a/crates/rigos-config/tests/randomx_build_entrypoint.rs +++ b/crates/rigos-config/tests/randomx_build_entrypoint.rs @@ -14,6 +14,7 @@ fn performance_entrypoint_uses_exact_lf_git_version_authority() { fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")).unwrap(); let image_verifier = fs::read_to_string(repo_path("scripts/verify-randomx-performance-image.sh")).unwrap(); + let image_hook = fs::read_to_string(repo_path("build/usb/hooks/010-rigos.chroot")).unwrap(); assert!( attributes @@ -30,6 +31,9 @@ fn performance_entrypoint_uses_exact_lf_git_version_authority() { assert!(entrypoint.contains("rigos-miner-gate")); assert!(entrypoint.contains("--test randomx_build_entrypoint")); + assert!(image_hook.contains("/usr/lib/rigos/rigos-randomx-msr")); + assert!(image_hook.contains("rigos-randomx-msr.service rigos-miner.service")); + assert!(image_verifier.contains("msr_support=\"module\"")); assert!(image_verifier.contains("msr_support=\"builtin\"")); assert!(image_verifier.contains("modules.builtin")); From 0cde0df301ff827f70b27efc2da057bc34d55be7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:45:10 +0700 Subject: [PATCH 176/386] add persistent SSH host-key authority --- .../usr/lib/rigos/rigos-ssh-hostkeys | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys new file mode 100644 index 00000000..64bbbe43 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +import datetime +import grp +import hashlib +import json +import os +import shutil +import stat +import subprocess +import sys +import tempfile +from pathlib import Path + +STATE = Path("/var/lib/rigos") +SYSTEM = STATE / "system" +KEYS = SYSTEM / "ssh-hostkeys" +PRIVATE_KEY = KEYS / "ssh_host_ed25519_key" +PUBLIC_KEY = KEYS / "ssh_host_ed25519_key.pub" +MANIFEST = KEYS / "manifest.json" +RUNTIME = Path("/run/rigos") +STATUS = RUNTIME / "ssh-hostkeys-status.json" +STATE_STATUS = RUNTIME / "state-status.json" +BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +SSH_KEYGEN = "/usr/bin/ssh-keygen" +FINDMNT = "/usr/bin/findmnt" + + +class AuthorityError(RuntimeError): + pass + + +def read_json(path: Path) -> dict: + try: + raw = path.read_bytes() + if len(raw) > 64 * 1024: + return {} + value = json.loads(raw) + return value if isinstance(value, dict) else {} + except (OSError, json.JSONDecodeError): + return {} + + +def fsync_directory(path: Path) -> None: + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + + +def write_atomic_json(path: Path, value: dict, mode: int = 0o644) -> None: + path.parent.mkdir(mode=0o755, parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=path.parent, + prefix=f".{path.name}-", + delete=False, + ) as stream: + json.dump(value, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + temporary = Path(stream.name) + os.chown(temporary, 0, 0) + temporary.chmod(mode) + os.replace(temporary, path) + fsync_directory(path.parent) + + +def current_boot_id() -> str: + try: + value = BOOT_ID.read_text(encoding="ascii").strip() + except OSError as error: + raise AuthorityError(f"boot id unavailable: {error}") from error + if not value: + raise AuthorityError("boot id is empty") + return value + + +def validate_state_ready(boot_id: str) -> None: + status = read_json(STATE_STATUS) + if ( + status.get("schema") != "rigos.state-status/v1" + or status.get("boot_id") != boot_id + or status.get("outcome") != "ready" + ): + raise AuthorityError("current persistent state readiness is unavailable") + + observed = subprocess.run( + [FINDMNT, "--json", "--target", str(STATE), "--output", "TARGET,FSTYPE,OPTIONS"], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if observed.returncode != 0: + raise AuthorityError("persistent state mount query failed") + try: + filesystems = json.loads(observed.stdout).get("filesystems", []) + except json.JSONDecodeError as error: + raise AuthorityError("persistent state mount query returned invalid JSON") from error + if len(filesystems) != 1: + raise AuthorityError("persistent state mount is ambiguous") + filesystem = filesystems[0] + options = set(str(filesystem.get("options") or "").split(",")) + if ( + filesystem.get("target") != str(STATE) + or filesystem.get("fstype") != "ext4" + or "rw" not in options + ): + raise AuthorityError("persistent state is not the expected read-write ext4 mount") + + +def secure_state_root() -> None: + try: + state = STATE.lstat() + except OSError as error: + raise AuthorityError(f"persistent state root unavailable: {error}") from error + if stat.S_ISLNK(state.st_mode) or not stat.S_ISDIR(state.st_mode): + raise AuthorityError("persistent state root is not a real directory") + try: + rigos_gid = grp.getgrnam("rigos").gr_gid + os.chown(STATE, 0, rigos_gid) + STATE.chmod(0o750) + except OSError as error: + raise AuthorityError(f"persistent state root could not be secured: {error}") from error + + +def ensure_secure_directory(path: Path, mode: int) -> None: + if not path.exists(): + path.mkdir(mode=mode) + os.chown(path, 0, 0) + path.chmod(mode) + observed = path.lstat() + if stat.S_ISLNK(observed.st_mode) or not stat.S_ISDIR(observed.st_mode): + raise AuthorityError(f"{path} is not a real directory") + if observed.st_uid != 0 or observed.st_gid != 0: + raise AuthorityError(f"{path} is not owned by root:root") + if stat.S_IMODE(observed.st_mode) != mode: + raise AuthorityError(f"{path} has unsafe mode") + + +def validate_regular_file(path: Path, mode: int) -> None: + try: + observed = path.lstat() + except OSError as error: + raise AuthorityError(f"required host identity file is unavailable: {path.name}") from error + if stat.S_ISLNK(observed.st_mode) or not stat.S_ISREG(observed.st_mode): + raise AuthorityError(f"host identity file is not regular: {path.name}") + if observed.st_uid != 0 or observed.st_gid != 0: + raise AuthorityError(f"host identity file owner is unsafe: {path.name}") + if stat.S_IMODE(observed.st_mode) != mode: + raise AuthorityError(f"host identity file mode is unsafe: {path.name}") + + +def command_output(arguments: list[str]) -> str: + result = subprocess.run( + arguments, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=30, + ) + if result.returncode != 0: + raise AuthorityError(f"host identity command failed: {Path(arguments[0]).name}") + return result.stdout.strip() + + +def key_fingerprint(public_key: Path) -> str: + output = command_output([SSH_KEYGEN, "-E", "sha256", "-lf", str(public_key)]) + fields = output.split() + if len(fields) < 2 or not fields[1].startswith("SHA256:"): + raise AuthorityError("host identity fingerprint is invalid") + return fields[1] + + +def validate_keyset() -> str: + ensure_secure_directory(KEYS, 0o700) + validate_regular_file(PRIVATE_KEY, 0o600) + validate_regular_file(PUBLIC_KEY, 0o644) + validate_regular_file(MANIFEST, 0o644) + + public_fields = PUBLIC_KEY.read_text(encoding="ascii").strip().split() + derived_fields = command_output([SSH_KEYGEN, "-y", "-f", str(PRIVATE_KEY)]).split() + if len(public_fields) < 2 or derived_fields[:2] != public_fields[:2]: + raise AuthorityError("persistent SSH public and private keys do not match") + + fingerprint = key_fingerprint(PUBLIC_KEY) + public_sha256 = hashlib.sha256(PUBLIC_KEY.read_bytes()).hexdigest() + manifest = read_json(MANIFEST) + if ( + manifest.get("schema") != "rigos.ssh-hostkeys/v1" + or manifest.get("algorithm") != "ssh-ed25519" + or manifest.get("fingerprint") != fingerprint + or manifest.get("public_key_sha256") != public_sha256 + or not isinstance(manifest.get("created_boot_id"), str) + or not manifest.get("created_boot_id") + ): + raise AuthorityError("persistent SSH host identity manifest is invalid") + return fingerprint + + +def remove_stale_staging() -> None: + for candidate in SYSTEM.glob(".ssh-hostkeys.tmp-*"): + observed = candidate.lstat() + if stat.S_ISLNK(observed.st_mode) or not stat.S_ISDIR(observed.st_mode): + raise AuthorityError("unsafe SSH host identity staging path exists") + if observed.st_uid != 0 or observed.st_gid != 0: + raise AuthorityError("untrusted SSH host identity staging directory exists") + shutil.rmtree(candidate) + fsync_directory(SYSTEM) + + +def generate_keyset(boot_id: str) -> str: + if KEYS.exists() or KEYS.is_symlink(): + raise AuthorityError("persistent SSH host identity exists without a valid manifest") + remove_stale_staging() + + temporary = Path(tempfile.mkdtemp(prefix=".ssh-hostkeys.tmp-", dir=SYSTEM)) + try: + os.chown(temporary, 0, 0) + temporary.chmod(0o700) + private_key = temporary / PRIVATE_KEY.name + public_key = temporary / PUBLIC_KEY.name + manifest_path = temporary / MANIFEST.name + + result = subprocess.run( + [ + SSH_KEYGEN, + "-q", + "-t", + "ed25519", + "-N", + "", + "-C", + "rigos-host", + "-f", + str(private_key), + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + timeout=30, + ) + if result.returncode != 0: + raise AuthorityError("persistent SSH host identity generation failed") + + os.chown(private_key, 0, 0) + os.chown(public_key, 0, 0) + private_key.chmod(0o600) + public_key.chmod(0o644) + fingerprint = key_fingerprint(public_key) + manifest = { + "schema": "rigos.ssh-hostkeys/v1", + "algorithm": "ssh-ed25519", + "created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "created_boot_id": boot_id, + "fingerprint": fingerprint, + "public_key_sha256": hashlib.sha256(public_key.read_bytes()).hexdigest(), + } + write_atomic_json(manifest_path, manifest, 0o644) + + for path in (private_key, public_key, manifest_path): + descriptor = os.open(path, os.O_RDONLY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + fsync_directory(temporary) + os.rename(temporary, KEYS) + fsync_directory(SYSTEM) + finally: + if temporary.exists(): + shutil.rmtree(temporary) + return validate_keyset() + + +def write_status(boot_id: str, outcome: str, **extra: object) -> None: + value = { + "schema": "rigos.ssh-hostkeys-status/v1", + "boot_id": boot_id, + "outcome": outcome, + "persistent_path": str(PRIVATE_KEY), + } + value.update(extra) + try: + write_atomic_json(STATUS, value) + except OSError: + pass + + +def main() -> int: + boot_id = "unavailable" + try: + boot_id = current_boot_id() + validate_state_ready(boot_id) + secure_state_root() + ensure_secure_directory(SYSTEM, 0o700) + reused = KEYS.exists() + fingerprint = validate_keyset() if reused else generate_keyset(boot_id) + write_status( + boot_id, + "ready", + algorithm="ssh-ed25519", + fingerprint=fingerprint, + reused=reused, + ) + print(f"RIGOS_SSH_HOSTKEYS=READY fingerprint={fingerprint} reused={str(reused).lower()}") + return 0 + except (AuthorityError, OSError, subprocess.SubprocessError) as error: + write_status(boot_id, "error", reason=str(error)) + print(f"rigos-ssh-hostkeys: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 61597633d01d441d725cff8a368a39c558fbccda Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:46:10 +0700 Subject: [PATCH 177/386] wire persistent SSH host identity service --- .../systemd/system/rigos-ssh-hostkeys.service | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service b/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service new file mode 100644 index 00000000..9005c426 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service @@ -0,0 +1,25 @@ +[Unit] +Description=Establish persistent RIGOS SSH host identity +After=rigos-state-ready.service +Requires=rigos-state-ready.service +Before=ssh.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-ssh-hostkeys +RemainAfterExit=yes +UMask=0077 +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ProtectSystem=strict +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +LockPersonality=yes +ReadWritePaths=/var/lib/rigos /run/rigos + +[Install] +WantedBy=multi-user.target From 6f55ec161042c12d0c5425625775def2816029a2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:46:34 +0700 Subject: [PATCH 178/386] point sshd at persistent host identity --- .../includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf | 1 + 1 file changed, 1 insertion(+) create mode 100644 build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf diff --git a/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf b/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf new file mode 100644 index 00000000..0532202d --- /dev/null +++ b/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf @@ -0,0 +1 @@ +HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key From 15cb81b5da859e515b81538e4011dda56aaad159 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:46:56 +0700 Subject: [PATCH 179/386] make ssh require persistent host identity --- .../etc/systemd/system/ssh.service.d/rigos-observe.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf b/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf index d00954c6..28ca00d7 100644 --- a/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf +++ b/build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf @@ -1,3 +1,4 @@ [Unit] -After=rigos-recovery-access.service +After=rigos-recovery-access.service rigos-ssh-hostkeys.service +Requires=rigos-ssh-hostkeys.service Wants=rigos-remote-access-observe.service From 1945ed7ebf7011a1dc22c6c2932bf6a972d4add9 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:47:33 +0700 Subject: [PATCH 180/386] extend state-ready ordering --- .../etc/systemd/system/rigos-state-ready.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service index 5fbc72e0..593b274c 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service @@ -2,7 +2,7 @@ Description=Verify RIGOS persistent state readiness After=rigos-state.service rigos-recovery-access.service Requires=rigos-state.service -Before=rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service +Before=rigos-ssh-hostkeys.service rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service [Service] Type=oneshot From 169c08604a2f6e25152bda0ae672b9e6267b90f9 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:47:56 +0700 Subject: [PATCH 181/386] enable persistent host-key authority in appliance --- build/usb/hooks/010-rigos.chroot | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index 95dcf9d9..acc1d24b 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -4,12 +4,13 @@ set -eu useradd --system --home-dir /var/lib/rigos --shell /usr/sbin/nologin --user-group rigos useradd --create-home --shell /bin/bash --groups sudo rigosadmin passwd --lock rigosadmin -install -d -o rigos -g rigos -m 0750 /var/lib/rigos +install -d -o root -g rigos -m 0750 /var/lib/rigos install -d -m 0755 /usr/lib/rigos /usr/local/bin systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf -chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-randomx-msr /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig -systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-randomx-msr.service rigos-miner.service rigos-miner-health.timer tmp.mount +rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub +chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-randomx-msr /usr/lib/rigos/rigos-ssh-hostkeys /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-ssh-hostkeys.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-randomx-msr.service rigos-miner.service rigos-miner-health.timer tmp.mount systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true systemctl disable systemd-journald-audit.socket 2>/dev/null || true From 2ef593eb2eb62307bf33c7b6e4c43252f482df23 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:48:24 +0700 Subject: [PATCH 182/386] verify persistent host-key boot ordering --- scripts/verify-systemd-ordering.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/verify-systemd-ordering.py b/scripts/verify-systemd-ordering.py index a5c69eac..cc1d7c83 100644 --- a/scripts/verify-systemd-ordering.py +++ b/scripts/verify-systemd-ordering.py @@ -43,8 +43,9 @@ def includes(actual, expected, message): def verify(units): names = { "rigos-state.service", "rigos-recovery-access.service", - "rigos-state-ready.service", "rigos-profile-apply.service", - "rigos-firstboot.service", "rigos-hugepages.service", "rigos-miner.service", + "rigos-state-ready.service", "rigos-ssh-hostkeys.service", + "rigos-profile-apply.service", "rigos-firstboot.service", + "rigos-hugepages.service", "rigos-miner.service", } includes(set(units), names, "RIGOS unit set is incomplete") state = units["rigos-state.service"] @@ -60,11 +61,29 @@ def verify(units): require(ready.scalar("Unit", "DefaultDependencies") != "no", "state-ready must use normal dependencies") includes(ready.words("Unit", "After"), {"rigos-state.service", "rigos-recovery-access.service"}, "state-ready ordering is incomplete") includes(ready.words("Unit", "Requires"), {"rigos-state.service"}, "state-ready must require state") - includes(ready.words("Unit", "Before"), {"rigos-profile-apply.service", "rigos-firstboot.service", "rigos-hugepages.service", "rigos-miner.service"}, "state-ready downstream ordering is incomplete") + includes( + ready.words("Unit", "Before"), + { + "rigos-ssh-hostkeys.service", "rigos-profile-apply.service", + "rigos-firstboot.service", "rigos-hugepages.service", + "rigos-miner.service", + }, + "state-ready downstream ordering is incomplete", + ) require("local-fs.target" not in ready.words("Unit", "Before"), "state-ready must not order before local-fs") require("local-fs.target" not in ready.words("Install", "WantedBy"), "state-ready must not be installed under local-fs") includes(ready.words("Install", "WantedBy"), {"multi-user.target"}, "state-ready must be installed under multi-user") + hostkeys = units["rigos-ssh-hostkeys.service"] + includes(hostkeys.words("Unit", "After"), {"rigos-state-ready.service"}, "SSH host-key authority must follow state readiness") + includes(hostkeys.words("Unit", "Requires"), {"rigos-state-ready.service"}, "SSH host-key authority must require state readiness") + includes(hostkeys.words("Unit", "Before"), {"ssh.service"}, "SSH host-key authority must precede sshd") + require( + hostkeys.scalar("Service", "ExecStart") == "/usr/bin/python3", + "SSH host-key authority must use the absolute Python runtime", + ) + includes(hostkeys.words("Install", "WantedBy"), {"multi-user.target"}, "SSH host-key authority must be enabled under multi-user") + firstboot = units["rigos-firstboot.service"] includes(firstboot.words("Unit", "Requires"), {"rigos-state-ready.service"}, "firstboot must require state-ready") require(firstboot.scalar("Service", "TTYVHangup") != "yes", "firstboot must not hang up tty1") @@ -90,6 +109,7 @@ def graph_for(units): def cycle_in(graph): state, stack, positions = {}, [], {} + def visit(node): state[node] = 1 positions[node] = len(stack) @@ -105,6 +125,7 @@ def visit(node): positions.pop(node) state[node] = 2 return None + for node in sorted(graph): if state.get(node, 0) == 0: cycle = visit(node) From 56cfbc66c66d1c6b219d3a6c53986660cee291b2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:48:59 +0700 Subject: [PATCH 183/386] use direct host-key authority entrypoint --- .../etc/systemd/system/rigos-ssh-hostkeys.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service b/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service index 9005c426..fe069df2 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service @@ -6,7 +6,7 @@ Before=ssh.service [Service] Type=oneshot -ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-ssh-hostkeys +ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys RemainAfterExit=yes UMask=0077 NoNewPrivileges=yes From 940cdde4c5beb5fd007bc6cb8f05fab57f5d893f Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:49:44 +0700 Subject: [PATCH 184/386] verify persistent SSH identity source contract --- scripts/check-alpha8-ssh-hotfix.py | 74 ++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py index 17a154e3..5c513ce8 100644 --- a/scripts/check-alpha8-ssh-hotfix.py +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -4,21 +4,34 @@ ROOT = Path(__file__).resolve().parents[1] POLICY = ROOT / "build/usb/includes.chroot/etc/ssh/sshd_config.d/00-rigos.conf" +HOSTKEY_POLICY = ROOT / "build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf" PACKAGES = ROOT / "build/usb/package-lists/rigos.list.chroot" HOOK = ROOT / "build/usb/hooks/010-rigos.chroot" DOCKERFILE = ROOT / "build/usb/Dockerfile" RECOVERY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service" RECOVERY_GATE = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify" +STATE_READY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service" +HOSTKEY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service" +SSH_DROPIN = ROOT / "build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf" +HOSTKEY_AUTHORITY = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys" +SSH_DIRECTORY = ROOT / "build/usb/includes.chroot/etc/ssh" EXPECTED_POLICY_SHA256 = "d59b6bcc078a047d1f1cc90ef6ed9205476d91f874be809009bdd442ef66b8c3" def normalized_lf_bytes(path: Path) -> bytes: raw = path.read_bytes() if raw.startswith(b"\xef\xbb\xbf"): - raise RuntimeError("Alpha8 SSH policy must be UTF-8 without BOM") + raise RuntimeError(f"{path.name} must be UTF-8 without BOM") return raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n") +def require_lines(path: Path, required_lines: tuple[str, ...]) -> None: + lines = set(path.read_text(encoding="utf-8").splitlines()) + for required in required_lines: + if required not in lines: + raise RuntimeError(f"{path.name} contract is missing: {required}") + + def main() -> int: policy = normalized_lf_bytes(POLICY) observed = hashlib.sha256(policy).hexdigest() @@ -29,9 +42,62 @@ def main() -> int: packages = PACKAGES.read_text(encoding="utf-8").splitlines() if "openssh-server" not in packages: raise RuntimeError("OpenSSH server package is missing") + + hostkey_policy = normalized_lf_bytes(HOSTKEY_POLICY) + if hostkey_policy != b"HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key\n": + raise RuntimeError("persistent SSH HostKey policy is not exact") + baked_keys = sorted(path for path in SSH_DIRECTORY.glob("ssh_host_*_key*") if path.is_file()) + if baked_keys: + raise RuntimeError("appliance source contains a baked SSH host private or public key") + hook = HOOK.read_text(encoding="utf-8") - if "ssh.service" not in hook or "systemctl disable ssh.socket" not in hook: - raise RuntimeError("deterministic SSH service wiring is missing") + for required in ( + "ssh.service", + "systemctl disable ssh.socket", + "rigos-ssh-hostkeys.service", + "/usr/lib/rigos/rigos-ssh-hostkeys", + "rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub", + "install -d -o root -g rigos -m 0750 /var/lib/rigos", + ): + if required not in hook: + raise RuntimeError(f"deterministic SSH service wiring is missing: {required}") + + authority = HOSTKEY_AUTHORITY.read_text(encoding="utf-8") + compile(authority, str(HOSTKEY_AUTHORITY), "exec") + for required in ( + 'STATE = Path("/var/lib/rigos")', + 'KEYS = SYSTEM / "ssh-hostkeys"', + '"schema": "rigos.ssh-hostkeys/v1"', + 'os.rename(temporary, KEYS)', + 'or status.get("outcome") != "ready"', + '"persistent SSH public and private keys do not match"', + '"persistent SSH host identity exists without a valid manifest"', + ): + if required not in authority: + raise RuntimeError(f"persistent SSH host-key authority contract is missing: {required}") + + require_lines( + HOSTKEY_UNIT, + ( + "After=rigos-state-ready.service", + "Requires=rigos-state-ready.service", + "Before=ssh.service", + "ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys", + "ReadWritePaths=/var/lib/rigos /run/rigos", + "WantedBy=multi-user.target", + ), + ) + require_lines( + SSH_DROPIN, + ( + "After=rigos-recovery-access.service rigos-ssh-hostkeys.service", + "Requires=rigos-ssh-hostkeys.service", + "Wants=rigos-remote-access-observe.service", + ), + ) + if "Before=rigos-ssh-hostkeys.service" not in STATE_READY_UNIT.read_text(encoding="utf-8"): + raise RuntimeError("state readiness is not ordered before persistent SSH identity") + dockerfile = DOCKERFILE.read_text(encoding="utf-8") if 'ENV PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:${PATH}"' not in dockerfile: raise RuntimeError("builder Cargo PATH is not explicit") @@ -62,7 +128,7 @@ def main() -> int: if required not in recovery_gate: raise RuntimeError(f"recovery access validator contract is missing: {required}") - print("RIGOS Alpha8 SSH and recovery hotfix verification passed") + print("RIGOS Alpha8 SSH, recovery, and persistent host-key verification passed") return 0 From 310d9d6a37c28abe8ee81f1931ff92b2d8d92737 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:50:14 +0700 Subject: [PATCH 185/386] run SSH identity checks before image build --- scripts/build-usb-image-entrypoint.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 0466e3f7..4d31eba2 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -20,9 +20,11 @@ fi source "$version_env" python3 ./scripts/check-alpha8-ssh-hotfix.py +python3 ./scripts/verify-systemd-ordering.py python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr \ - ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate + ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \ + ./build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture From 72d7df7cbd8671adc54ef31658f79e336a9d8b14 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:50:53 +0700 Subject: [PATCH 186/386] verify persistent SSH identity in exact image --- scripts/verify-randomx-performance-image.sh | 51 ++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/scripts/verify-randomx-performance-image.sh b/scripts/verify-randomx-performance-image.sh index f4ed9133..1a774407 100644 --- a/scripts/verify-randomx-performance-image.sh +++ b/scripts/verify-randomx-performance-image.sh @@ -34,6 +34,12 @@ squashfs="$temporary/root-a/live/filesystem.squashfs" listing="$temporary/squashfs.list" unsquashfs -ll "$squashfs" >"$listing" +if awk '{print $NF}' "$listing" \ + | grep -E '^squashfs-root/etc/ssh/ssh_host_.*_key(\.pub)?$' >/dev/null +then + die 'appliance image contains a baked SSH host key' +fi + msr_support="missing" # Do not use grep -q in a pipe while pipefail is enabled. An early grep exit can # SIGPIPE the producer and turn a real match into a false pipeline failure. @@ -63,29 +69,42 @@ fi unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ usr/bin/python3 usr/bin/python3.11 usr/bin/kmod usr/sbin/modprobe \ usr/lib/rigos/rigos-randomx-msr usr/lib/rigos/rigos-miner-gate \ + usr/lib/rigos/rigos-ssh-hostkeys \ + etc/ssh/sshd_config.d/01-rigos-hostkeys.conf \ etc/systemd/system/rigos-randomx-msr.service \ + etc/systemd/system/rigos-ssh-hostkeys.service \ + etc/systemd/system/ssh.service.d/rigos-observe.conf \ etc/systemd/system/rigos-miner.service \ etc/systemd/system/rigos-miner.service.d/randomx-msr.conf \ etc/systemd/system/multi-user.target.wants/rigos-randomx-msr.service \ + etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service \ >/dev/null root="$temporary/squash" -[[ -x "$root/usr/bin/python3" ]] || die 'Python runtime for MSR authority is missing' +[[ -x "$root/usr/bin/python3" ]] || die 'Python runtime for authority services is missing' [[ -x "$root/usr/bin/kmod" ]] || die 'kmod runtime for MSR authority is missing' [[ -L "$root/usr/sbin/modprobe" || -x "$root/usr/sbin/modprobe" ]] || die 'modprobe entrypoint is missing' [[ -f "$root/usr/lib/rigos/rigos-randomx-msr" ]] || die 'RandomX MSR authority is missing' [[ -f "$root/usr/lib/rigos/rigos-miner-gate" ]] || die 'miner safety gate is missing' +[[ -f "$root/usr/lib/rigos/rigos-ssh-hostkeys" ]] || die 'persistent SSH host-key authority is missing' [[ -L "$root/etc/systemd/system/multi-user.target.wants/rigos-randomx-msr.service" ]] \ || die 'RandomX MSR authority is not enabled in the appliance' +[[ -L "$root/etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service" ]] \ + || die 'persistent SSH host-key authority is not enabled in the appliance' python3 -m py_compile \ "$root/usr/lib/rigos/rigos-randomx-msr" \ - "$root/usr/lib/rigos/rigos-miner-gate" + "$root/usr/lib/rigos/rigos-miner-gate" \ + "$root/usr/lib/rigos/rigos-ssh-hostkeys" service="$root/etc/systemd/system/rigos-randomx-msr.service" dropin="$root/etc/systemd/system/rigos-miner.service.d/randomx-msr.conf" miner="$root/etc/systemd/system/rigos-miner.service" authority="$root/usr/lib/rigos/rigos-randomx-msr" miner_gate="$root/usr/lib/rigos/rigos-miner-gate" +hostkey_service="$root/etc/systemd/system/rigos-ssh-hostkeys.service" +hostkey_authority="$root/usr/lib/rigos/rigos-ssh-hostkeys" +hostkey_policy="$root/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf" +ssh_dropin="$root/etc/systemd/system/ssh.service.d/rigos-observe.conf" for required in \ 'ExecStartPre=-/usr/sbin/modprobe msr' \ @@ -106,6 +125,33 @@ grep -Fqx 'User=rigos' "$miner" || die 'miner no longer runs as the unprivileged grep -Fqx 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$miner" \ || die 'miner safety gate is not wired' +for required in \ + 'After=rigos-state-ready.service' \ + 'Requires=rigos-state-ready.service' \ + 'Before=ssh.service' \ + 'ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys' \ + 'ReadWritePaths=/var/lib/rigos /run/rigos' +do + grep -Fqx "$required" "$hostkey_service" \ + || die "persistent SSH host-key service contract is missing: $required" +done +grep -Fqx 'HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key' "$hostkey_policy" \ + || die 'sshd does not use the persistent RIGOS host identity' +grep -Fqx 'Requires=rigos-ssh-hostkeys.service' "$ssh_dropin" \ + || die 'ssh.service does not require persistent host identity' +grep -Fqx 'After=rigos-recovery-access.service rigos-ssh-hostkeys.service' "$ssh_dropin" \ + || die 'ssh.service ordering bypasses persistent host identity' +for required in \ + 'STATE = Path("/var/lib/rigos")' \ + 'KEYS = SYSTEM / "ssh-hostkeys"' \ + '"schema": "rigos.ssh-hostkeys/v1"' \ + 'os.rename(temporary, KEYS)' \ + '"persistent SSH host identity exists without a valid manifest"' +do + grep -Fq "$required" "$hostkey_authority" \ + || die "persistent SSH host-key authority contract is missing: $required" +done + for required in \ 'SUPPORTED_CPUS = {("GenuineIntel", 6, 42)}' \ 'REGISTER = 0x1A4' \ @@ -128,4 +174,5 @@ do done printf 'RIGOS RandomX kernel MSR support: %s\n' "$msr_support" +printf 'RIGOS persistent SSH host-key image verification passed\n' printf 'RIGOS RandomX performance image verification passed: %s\n' "$image" From 546f929f733ee4e9a352bdafd89f66d3a85a6c51 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:51:28 +0700 Subject: [PATCH 187/386] correct host-key service entrypoint check --- scripts/verify-systemd-ordering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/verify-systemd-ordering.py b/scripts/verify-systemd-ordering.py index cc1d7c83..a6f84438 100644 --- a/scripts/verify-systemd-ordering.py +++ b/scripts/verify-systemd-ordering.py @@ -79,8 +79,8 @@ def verify(units): includes(hostkeys.words("Unit", "Requires"), {"rigos-state-ready.service"}, "SSH host-key authority must require state readiness") includes(hostkeys.words("Unit", "Before"), {"ssh.service"}, "SSH host-key authority must precede sshd") require( - hostkeys.scalar("Service", "ExecStart") == "/usr/bin/python3", - "SSH host-key authority must use the absolute Python runtime", + hostkeys.scalar("Service", "ExecStart") == "/usr/lib/rigos/rigos-ssh-hostkeys", + "SSH host-key authority entrypoint is not exact", ) includes(hostkeys.words("Install", "WantedBy"), {"multi-user.target"}, "SSH host-key authority must be enabled under multi-user") From 862d5b01ac29857fbf5226181d449e5f19aca8bd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 21:54:23 +0700 Subject: [PATCH 188/386] verify persistent SSH identity in appliance image --- scripts/verify-usb-appliance.sh | 52 ++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index 6fab56fe..f89df2bf 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -68,17 +68,29 @@ mount -o ro "$p3" "$temporary/b" [[ "$(sha256sum "$p3" | cut -d' ' -f1)" == "$(jq -r .root_b_sha256 "$manifest")" ]] || die 'ROOT_B hash mismatch' cmp "$temporary/a/live/filesystem.squashfs" "$temporary/b/live/filesystem.squashfs" cmp "$temporary/a/image-layout.json" "$temporary/b/image-layout.json" -[[ "$(sha256sum "$temporary/a/live/filesystem.squashfs" | cut -d' ' -f1)" == "$(jq -r .root_payload_sha256 "$manifest")" ]] || die 'root payload hash mismatch' +squashfs="$temporary/a/live/filesystem.squashfs" +[[ "$(sha256sum "$squashfs" | cut -d' ' -f1)" == "$(jq -r .root_payload_sha256 "$manifest")" ]] || die 'root payload hash mismatch' [[ "$(jq -r .schema "$temporary/a/image-layout.json")" == 'rigos.image-layout/v2' ]] || die 'embedded layout schema mismatch' [[ "$(jq -r .partition_table "$temporary/a/image-layout.json")" == mbr ]] || die 'embedded layout table mismatch' [[ "$(jq -r .final_state_partition "$temporary/a/image-layout.json")" == 4 ]] || die 'final state partition mismatch' [[ "$(jq -r '.partitions[-1].label' "$temporary/a/image-layout.json")" == RIGOS_STATE_SEED ]] || die 'state seed is not final' -unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squashfs" \ + +listing="$temporary/squashfs.list" +unsquashfs -ll "$squashfs" >"$listing" +if awk '{print $NF}' "$listing" | grep -E '^squashfs-root/etc/ssh/ssh_host_.*_key(\.pub)?$' >/dev/null; then + die 'appliance image contains a baked SSH host key' +fi + +unsquashfs -no-progress -d "$temporary/root" "$squashfs" \ etc/rigos-release etc/os-release \ + etc/ssh/sshd_config.d/01-rigos-hostkeys.conf \ usr/lib/tmpfiles.d/rigos.conf \ etc/systemd/system/rigos-state.service \ etc/systemd/system/rigos-state-ready.service \ etc/systemd/system/rigos-recovery-access.service \ + etc/systemd/system/rigos-ssh-hostkeys.service \ + etc/systemd/system/ssh.service.d/rigos-observe.conf \ + etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service \ etc/systemd/system/rigos-firstboot.service \ etc/systemd/system/rigos-hugepages.service \ etc/systemd/system/rigos-miner.service \ @@ -88,20 +100,23 @@ unsquashfs -no-progress -d "$temporary/root" "$temporary/a/live/filesystem.squas etc/systemd/system/rigos-miner-health.timer \ etc/systemd/system/rigos-runtime-render.service \ etc/systemd/system/rigos-profile-apply.service \ - usr/bin/jq usr/bin/python3 usr/bin/python3.11 \ + usr/bin/jq usr/bin/python3 usr/bin/python3.11 usr/bin/findmnt usr/bin/ssh-keygen \ usr/lib/rigos/rigosd usr/lib/rigos/rigosctl \ - usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-miner-health usr/lib/rigos/rigos-runtime-render usr/lib/rigos/rigos-runtime-publish usr/lib/rigos/rigos-runtime-authority usr/lib/rigos/rigos-runtime-gate usr/lib/rigos/xmrig \ + usr/lib/rigos/lsblk-compat usr/lib/rigos/rigos-state-init usr/lib/rigos/rigos-state-ready usr/lib/rigos/rigos-config usr/lib/rigos/rigos-performance usr/lib/rigos/rigos-lifecycle-cycles usr/lib/rigos/rigos-miner-gate usr/lib/rigos/rigos-miner-health usr/lib/rigos/rigos-runtime-render usr/lib/rigos/rigos-runtime-publish usr/lib/rigos/rigos-runtime-authority usr/lib/rigos/rigos-runtime-gate usr/lib/rigos/rigos-ssh-hostkeys usr/lib/rigos/xmrig \ usr/local/bin/rigosd usr/local/bin/rigosctl \ usr/local/sbin/rigosctl usr/local/sbin/rigos-firstboot usr/local/sbin/rigos-recovery-access usr/local/sbin/rigos-state-orchestrate \ usr/share/rigos >/dev/null grep -Fqx "VERSION_ID=\"$image_version\"" "$temporary/root/etc/rigos-release" || die 'embedded release version mismatch' grep -q 'NAME="RIGOS"' "$temporary/root/etc/os-release" || die 'embedded OS identity mismatch' [[ -x "$temporary/root/usr/bin/jq" ]] || die 'jq runtime dependency is missing from the appliance' +[[ -x "$temporary/root/usr/bin/findmnt" ]] || die 'findmnt runtime dependency is missing from the appliance' +[[ -x "$temporary/root/usr/bin/ssh-keygen" ]] || die 'ssh-keygen runtime dependency is missing from the appliance' python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-firstboot" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-recovery-access" python3 -m py_compile "$temporary/root/usr/local/sbin/rigos-state-orchestrate" python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-gate" python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-miner-health" +python3 -m py_compile "$temporary/root/usr/lib/rigos/rigos-ssh-hostkeys" sh -n "$temporary/root/usr/lib/rigos/rigos-runtime-publish" sh -n "$temporary/root/usr/lib/rigos/rigos-runtime-authority" python3 "$script_dir/verify-systemd-ordering.py" "$temporary/root/etc/systemd/system" @@ -121,6 +136,35 @@ if strings "$temporary/root/usr/lib/rigos/rigos-state-init" | grep -F '/run/rigo grep -Fq 'ExecStart=/usr/lib/rigos/rigos-state-ready' "$temporary/root/etc/systemd/system/rigos-state-ready.service" || die 'state readiness verifier is not wired' if grep -Fq 'Wants=rigos-recovery-access.service' "$temporary/root/etc/systemd/system/rigos-state-ready.service"; then die 'state readiness retriggers interactive recovery access'; fi grep -Fq 'Requires=rigos-state-ready.service' "$temporary/root/etc/systemd/system/rigos-profile-apply.service" || die 'profile apply bypasses state readiness' + +hostkey_service="$temporary/root/etc/systemd/system/rigos-ssh-hostkeys.service" +hostkey_policy="$temporary/root/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf" +ssh_dropin="$temporary/root/etc/systemd/system/ssh.service.d/rigos-observe.conf" +hostkey_authority="$temporary/root/usr/lib/rigos/rigos-ssh-hostkeys" +[[ -x "$hostkey_authority" ]] || die 'persistent SSH host-key authority is missing or not executable' +[[ -L "$temporary/root/etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service" ]] || die 'persistent SSH host-key service is not enabled' +for required in \ + 'After=rigos-state-ready.service' \ + 'Requires=rigos-state-ready.service' \ + 'Before=ssh.service' \ + 'ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys' \ + 'ReadWritePaths=/var/lib/rigos /run/rigos' +do + grep -Fqx "$required" "$hostkey_service" || die "persistent SSH host-key service contract is missing: $required" +done +grep -Fqx 'HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key' "$hostkey_policy" || die 'sshd persistent HostKey policy is missing' +grep -Fqx 'Requires=rigos-ssh-hostkeys.service' "$ssh_dropin" || die 'ssh.service does not require persistent host identity' +grep -Fqx 'After=rigos-recovery-access.service rigos-ssh-hostkeys.service' "$ssh_dropin" || die 'ssh.service ordering bypasses persistent host identity' +for required in \ + 'STATE = Path("/var/lib/rigos")' \ + 'KEYS = SYSTEM / "ssh-hostkeys"' \ + '"schema": "rigos.ssh-hostkeys/v1"' \ + 'os.rename(temporary, KEYS)' \ + '"persistent SSH host identity exists without a valid manifest"' +do + grep -Fq "$required" "$hostkey_authority" || die "persistent SSH host-key authority contract is missing: $required" +done + [[ -x "$temporary/root/usr/lib/rigos/rigos-performance" ]] || die 'performance authority is missing or not executable' grep -Fq 'After=rigos-state-ready.service rigos-profile-apply.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority ordering is missing' grep -Fq 'Before=rigos-miner.service' "$temporary/root/etc/systemd/system/rigos-hugepages.service" || die 'huge page authority is not ordered before miner' From f473c2f696b8a9b30e08f5459aeea4a887530bf1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:14:43 +0700 Subject: [PATCH 189/386] add loopback-only authenticated XMRig API authority --- .../usr/lib/rigos/rigos-runtime-render | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render index 3effedd9..fca7c8f9 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render @@ -4,6 +4,9 @@ import grp import hashlib import json import os +import re +import secrets +import stat import tempfile from pathlib import Path @@ -12,7 +15,12 @@ RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) PRIVATE_CONFIG = RUNTIME / "xmrig.json" PUBLIC_CONFIG = RUNTIME / "xmrig-public.json" STATUS = RUNTIME / "runtime-config-status.json" +API_TOKEN = RUNTIME / "xmrig-api-token" +API_HOST = "127.0.0.1" +API_PORT = 18080 MAX_JSON_BYTES = 2 * 1024 * 1024 +TOKEN_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$") +SKIP_CHOWN = os.environ.get("RIGOS_RENDER_SKIP_CHOWN") == "1" def read_object(path: Path) -> dict: @@ -53,6 +61,57 @@ def atomic_json(path: Path, value: dict, mode: int, gid: int | None = None) -> N temporary.unlink(missing_ok=True) +def token_owner_uid() -> int: + return os.geteuid() if SKIP_CHOWN else 0 + + +def validate_token_descriptor(descriptor: int) -> str: + observed = os.fstat(descriptor) + if not stat.S_ISREG(observed.st_mode): + raise RuntimeError("XMRig API token is not a regular file") + if observed.st_uid != token_owner_uid(): + raise RuntimeError("XMRig API token owner is unsafe") + if stat.S_IMODE(observed.st_mode) != 0o600: + raise RuntimeError("XMRig API token mode is unsafe") + if observed.st_size > 256: + raise RuntimeError("XMRig API token is oversized") + os.lseek(descriptor, 0, os.SEEK_SET) + raw = os.read(descriptor, 257) + try: + token = raw.decode("ascii").strip() + except UnicodeDecodeError as error: + raise RuntimeError("XMRig API token is not ASCII") from error + if not TOKEN_RE.fullmatch(token): + raise RuntimeError("XMRig API token format is invalid") + return token + + +def load_or_create_api_token() -> str: + RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) + nofollow = getattr(os, "O_NOFOLLOW", 0) + flags = os.O_RDONLY | os.O_CLOEXEC | nofollow + try: + descriptor = os.open(API_TOKEN, flags) + except FileNotFoundError: + token = secrets.token_urlsafe(48) + if not TOKEN_RE.fullmatch(token): + raise RuntimeError("generated XMRig API token format is invalid") + create_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | nofollow + descriptor = os.open(API_TOKEN, create_flags, 0o600) + try: + os.write(descriptor, (token + "\n").encode("ascii")) + os.fsync(descriptor) + finally: + os.close(descriptor) + API_TOKEN.chmod(0o600) + fsync_directory(RUNTIME) + descriptor = os.open(API_TOKEN, flags) + try: + return validate_token_descriptor(descriptor) + finally: + os.close(descriptor) + + def exact_profile_name(algorithm: str) -> str: if algorithm in ("rx", "rx/0"): return "rx" @@ -103,6 +162,15 @@ def render() -> tuple[dict, dict, dict]: elif threads != "auto": raise RuntimeError("threads must be auto or an integer") + api_token = load_or_create_api_token() + runtime["http"] = { + "enabled": True, + "host": API_HOST, + "port": API_PORT, + "access-token": api_token, + "restricted": True, + } + public = copy.deepcopy(runtime) for pool in public.get("pools", []): if isinstance(pool, dict): @@ -142,6 +210,13 @@ def render() -> tuple[dict, dict, dict]: (json.dumps(runtime, sort_keys=True) + "\n").encode("utf-8") ).hexdigest(), "identity_redacted_public_view": True, + "http_api": { + "enabled": True, + "host": API_HOST, + "port": API_PORT, + "restricted": True, + "token_path": str(API_TOKEN), + }, } return runtime, public, status @@ -149,7 +224,7 @@ def render() -> tuple[dict, dict, dict]: def main() -> int: runtime, public, status = render() gid = None - if os.environ.get("RIGOS_RENDER_SKIP_CHOWN") != "1": + if not SKIP_CHOWN: gid = grp.getgrnam("rigos").gr_gid atomic_json(PRIVATE_CONFIG, runtime, 0o640, gid) atomic_json(PUBLIC_CONFIG, public, 0o644) From eb24ace9512c303034f2bb6a2dda7b07e5cd6c00 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:15:41 +0700 Subject: [PATCH 190/386] replace journal-only miner health with authenticated XMRig API evidence --- .../usr/lib/rigos/rigos-miner-health | 194 +++++++++++++++++- 1 file changed, 188 insertions(+), 6 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index 39221147..3fa6be32 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -1,8 +1,11 @@ #!/usr/bin/python3 import datetime as dt +import http.client import json +import math import os import re +import stat import subprocess import sys import uuid @@ -15,9 +18,16 @@ PROC = Path(os.environ.get("RIGOS_PROC_ROOT", "/proc")) SYSTEMCTL = os.environ.get("RIGOS_SYSTEMCTL", "/usr/bin/systemctl") JOURNALCTL = os.environ.get("RIGOS_JOURNALCTL", "/usr/bin/journalctl") STATUS = RUNTIME / "miner-health-status.json" +API_TOKEN = Path(os.environ.get("RIGOS_XMRIG_API_TOKEN_PATH", str(RUNTIME / "xmrig-api-token"))) +API_HOST = os.environ.get("RIGOS_XMRIG_API_HOST", "127.0.0.1") +API_PORT = int(os.environ.get("RIGOS_XMRIG_API_PORT", "18080")) +API_TIMEOUT_SECONDS = float(os.environ.get("RIGOS_XMRIG_API_TIMEOUT_SECONDS", "2")) +API_MAX_BYTES = 256 * 1024 +TEST_MODE = os.environ.get("RIGOS_MINER_HEALTH_TEST_MODE") == "1" WINDOW_SECONDS = int(os.environ.get("RIGOS_MINER_HEALTH_WINDOW_SECONDS", "180")) WARMUP_SECONDS = int(os.environ.get("RIGOS_MINER_WARMUP_SECONDS", "240")) MAX_JOURNAL_LINES = 500 +TOKEN_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$") EXTERNAL_MARKERS = ( "connect error", @@ -43,7 +53,7 @@ def atomic_json(path: Path, value: dict) -> None: handle.flush() os.fsync(handle.fileno()) os.replace(temporary, path) - directory = os.open(path.parent, os.O_RDONLY) + directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(directory) finally: @@ -155,6 +165,148 @@ def last_share_counts(text: str) -> tuple[int | None, int | None]: return int(accepted), int(rejected) +def expected_token_uid() -> int: + return os.geteuid() if TEST_MODE else 0 + + +def read_api_token() -> str: + nofollow = getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(API_TOKEN, os.O_RDONLY | os.O_CLOEXEC | nofollow) + try: + observed = os.fstat(descriptor) + if not stat.S_ISREG(observed.st_mode): + raise RuntimeError("api_token_not_regular") + if observed.st_uid != expected_token_uid(): + raise RuntimeError("api_token_owner_unsafe") + if stat.S_IMODE(observed.st_mode) != 0o600: + raise RuntimeError("api_token_mode_unsafe") + if observed.st_size > 256: + raise RuntimeError("api_token_oversized") + raw = os.read(descriptor, 257) + finally: + os.close(descriptor) + try: + token = raw.decode("ascii").strip() + except UnicodeDecodeError as error: + raise RuntimeError("api_token_not_ascii") from error + if not TOKEN_RE.fullmatch(token): + raise RuntimeError("api_token_invalid") + return token + + +def fetch_api_summary() -> tuple[dict | None, str | None]: + connection = None + try: + token = read_api_token() + connection = http.client.HTTPConnection( + API_HOST, + API_PORT, + timeout=API_TIMEOUT_SECONDS, + ) + connection.request( + "GET", + "/2/summary", + headers={ + "Accept": "application/json", + "Authorization": f"Bearer {token}", + "Connection": "close", + }, + ) + response = connection.getresponse() + if response.status != 200: + response.read(min(API_MAX_BYTES, 4096)) + return None, f"api_http_status_{response.status}" + length = response.getheader("Content-Length") + if length is not None: + try: + if int(length) > API_MAX_BYTES: + return None, "api_response_oversized" + except ValueError: + return None, "api_content_length_invalid" + raw = response.read(API_MAX_BYTES + 1) + if len(raw) > API_MAX_BYTES: + return None, "api_response_oversized" + value = json.loads(raw) + if not isinstance(value, dict): + return None, "api_response_not_object" + return value, None + except FileNotFoundError: + return None, "api_token_missing" + except PermissionError: + return None, "api_token_permission_denied" + except (OSError, RuntimeError, http.client.HTTPException, json.JSONDecodeError) as error: + message = str(error) + if message.startswith("api_"): + return None, message + return None, "api_unavailable" + finally: + if connection is not None: + connection.close() + + +def nonnegative_number(value: object) -> int | float | None: + if isinstance(value, bool) or not isinstance(value, (int, float)): + return None + if not math.isfinite(float(value)) or value < 0: + return None + return value + + +def summary_metrics(summary: dict) -> dict: + hashrate = summary.get("hashrate") + if not isinstance(hashrate, dict): + hashrate = {} + raw_total = hashrate.get("total") + rates: list[int | float | None] = [None, None, None] + if isinstance(raw_total, list): + for index, value in enumerate(raw_total[:3]): + rates[index] = nonnegative_number(value) + + connection = summary.get("connection") + if not isinstance(connection, dict): + connection = {} + results = summary.get("results") + if not isinstance(results, dict): + results = {} + + accepted = nonnegative_number(connection.get("accepted")) + rejected = nonnegative_number(connection.get("rejected")) + if accepted is None: + accepted = nonnegative_number(results.get("shares_good")) + if rejected is None: + total = nonnegative_number(results.get("shares_total")) + if total is not None and accepted is not None and total >= accepted: + rejected = total - accepted + + pool = connection.get("pool") + if not isinstance(pool, str) or not pool: + pool = None + hugepages = summary.get("hugepages") + hugepages_used = None + hugepages_total = None + if isinstance(hugepages, list) and len(hugepages) >= 2: + hugepages_used = nonnegative_number(hugepages[0]) + hugepages_total = nonnegative_number(hugepages[1]) + + return { + "uptime_seconds": nonnegative_number(summary.get("uptime")), + "algorithm": summary.get("algo") if isinstance(summary.get("algo"), str) else None, + "hashrate_10s": rates[0], + "hashrate_60s": rates[1], + "hashrate_15m": rates[2], + "hashrate_highest": nonnegative_number(hashrate.get("highest")), + "pool_connected": pool is not None, + "pool": pool, + "connection_uptime_seconds": nonnegative_number(connection.get("uptime")), + "connection_failures": nonnegative_number(connection.get("failures")), + "pool_ping_ms": nonnegative_number(connection.get("ping")), + "accepted_shares": int(accepted) if accepted is not None else None, + "rejected_shares": int(rejected) if rejected is not None else None, + "hugepages_used": int(hugepages_used) if hugepages_used is not None else None, + "hugepages_total": int(hugepages_total) if hugepages_total is not None else None, + } + + def classify( properties: dict[str, str], proc_state: str | None, @@ -162,6 +314,8 @@ def classify( canonical_revision: str | None, rendered_outcome: str | None, rendered_revision: str | None, + api_metrics: dict | None, + api_error: str | None, journal: str, journal_available: bool, ) -> tuple[str, str | None]: @@ -177,17 +331,31 @@ def classify( return "blocked", "current_revision_unavailable" if rendered_outcome != "ready" or rendered_revision != canonical_revision: return "blocked", "runtime_revision_mismatch" - if not journal_available: - return "unknown", "journal_unavailable" + if api_metrics is not None: + rates = ( + api_metrics.get("hashrate_10s"), + api_metrics.get("hashrate_60s"), + api_metrics.get("hashrate_15m"), + ) + if any(isinstance(value, (int, float)) and value > 0 for value in rates): + return "ready", None + if active_for is not None and active_for < WARMUP_SECONDS: + return "warming_up", None + if not api_metrics.get("pool_connected"): + return "waiting_external", "pool_or_network_unavailable" + return "degraded", "no_hashrate_from_api" + + if not journal_available: + return "unknown", api_error or "journal_unavailable" lowered = journal.lower() - if "miner speed" in lowered: + if "miner speed" in lowered or "accepted (" in lowered: return "ready", None if any(marker in lowered for marker in EXTERNAL_MARKERS): return "waiting_external", "pool_or_network_unavailable" if active_for is not None and active_for < WARMUP_SECONDS: return "warming_up", None - return "degraded", "no_recent_speed_evidence" + return "degraded", api_error or "miner_api_unavailable" def main() -> int: @@ -204,8 +372,10 @@ def main() -> int: active_for = active_seconds(properties) canonical = current_revision() rendered_outcome, rendered_revision = runtime_revision() + summary, api_error = fetch_api_summary() + metrics = summary_metrics(summary) if summary is not None else None journal, journal_available = recent_journal() - accepted, rejected = last_share_counts(journal) + journal_accepted, journal_rejected = last_share_counts(journal) state, reason = classify( properties, proc_state, @@ -213,10 +383,14 @@ def main() -> int: canonical, rendered_outcome, rendered_revision, + metrics, + api_error, journal, journal_available, ) lowered_journal = journal.lower() + accepted = metrics.get("accepted_shares") if metrics is not None else journal_accepted + rejected = metrics.get("rejected_shares") if metrics is not None else journal_rejected value = { "schema": "rigos.miner-health-status/v1", "generated_at": dt.datetime.now(dt.timezone.utc).isoformat(timespec="milliseconds"), @@ -238,7 +412,14 @@ def main() -> int: "runtime_outcome": rendered_outcome, "runtime_revision": rendered_revision, }, + "api": { + "available": metrics is not None, + "error": api_error, + "endpoint": f"http://{API_HOST}:{API_PORT}/2/summary", + "metrics": metrics, + }, "evidence": { + "source": "xmrig_http_api" if metrics is not None else "journal_fallback", "window_seconds": WINDOW_SECONDS, "journal_available": journal_available, "journal_line_limit": MAX_JOURNAL_LINES, @@ -256,6 +437,7 @@ def main() -> int: "schema": "rigos.miner-health-observer/v1", "state": state, "reason": reason, + "evidence_source": value["evidence"]["source"], "remediation": "observe_only", }, sort_keys=True)) return 0 From 3e0d64b3279b6f38d575bebba0d3e24bfb389042 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:15:57 +0700 Subject: [PATCH 191/386] allow miner observer to query loopback XMRig API only --- .../etc/systemd/system/rigos-miner-health.service | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service index bd3523ea..9715a89e 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-miner-health.service @@ -12,7 +12,9 @@ ProtectSystem=strict ProtectKernelTunables=yes ProtectKernelModules=yes ProtectControlGroups=yes -RestrictAddressFamilies=AF_UNIX +RestrictAddressFamilies=AF_UNIX AF_INET +IPAddressDeny=any +IPAddressAllow=127.0.0.0/8 RestrictNamespaces=yes LockPersonality=yes ReadWritePaths=/run/rigos From fdd43b70e3aa2d9340d11b1ffb694dc2bd99e708 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:16:46 +0700 Subject: [PATCH 192/386] test authenticated XMRig API health evidence --- scripts/test-miner-health-api.py | 267 +++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 scripts/test-miner-health-api.py diff --git a/scripts/test-miner-health-api.py b/scripts/test-miner-health-api.py new file mode 100644 index 00000000..d2068cbe --- /dev/null +++ b/scripts/test-miner-health-api.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +import importlib.machinery +import importlib.util +import json +import os +import stat +import tempfile +import threading +import unittest +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RUNTIME_RENDER = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render" +MINER_HEALTH = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health" + + +def load_source(name: str, path: Path): + loader = importlib.machinery.SourceFileLoader(name, str(path)) + spec = importlib.util.spec_from_loader(name, loader) + if spec is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +class Environment: + def __init__(self, **values: str): + self.values = values + self.original: dict[str, str | None] = {} + + def __enter__(self): + for key, value in self.values.items(): + self.original[key] = os.environ.get(key) + os.environ[key] = value + return self + + def __exit__(self, _type, _value, _traceback): + for key, value in self.original.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + +class RuntimeRenderApiTests(unittest.TestCase): + def test_runtime_api_is_local_authenticated_redacted_and_idempotent(self): + with tempfile.TemporaryDirectory(prefix="rigos-render-api-") as temporary: + root = Path(temporary) + state = root / "state" + runtime = root / "run" + revision = state / "revisions/r1" + (revision / "flight-sheets").mkdir(parents=True) + (revision / "policy.json").write_text( + json.dumps({ + "schema": "rigos.policy/v1", + "active_flight_sheet": "sheet", + }), + encoding="utf-8", + ) + (revision / "flight-sheets/sheet.json").write_text( + json.dumps({ + "schema": "rigos.flight-sheet/v1", + "backend": "xmrig", + "algorithm": "rx/0", + "cpu": {"threads": "auto"}, + }), + encoding="utf-8", + ) + (revision / "xmrig.json").write_text( + json.dumps({ + "autosave": False, + "cpu": {"enabled": True, "huge-pages": True}, + "pools": [{ + "url": "pool.example:1234", + "user": "secret-wallet", + "pass": "worker", + "algo": "rx/0", + }], + "http": { + "enabled": True, + "host": "0.0.0.0", + "port": 9999, + "access-token": "attacker-controlled", + "restricted": False, + }, + }), + encoding="utf-8", + ) + state.mkdir(exist_ok=True) + (state / "current").symlink_to("revisions/r1") + + with Environment( + RIGOS_STATE_PATH=str(state), + RIGOS_RUNTIME_PATH=str(runtime), + RIGOS_RENDER_SKIP_CHOWN="1", + ): + module = load_source("rigos_runtime_render_test", RUNTIME_RENDER) + private_first, public_first, status_first = module.render() + token_path = runtime / "xmrig-api-token" + token_first = token_path.read_text(encoding="ascii").strip() + + self.assertEqual(private_first["http"]["host"], "127.0.0.1") + self.assertEqual(private_first["http"]["port"], 18080) + self.assertTrue(private_first["http"]["enabled"]) + self.assertTrue(private_first["http"]["restricted"]) + self.assertEqual(private_first["http"]["access-token"], token_first) + self.assertNotIn("access-token", public_first["http"]) + self.assertNotIn("secret-wallet", json.dumps(public_first)) + self.assertEqual(status_first["http_api"]["host"], "127.0.0.1") + self.assertEqual(status_first["http_api"]["port"], 18080) + self.assertNotIn(token_first, json.dumps(status_first)) + self.assertEqual(stat.S_IMODE(token_path.stat().st_mode), 0o600) + + private_second, _, _ = module.render() + self.assertEqual(private_second["http"]["access-token"], token_first) + self.assertEqual(token_path.read_text(encoding="ascii").strip(), token_first) + + self.assertEqual(module.main(), 0) + self.assertEqual(stat.S_IMODE((runtime / "xmrig.json").stat().st_mode), 0o640) + self.assertEqual(stat.S_IMODE((runtime / "xmrig-public.json").stat().st_mode), 0o644) + persisted_public = json.loads((runtime / "xmrig-public.json").read_text(encoding="utf-8")) + self.assertNotIn("access-token", persisted_public["http"]) + + +class SummaryHandler(BaseHTTPRequestHandler): + expected_token = "" + summary: dict = {} + + def do_GET(self): + if self.path != "/2/summary": + self.send_response(404) + self.end_headers() + return + if self.headers.get("Authorization") != f"Bearer {self.expected_token}": + self.send_response(401) + self.end_headers() + return + payload = json.dumps(self.summary).encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, _format, *_args): + return + + +class MinerHealthApiTests(unittest.TestCase): + def test_authenticated_summary_drives_health_truth(self): + with tempfile.TemporaryDirectory(prefix="rigos-health-api-") as temporary: + root = Path(temporary) + token = "A" * 48 + token_path = root / "xmrig-api-token" + token_path.write_text(token + "\n", encoding="ascii") + token_path.chmod(0o600) + + SummaryHandler.expected_token = token + SummaryHandler.summary = { + "uptime": 600, + "algo": "rx/0", + "hashrate": {"total": [341.2, 340.9, 340.7], "highest": 342.1}, + "connection": { + "pool": "pool.example:1234", + "uptime": 590, + "accepted": 43, + "rejected": 0, + "failures": 1, + "ping": 109, + }, + "hugepages": [1168, 1168], + } + server = HTTPServer(("127.0.0.1", 0), SummaryHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with Environment( + RIGOS_XMRIG_API_TOKEN_PATH=str(token_path), + RIGOS_XMRIG_API_HOST="127.0.0.1", + RIGOS_XMRIG_API_PORT=str(server.server_port), + RIGOS_MINER_HEALTH_TEST_MODE="1", + ): + module = load_source("rigos_miner_health_test", MINER_HEALTH) + summary, error = module.fetch_api_summary() + self.assertIsNone(error) + self.assertIsNotNone(summary) + metrics = module.summary_metrics(summary) + self.assertEqual(metrics["hashrate_60s"], 340.9) + self.assertEqual(metrics["accepted_shares"], 43) + self.assertEqual(metrics["rejected_shares"], 0) + self.assertTrue(metrics["pool_connected"]) + self.assertEqual(metrics["hugepages_used"], 1168) + self.assertEqual(metrics["hugepages_total"], 1168) + + state, reason = module.classify( + {"ActiveState": "active", "SubState": "running", "MainPID": "123"}, + "S", + 600, + "r1", + "ready", + "r1", + metrics, + None, + "", + True, + ) + self.assertEqual((state, reason), ("ready", None)) + + no_pool = dict(metrics) + no_pool.update({ + "hashrate_10s": 0, + "hashrate_60s": 0, + "hashrate_15m": 0, + "pool_connected": False, + "pool": None, + }) + state, reason = module.classify( + {"ActiveState": "active", "SubState": "running", "MainPID": "123"}, + "S", + 600, + "r1", + "ready", + "r1", + no_pool, + None, + "", + True, + ) + self.assertEqual((state, reason), ("waiting_external", "pool_or_network_unavailable")) + + state, reason = module.classify( + {"ActiveState": "active", "SubState": "running", "MainPID": "123"}, + "S", + 600, + "r1", + "ready", + "r1", + None, + "api_unavailable", + "cpu accepted (43/0) diff 10000", + True, + ) + self.assertEqual((state, reason), ("ready", None)) + + state, reason = module.classify( + {"ActiveState": "active", "SubState": "running", "MainPID": "123"}, + "S", + 600, + "r1", + "ready", + "r1", + None, + "api_unavailable", + "", + True, + ) + self.assertEqual((state, reason), ("degraded", "api_unavailable")) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From c89b2cbb65697549b0ff4a2b2fd6217904aedd46 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:17:11 +0700 Subject: [PATCH 193/386] run authenticated miner observer regression before image build --- scripts/build-usb-image-entrypoint.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 4d31eba2..ea81718e 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -24,7 +24,11 @@ python3 ./scripts/verify-systemd-ordering.py python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \ - ./build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys + ./build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys \ + ./build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render \ + ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health \ + ./scripts/test-miner-health-api.py +python3 ./scripts/test-miner-health-api.py export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture From c1301af43f78d75f98e2ff4e3ef0e60a6a76c8e7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:17:37 +0700 Subject: [PATCH 194/386] add exact-image verifier for authenticated miner telemetry --- scripts/verify-miner-observer-image.sh | 95 ++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 scripts/verify-miner-observer-image.sh diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh new file mode 100644 index 00000000..0f14168c --- /dev/null +++ b/scripts/verify-miner-observer-image.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -euo pipefail + +die() { + printf 'verify-miner-observer-image: %s\n' "$*" >&2 + exit 1 +} + +[[ $# -eq 1 ]] || die 'usage: verify-miner-observer-image.sh ' +image="$(readlink -f "$1")" +[[ -f "$image" ]] || die "image is missing: $image" +[[ "$(id -u)" -eq 0 ]] || die 'must run as root' + +partition_json="$(sfdisk --json "$image")" +start="$(jq -r '.partitiontable.partitions[1].start' <<<"$partition_json")" +size="$(jq -r '.partitiontable.partitions[1].size' <<<"$partition_json")" +[[ "$start" =~ ^[0-9]+$ && "$size" =~ ^[0-9]+$ ]] || die 'ROOT_A geometry is invalid' + +loop="$(losetup --find --show --read-only --offset $((start * 512)) --sizelimit $((size * 512)) "$image")" +temporary="$(mktemp -d)" +cleanup() { + set +e + mountpoint -q "$temporary/root-a" && umount "$temporary/root-a" + losetup -d "$loop" 2>/dev/null + rm -rf "$temporary" +} +trap cleanup EXIT + +mkdir -p "$temporary/root-a" "$temporary/squash" +mount -o ro "$loop" "$temporary/root-a" +squashfs="$temporary/root-a/live/filesystem.squashfs" +[[ -f "$squashfs" ]] || die 'ROOT_A squashfs is missing' + +unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ + usr/bin/python3 usr/bin/python3.11 \ + usr/lib/rigos/rigos-runtime-render \ + usr/lib/rigos/rigos-miner-health \ + etc/systemd/system/rigos-miner-health.service \ + etc/systemd/system/rigos-miner-health.timer \ + etc/systemd/system/rigos-runtime-render.service \ + >/dev/null + +root="$temporary/squash" +renderer="$root/usr/lib/rigos/rigos-runtime-render" +observer="$root/usr/lib/rigos/rigos-miner-health" +service="$root/etc/systemd/system/rigos-miner-health.service" +timer="$root/etc/systemd/system/rigos-miner-health.timer" + +[[ -x "$root/usr/bin/python3" ]] || die 'Python runtime is missing' +[[ -x "$renderer" ]] || die 'runtime renderer is missing or not executable' +[[ -x "$observer" ]] || die 'miner observer is missing or not executable' +python3 -m py_compile "$renderer" "$observer" + +for required in \ + 'API_TOKEN = RUNTIME / "xmrig-api-token"' \ + 'API_HOST = "127.0.0.1"' \ + 'API_PORT = 18080' \ + 'token = secrets.token_urlsafe(48)' \ + '"access-token": api_token' \ + '"restricted": True' \ + 'http.pop("access-token", None)' \ + '"token_path": str(API_TOKEN)' +do + grep -Fq "$required" "$renderer" || die "runtime API authority contract is missing: $required" +done +if grep -Eq 'API_HOST = "(0\.0\.0\.0|::)"' "$renderer"; then + die 'runtime API authority exposes a non-loopback bind' +fi + +for required in \ + 'connection.request(' \ + '"/2/summary"' \ + '"Authorization": f"Bearer {token}"' \ + 'API_MAX_BYTES = 256 * 1024' \ + '"source": "xmrig_http_api" if metrics is not None else "journal_fallback"' \ + 'return "degraded", "no_hashrate_from_api"' \ + 'return "degraded", api_error or "miner_api_unavailable"' +do + grep -Fq "$required" "$observer" || die "miner observer API contract is missing: $required" +done + +for required in \ + 'ExecStart=/usr/bin/python3 /usr/lib/rigos/rigos-miner-health' \ + 'RestrictAddressFamilies=AF_UNIX AF_INET' \ + 'IPAddressDeny=any' \ + 'IPAddressAllow=127.0.0.0/8' \ + 'ReadWritePaths=/run/rigos' +do + grep -Fqx "$required" "$service" || die "miner observer sandbox contract is missing: $required" +done +grep -Fq 'OnUnitActiveSec=1min' "$timer" || die 'miner observer timer cadence is missing' +grep -Fq 'ExecStart=/usr/lib/rigos/rigos-runtime-authority' "$root/etc/systemd/system/rigos-runtime-render.service" \ + || die 'serialized runtime authority is not wired' + +printf 'RIGOS authenticated XMRig observer image verification passed: %s\n' "$image" From adeb6faec0cdc7732516b657c4f31ab57b7d861c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:17:50 +0700 Subject: [PATCH 195/386] gate image build on authenticated miner observer verifier --- scripts/build-usb-image-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index ea81718e..380ab2c7 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -38,3 +38,4 @@ cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture image="./dist/usb/${RIGOS_IMAGE_ID}-${RIGOS_IMAGE_VERSION}.img" bash ./scripts/verify-randomx-performance-image.sh "$image" +bash ./scripts/verify-miner-observer-image.sh "$image" From 943dbb3adf3cb4f1622b1b1888c550650df1fb0c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:53:55 +0700 Subject: [PATCH 196/386] keep xmrig api token outside render staging --- .../usb/includes.chroot/usr/lib/rigos/rigos-runtime-render | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render index fca7c8f9..c1f1bdce 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render @@ -15,7 +15,7 @@ RUNTIME = Path(os.environ.get("RIGOS_RUNTIME_PATH", "/run/rigos")) PRIVATE_CONFIG = RUNTIME / "xmrig.json" PUBLIC_CONFIG = RUNTIME / "xmrig-public.json" STATUS = RUNTIME / "runtime-config-status.json" -API_TOKEN = RUNTIME / "xmrig-api-token" +API_TOKEN = Path(os.environ.get("RIGOS_XMRIG_API_TOKEN_PATH", str(RUNTIME / "xmrig-api-token"))) API_HOST = "127.0.0.1" API_PORT = 18080 MAX_JSON_BYTES = 2 * 1024 * 1024 @@ -87,7 +87,7 @@ def validate_token_descriptor(descriptor: int) -> str: def load_or_create_api_token() -> str: - RUNTIME.mkdir(mode=0o755, parents=True, exist_ok=True) + API_TOKEN.parent.mkdir(mode=0o755, parents=True, exist_ok=True) nofollow = getattr(os, "O_NOFOLLOW", 0) flags = os.O_RDONLY | os.O_CLOEXEC | nofollow try: @@ -104,7 +104,7 @@ def load_or_create_api_token() -> str: finally: os.close(descriptor) API_TOKEN.chmod(0o600) - fsync_directory(RUNTIME) + fsync_directory(API_TOKEN.parent) descriptor = os.open(API_TOKEN, flags) try: return validate_token_descriptor(descriptor) From db5da19919066b73805ae4ab2fc65903775116ec Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:54:10 +0700 Subject: [PATCH 197/386] wire persistent xmrig api token authority --- build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish index c0aa5740..7e3611bc 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish @@ -24,7 +24,9 @@ cleanup() { } trap cleanup EXIT HUP INT TERM -RIGOS_RUNTIME_PATH="$stage" "$renderer" +RIGOS_RUNTIME_PATH="$stage" \ +RIGOS_XMRIG_API_TOKEN_PATH="$runtime/xmrig-api-token" \ + "$renderer" "$jq_bin" -e ' .schema == "rigos.runtime-config-status/v1" and From a995812cc9d1e7598004ef7b78e26aee9a95b6be Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:54:32 +0700 Subject: [PATCH 198/386] test runtime token publication across staging cleanup --- scripts/test-runtime-token-publication.py | 145 ++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 scripts/test-runtime-token-publication.py diff --git a/scripts/test-runtime-token-publication.py b/scripts/test-runtime-token-publication.py new file mode 100644 index 00000000..2df1a71a --- /dev/null +++ b/scripts/test-runtime-token-publication.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +import importlib.machinery +import importlib.util +import json +import os +import shutil +import stat +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +RENDERER = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render" +PUBLISHER = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish" + + +def load_source(name: str, path: Path): + loader = importlib.machinery.SourceFileLoader(name, str(path)) + spec = importlib.util.spec_from_loader(name, loader) + if spec is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +class Environment: + def __init__(self, **values: str): + self.values = values + self.original: dict[str, str | None] = {} + + def __enter__(self): + for key, value in self.values.items(): + self.original[key] = os.environ.get(key) + os.environ[key] = value + return self + + def __exit__(self, _type, _value, _traceback): + for key, value in self.original.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + +def create_state(root: Path) -> Path: + state = root / "state" + revision = state / "revisions/r1" + (revision / "flight-sheets").mkdir(parents=True) + (revision / "policy.json").write_text( + json.dumps({ + "schema": "rigos.policy/v1", + "active_flight_sheet": "sheet", + }), + encoding="utf-8", + ) + (revision / "flight-sheets/sheet.json").write_text( + json.dumps({ + "schema": "rigos.flight-sheet/v1", + "backend": "xmrig", + "algorithm": "rx/0", + "cpu": {"threads": "auto"}, + }), + encoding="utf-8", + ) + (revision / "xmrig.json").write_text( + json.dumps({ + "autosave": False, + "cpu": {"enabled": True, "huge-pages": True}, + "pools": [{ + "url": "pool.example:1234", + "user": "secret-wallet", + "pass": "worker", + "algo": "rx/0", + }], + }), + encoding="utf-8", + ) + (state / "current").symlink_to("revisions/r1") + return state + + +class RuntimeTokenPublicationTests(unittest.TestCase): + def test_token_survives_render_stage_cleanup_and_is_reused(self): + with tempfile.TemporaryDirectory(prefix="rigos-token-publication-") as temporary: + root = Path(temporary) + state = create_state(root) + authority_runtime = root / "runtime" + token_path = authority_runtime / "xmrig-api-token" + first_stage = authority_runtime / ".render-stage.first" + first_stage.mkdir(parents=True) + + with Environment( + RIGOS_STATE_PATH=str(state), + RIGOS_RUNTIME_PATH=str(first_stage), + RIGOS_XMRIG_API_TOKEN_PATH=str(token_path), + RIGOS_RENDER_SKIP_CHOWN="1", + ): + first = load_source("rigos_runtime_render_publication_first", RENDERER) + self.assertEqual(first.API_TOKEN, token_path) + self.assertEqual(first.main(), 0) + + self.assertTrue(token_path.is_file()) + self.assertEqual(stat.S_IMODE(token_path.stat().st_mode), 0o600) + self.assertFalse((first_stage / "xmrig-api-token").exists()) + token_first = token_path.read_text(encoding="ascii").strip() + private_first = json.loads((first_stage / "xmrig.json").read_text(encoding="utf-8")) + status_first = json.loads( + (first_stage / "runtime-config-status.json").read_text(encoding="utf-8") + ) + self.assertEqual(private_first["http"]["access-token"], token_first) + self.assertEqual(status_first["http_api"]["token_path"], str(token_path)) + + shutil.rmtree(first_stage) + self.assertTrue(token_path.is_file()) + + second_stage = authority_runtime / ".render-stage.second" + second_stage.mkdir() + with Environment( + RIGOS_STATE_PATH=str(state), + RIGOS_RUNTIME_PATH=str(second_stage), + RIGOS_XMRIG_API_TOKEN_PATH=str(token_path), + RIGOS_RENDER_SKIP_CHOWN="1", + ): + second = load_source("rigos_runtime_render_publication_second", RENDERER) + self.assertEqual(second.main(), 0) + + private_second = json.loads((second_stage / "xmrig.json").read_text(encoding="utf-8")) + self.assertEqual(private_second["http"]["access-token"], token_first) + self.assertEqual(token_path.read_text(encoding="ascii").strip(), token_first) + shutil.rmtree(second_stage) + self.assertTrue(token_path.is_file()) + + def test_publisher_wires_authority_token_outside_stage(self): + publisher = PUBLISHER.read_text(encoding="utf-8") + self.assertIn('RIGOS_RUNTIME_PATH="$stage" \\\n', publisher) + self.assertIn( + 'RIGOS_XMRIG_API_TOKEN_PATH="$runtime/xmrig-api-token" \\\n', + publisher, + ) + self.assertIn(' "$renderer"\n', publisher) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 4bfc023625243a3d655c6a88a68fa41b8d97230b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:54:43 +0700 Subject: [PATCH 199/386] gate image build on token publication regression --- scripts/build-usb-image-entrypoint.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 380ab2c7..2aa57c81 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -27,8 +27,10 @@ python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health \ - ./scripts/test-miner-health-api.py + ./scripts/test-miner-health-api.py \ + ./scripts/test-runtime-token-publication.py python3 ./scripts/test-miner-health-api.py +python3 ./scripts/test-runtime-token-publication.py export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture From 898aece00fec7cf95e313011e676efad2a1952bd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:54:59 +0700 Subject: [PATCH 200/386] verify token publication in exact image --- scripts/verify-miner-observer-image.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index 0f14168c..ff11e89c 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -34,6 +34,7 @@ squashfs="$temporary/root-a/live/filesystem.squashfs" unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ usr/bin/python3 usr/bin/python3.11 \ usr/lib/rigos/rigos-runtime-render \ + usr/lib/rigos/rigos-runtime-publish \ usr/lib/rigos/rigos-miner-health \ etc/systemd/system/rigos-miner-health.service \ etc/systemd/system/rigos-miner-health.timer \ @@ -42,17 +43,19 @@ unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ root="$temporary/squash" renderer="$root/usr/lib/rigos/rigos-runtime-render" +publisher="$root/usr/lib/rigos/rigos-runtime-publish" observer="$root/usr/lib/rigos/rigos-miner-health" service="$root/etc/systemd/system/rigos-miner-health.service" timer="$root/etc/systemd/system/rigos-miner-health.timer" [[ -x "$root/usr/bin/python3" ]] || die 'Python runtime is missing' [[ -x "$renderer" ]] || die 'runtime renderer is missing or not executable' +[[ -x "$publisher" ]] || die 'runtime publisher is missing or not executable' [[ -x "$observer" ]] || die 'miner observer is missing or not executable' python3 -m py_compile "$renderer" "$observer" for required in \ - 'API_TOKEN = RUNTIME / "xmrig-api-token"' \ + 'API_TOKEN = Path(os.environ.get("RIGOS_XMRIG_API_TOKEN_PATH", str(RUNTIME / "xmrig-api-token")))' \ 'API_HOST = "127.0.0.1"' \ 'API_PORT = 18080' \ 'token = secrets.token_urlsafe(48)' \ @@ -67,6 +70,14 @@ if grep -Eq 'API_HOST = "(0\.0\.0\.0|::)"' "$renderer"; then die 'runtime API authority exposes a non-loopback bind' fi +for required in \ + 'RIGOS_RUNTIME_PATH="$stage" \' \ + 'RIGOS_XMRIG_API_TOKEN_PATH="$runtime/xmrig-api-token" \' \ + ' "$renderer"' +do + grep -Fqx "$required" "$publisher" || die "runtime token publication contract is missing: $required" +done + for required in \ 'connection.request(' \ '"/2/summary"' \ From c6a4c5092e36da5b2b9e9770b60cc98966c73034 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:59:19 +0700 Subject: [PATCH 201/386] derive pool connectivity from active connection evidence --- .../usr/lib/rigos/rigos-miner-health | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index 3fa6be32..4e7e873c 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -281,6 +281,17 @@ def summary_metrics(summary: dict) -> dict: pool = connection.get("pool") if not isinstance(pool, str) or not pool: pool = None + connection_ip = connection.get("ip") + if not isinstance(connection_ip, str) or not connection_ip: + connection_ip = None + connection_uptime_seconds = nonnegative_number(connection.get("uptime")) + connection_uptime_ms = nonnegative_number(connection.get("uptime_ms")) + pool_connected = pool is not None and ( + connection_ip is not None + or (connection_uptime_ms is not None and connection_uptime_ms > 0) + or (connection_uptime_seconds is not None and connection_uptime_seconds > 0) + ) + hugepages = summary.get("hugepages") hugepages_used = None hugepages_total = None @@ -295,9 +306,11 @@ def summary_metrics(summary: dict) -> dict: "hashrate_60s": rates[1], "hashrate_15m": rates[2], "hashrate_highest": nonnegative_number(hashrate.get("highest")), - "pool_connected": pool is not None, + "pool_connected": pool_connected, "pool": pool, - "connection_uptime_seconds": nonnegative_number(connection.get("uptime")), + "connection_ip": connection_ip, + "connection_uptime_seconds": connection_uptime_seconds, + "connection_uptime_ms": connection_uptime_ms, "connection_failures": nonnegative_number(connection.get("failures")), "pool_ping_ms": nonnegative_number(connection.get("ping")), "accepted_shares": int(accepted) if accepted is not None else None, From 9c49e027c01e4984cafc9156b938fb665d0b6d79 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:59:33 +0700 Subject: [PATCH 202/386] test stale pool connection classification --- scripts/test-miner-health-connection-state.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 scripts/test-miner-health-connection-state.py diff --git a/scripts/test-miner-health-connection-state.py b/scripts/test-miner-health-connection-state.py new file mode 100644 index 00000000..490c67a7 --- /dev/null +++ b/scripts/test-miner-health-connection-state.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +import importlib.machinery +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MINER_HEALTH = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health" + + +def load_source(name: str, path: Path): + loader = importlib.machinery.SourceFileLoader(name, str(path)) + spec = importlib.util.spec_from_loader(name, loader) + if spec is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def classify(module, metrics): + return module.classify( + {"ActiveState": "active", "SubState": "running", "MainPID": "123"}, + "S", + 600, + "r1", + "ready", + "r1", + metrics, + None, + "", + True, + ) + + +class MinerHealthConnectionStateTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.module = load_source("rigos_miner_health_connection_state", MINER_HEALTH) + + def test_stale_pool_name_after_disconnect_is_waiting_external(self): + metrics = self.module.summary_metrics({ + "uptime": 900, + "algo": "rx/0", + "hashrate": {"total": [0, 0, 0], "highest": 341.2}, + "connection": { + "pool": "pool.example:1234", + "ip": None, + "uptime": 0, + "uptime_ms": 0, + "accepted": 43, + "rejected": 0, + "failures": 3, + "ping": 0, + }, + "hugepages": [1168, 1168], + }) + + self.assertEqual(metrics["pool"], "pool.example:1234") + self.assertIsNone(metrics["connection_ip"]) + self.assertFalse(metrics["pool_connected"]) + self.assertEqual( + classify(self.module, metrics), + ("waiting_external", "pool_or_network_unavailable"), + ) + + def test_active_connection_without_hashrate_is_degraded(self): + metrics = self.module.summary_metrics({ + "uptime": 900, + "algo": "rx/0", + "hashrate": {"total": [0, 0, 0], "highest": 341.2}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 0, + "uptime_ms": 125, + "accepted": 43, + "rejected": 0, + "failures": 0, + "ping": 109, + }, + "hugepages": [1168, 1168], + }) + + self.assertTrue(metrics["pool_connected"]) + self.assertEqual(metrics["connection_ip"], "203.0.113.10") + self.assertEqual(metrics["connection_uptime_ms"], 125) + self.assertEqual(classify(self.module, metrics), ("degraded", "no_hashrate_from_api")) + + def test_seconds_uptime_remains_compatible_without_ip_or_milliseconds(self): + metrics = self.module.summary_metrics({ + "hashrate": {"total": [0, 0, 0]}, + "connection": { + "pool": "pool.example:1234", + "uptime": 5, + }, + }) + + self.assertTrue(metrics["pool_connected"]) + self.assertEqual(metrics["connection_uptime_seconds"], 5) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 885e427a67a84ecf333370f6f15a7ad8ddc7fc82 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:59:41 +0700 Subject: [PATCH 203/386] gate build on active pool classification regression --- scripts/build-usb-image-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 2aa57c81..128c6c90 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -28,8 +28,10 @@ python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health \ ./scripts/test-miner-health-api.py \ + ./scripts/test-miner-health-connection-state.py \ ./scripts/test-runtime-token-publication.py python3 ./scripts/test-miner-health-api.py +python3 ./scripts/test-miner-health-connection-state.py python3 ./scripts/test-runtime-token-publication.py export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target From b28e99b7e8dce83dd0040a65ad5dd1f4aa1a535a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 22:59:56 +0700 Subject: [PATCH 204/386] verify active pool evidence in exact image --- scripts/verify-miner-observer-image.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index ff11e89c..34b3a55c 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -83,6 +83,9 @@ for required in \ '"/2/summary"' \ '"Authorization": f"Bearer {token}"' \ 'API_MAX_BYTES = 256 * 1024' \ + 'connection_ip = connection.get("ip")' \ + 'connection_uptime_ms = nonnegative_number(connection.get("uptime_ms"))' \ + '"pool_connected": pool_connected' \ '"source": "xmrig_http_api" if metrics is not None else "journal_fallback"' \ 'return "degraded", "no_hashrate_from_api"' \ 'return "degraded", api_error or "miner_api_unavailable"' From d593f814dfb3976fb5e5c718bf518b7e57639f6c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:09:31 +0700 Subject: [PATCH 205/386] require active pool before api ready state --- .../usb/includes.chroot/usr/lib/rigos/rigos-miner-health | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index 4e7e873c..e8fa4902 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -351,11 +351,15 @@ def classify( api_metrics.get("hashrate_60s"), api_metrics.get("hashrate_15m"), ) - if any(isinstance(value, (int, float)) and value > 0 for value in rates): + hashrate_positive = any( + isinstance(value, (int, float)) and value > 0 for value in rates + ) + pool_connected = api_metrics.get("pool_connected") is True + if pool_connected and hashrate_positive: return "ready", None if active_for is not None and active_for < WARMUP_SECONDS: return "warming_up", None - if not api_metrics.get("pool_connected"): + if not pool_connected: return "waiting_external", "pool_or_network_unavailable" return "degraded", "no_hashrate_from_api" From 2ebb4d09f59e93fa88ced435640e637a64077d54 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:09:51 +0700 Subject: [PATCH 206/386] test disconnected historical hashrate classification --- scripts/test-miner-health-connection-state.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/scripts/test-miner-health-connection-state.py b/scripts/test-miner-health-connection-state.py index 490c67a7..2ddda9d7 100644 --- a/scripts/test-miner-health-connection-state.py +++ b/scripts/test-miner-health-connection-state.py @@ -64,6 +64,32 @@ def test_stale_pool_name_after_disconnect_is_waiting_external(self): ("waiting_external", "pool_or_network_unavailable"), ) + def test_disconnected_historical_hashrate_does_not_report_ready(self): + metrics = self.module.summary_metrics({ + "uptime": 900, + "algo": "rx/0", + "hashrate": {"total": [0, 340.8, 339.7], "highest": 341.2}, + "connection": { + "pool": "pool.example:1234", + "ip": None, + "uptime": 0, + "uptime_ms": 0, + "accepted": 43, + "rejected": 0, + "failures": 3, + "ping": 0, + }, + "hugepages": [1168, 1168], + }) + + self.assertGreater(metrics["hashrate_60s"], 0) + self.assertGreater(metrics["hashrate_15m"], 0) + self.assertFalse(metrics["pool_connected"]) + self.assertEqual( + classify(self.module, metrics), + ("waiting_external", "pool_or_network_unavailable"), + ) + def test_active_connection_without_hashrate_is_degraded(self): metrics = self.module.summary_metrics({ "uptime": 900, @@ -87,6 +113,27 @@ def test_active_connection_without_hashrate_is_degraded(self): self.assertEqual(metrics["connection_uptime_ms"], 125) self.assertEqual(classify(self.module, metrics), ("degraded", "no_hashrate_from_api")) + def test_active_connection_with_hashrate_is_ready(self): + metrics = self.module.summary_metrics({ + "uptime": 900, + "algo": "rx/0", + "hashrate": {"total": [341.2, 340.9, 340.7], "highest": 342.1}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 590, + "uptime_ms": 590125, + "accepted": 43, + "rejected": 0, + "failures": 0, + "ping": 109, + }, + "hugepages": [1168, 1168], + }) + + self.assertTrue(metrics["pool_connected"]) + self.assertEqual(classify(self.module, metrics), ("ready", None)) + def test_seconds_uptime_remains_compatible_without_ip_or_milliseconds(self): metrics = self.module.summary_metrics({ "hashrate": {"total": [0, 0, 0]}, From 13ce9f7c420305a1d90bc244ba693e481423195a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:10:17 +0700 Subject: [PATCH 207/386] behaviorally verify observer state truth in image --- scripts/verify-miner-observer-image.sh | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index 34b3a55c..a28e1d95 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -54,6 +54,67 @@ timer="$root/etc/systemd/system/rigos-miner-health.timer" [[ -x "$observer" ]] || die 'miner observer is missing or not executable' python3 -m py_compile "$renderer" "$observer" +python3 - "$observer" <<'PY' +import importlib.machinery +import importlib.util +import sys + +path = sys.argv[1] +loader = importlib.machinery.SourceFileLoader("rigos_image_observer", path) +spec = importlib.util.spec_from_loader("rigos_image_observer", loader) +if spec is None: + raise SystemExit("could not load extracted observer") +module = importlib.util.module_from_spec(spec) +loader.exec_module(module) + +properties = {"ActiveState": "active", "SubState": "running", "MainPID": "123"} + +def classify(summary): + metrics = module.summary_metrics(summary) + return metrics, module.classify( + properties, + "S", + 600, + "r1", + "ready", + "r1", + metrics, + None, + "", + True, + ) + +stale_metrics, stale_state = classify({ + "hashrate": {"total": [0, 340.8, 339.7], "highest": 341.2}, + "connection": { + "pool": "pool.example:1234", + "ip": None, + "uptime": 0, + "uptime_ms": 0, + "failures": 3, + }, +}) +if stale_metrics.get("pool_connected") is not False: + raise SystemExit("extracted observer trusts stale pool name") +if stale_state != ("waiting_external", "pool_or_network_unavailable"): + raise SystemExit(f"extracted observer misclassifies disconnected historical hashrate: {stale_state}") + +active_metrics, active_state = classify({ + "hashrate": {"total": [341.2, 340.9, 340.7], "highest": 342.1}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 590, + "uptime_ms": 590125, + "failures": 0, + }, +}) +if active_metrics.get("pool_connected") is not True: + raise SystemExit("extracted observer rejects active pool evidence") +if active_state != ("ready", None): + raise SystemExit(f"extracted observer rejects active hashrate: {active_state}") +PY + for required in \ 'API_TOKEN = Path(os.environ.get("RIGOS_XMRIG_API_TOKEN_PATH", str(RUNTIME / "xmrig-api-token")))' \ 'API_HOST = "127.0.0.1"' \ @@ -86,6 +147,7 @@ for required in \ 'connection_ip = connection.get("ip")' \ 'connection_uptime_ms = nonnegative_number(connection.get("uptime_ms"))' \ '"pool_connected": pool_connected' \ + 'if pool_connected and hashrate_positive:' \ '"source": "xmrig_http_api" if metrics is not None else "journal_fallback"' \ 'return "degraded", "no_hashrate_from_api"' \ 'return "degraded", api_error or "miner_api_unavailable"' From 2b67a29adde286060152541f676d299bcf2d00fd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:11:25 +0700 Subject: [PATCH 208/386] classify journal fallback from latest evidence --- .../usr/lib/rigos/rigos-miner-health | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index e8fa4902..cc776e5e 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -29,6 +29,10 @@ WARMUP_SECONDS = int(os.environ.get("RIGOS_MINER_WARMUP_SECONDS", "240")) MAX_JOURNAL_LINES = 500 TOKEN_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$") +READY_MARKERS = ( + "miner speed", + "accepted (", +) EXTERNAL_MARKERS = ( "connect error", "connection refused", @@ -165,6 +169,34 @@ def last_share_counts(text: str) -> tuple[int | None, int | None]: return int(accepted), int(rejected) +def latest_marker_index(text: str, markers: tuple[str, ...]) -> int: + return max((text.rfind(marker) for marker in markers), default=-1) + + +def latest_journal_signal(text: str) -> str | None: + lowered = text.lower() + ready_index = latest_marker_index(lowered, READY_MARKERS) + external_index = latest_marker_index(lowered, EXTERNAL_MARKERS) + if ready_index < 0 and external_index < 0: + return None + return "external_wait" if external_index > ready_index else "ready" + + +def journal_fallback_state( + journal: str, + active_for: int | None, + api_error: str | None, +) -> tuple[str, str | None]: + signal = latest_journal_signal(journal) + if signal == "ready": + return "ready", None + if signal == "external_wait": + return "waiting_external", "pool_or_network_unavailable" + if active_for is not None and active_for < WARMUP_SECONDS: + return "warming_up", None + return "degraded", api_error or "miner_api_unavailable" + + def expected_token_uid() -> int: return os.geteuid() if TEST_MODE else 0 @@ -365,14 +397,7 @@ def classify( if not journal_available: return "unknown", api_error or "journal_unavailable" - lowered = journal.lower() - if "miner speed" in lowered or "accepted (" in lowered: - return "ready", None - if any(marker in lowered for marker in EXTERNAL_MARKERS): - return "waiting_external", "pool_or_network_unavailable" - if active_for is not None and active_for < WARMUP_SECONDS: - return "warming_up", None - return "degraded", api_error or "miner_api_unavailable" + return journal_fallback_state(journal, active_for, api_error) def main() -> int: @@ -444,6 +469,7 @@ def main() -> int: "external_wait_marker": any( marker in lowered_journal for marker in EXTERNAL_MARKERS ), + "latest_journal_signal": latest_journal_signal(journal), "accepted_shares": accepted, "rejected_shares": rejected, }, From f3e2c550e497fe3d485766ba036fbbab06c30a19 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:11:36 +0700 Subject: [PATCH 209/386] test ordered journal fallback evidence --- scripts/test-miner-health-journal-fallback.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 scripts/test-miner-health-journal-fallback.py diff --git a/scripts/test-miner-health-journal-fallback.py b/scripts/test-miner-health-journal-fallback.py new file mode 100644 index 00000000..ac4595d4 --- /dev/null +++ b/scripts/test-miner-health-journal-fallback.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +import importlib.machinery +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MINER_HEALTH = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health" + + +def load_source(name: str, path: Path): + loader = importlib.machinery.SourceFileLoader(name, str(path)) + spec = importlib.util.spec_from_loader(name, loader) + if spec is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +class MinerHealthJournalFallbackTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.module = load_source("rigos_miner_health_journal_fallback", MINER_HEALTH) + + def test_newer_external_error_overrides_old_ready_evidence(self): + journal = "\n".join([ + "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", + "cpu accepted (43/0) diff 10000", + "net connect error: operation timed out", + ]) + + self.assertEqual(self.module.latest_journal_signal(journal), "external_wait") + self.assertEqual( + self.module.journal_fallback_state(journal, 600, "api_unavailable"), + ("waiting_external", "pool_or_network_unavailable"), + ) + + def test_newer_ready_evidence_overrides_old_external_error(self): + journal = "\n".join([ + "net connect error: operation timed out", + "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", + "cpu accepted (44/0) diff 10000", + ]) + + self.assertEqual(self.module.latest_journal_signal(journal), "ready") + self.assertEqual( + self.module.journal_fallback_state(journal, 600, "api_unavailable"), + ("ready", None), + ) + + def test_no_signal_during_warmup_is_warming_up(self): + self.assertIsNone(self.module.latest_journal_signal("starting miner")) + self.assertEqual( + self.module.journal_fallback_state("starting miner", 30, "api_unavailable"), + ("warming_up", None), + ) + + def test_no_signal_after_warmup_is_degraded(self): + self.assertEqual( + self.module.journal_fallback_state("miner remains silent", 600, "api_unavailable"), + ("degraded", "api_unavailable"), + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 3438768ebfd72d741e3671582ea6219809fdb7d9 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:11:45 +0700 Subject: [PATCH 210/386] gate build on ordered journal evidence regression --- scripts/build-usb-image-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 128c6c90..bfcf086f 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -29,9 +29,11 @@ python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health \ ./scripts/test-miner-health-api.py \ ./scripts/test-miner-health-connection-state.py \ + ./scripts/test-miner-health-journal-fallback.py \ ./scripts/test-runtime-token-publication.py python3 ./scripts/test-miner-health-api.py python3 ./scripts/test-miner-health-connection-state.py +python3 ./scripts/test-miner-health-journal-fallback.py python3 ./scripts/test-runtime-token-publication.py export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target From 114bdcde8c0dbad201a1ed06976569b180156e91 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:12:11 +0700 Subject: [PATCH 211/386] verify ordered journal fallback in exact image --- scripts/verify-miner-observer-image.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index a28e1d95..3285fcd9 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -113,6 +113,24 @@ if active_metrics.get("pool_connected") is not True: raise SystemExit("extracted observer rejects active pool evidence") if active_state != ("ready", None): raise SystemExit(f"extracted observer rejects active hashrate: {active_state}") + +old_ready_new_external = "\n".join([ + "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", + "cpu accepted (43/0) diff 10000", + "net connect error: operation timed out", +]) +state = module.journal_fallback_state(old_ready_new_external, 600, "api_unavailable") +if state != ("waiting_external", "pool_or_network_unavailable"): + raise SystemExit(f"extracted observer trusts stale journal ready evidence: {state}") + +old_external_new_ready = "\n".join([ + "net connect error: operation timed out", + "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", + "cpu accepted (44/0) diff 10000", +]) +state = module.journal_fallback_state(old_external_new_ready, 600, "api_unavailable") +if state != ("ready", None): + raise SystemExit(f"extracted observer ignores newer journal ready evidence: {state}") PY for required in \ @@ -148,6 +166,8 @@ for required in \ 'connection_uptime_ms = nonnegative_number(connection.get("uptime_ms"))' \ '"pool_connected": pool_connected' \ 'if pool_connected and hashrate_positive:' \ + 'def latest_journal_signal(text: str) -> str | None:' \ + '"latest_journal_signal": latest_journal_signal(journal)' \ '"source": "xmrig_http_api" if metrics is not None else "journal_fallback"' \ 'return "degraded", "no_hashrate_from_api"' \ 'return "degraded", api_error or "miner_api_unavailable"' From 432f9bc824cab9cbb32fb930e67b9772450f8c8b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:13:48 +0700 Subject: [PATCH 212/386] use current 10 second hashrate as ready authority --- .../usr/lib/rigos/rigos-miner-health | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index cc776e5e..dfbb50c4 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -378,22 +378,22 @@ def classify( return "blocked", "runtime_revision_mismatch" if api_metrics is not None: - rates = ( - api_metrics.get("hashrate_10s"), - api_metrics.get("hashrate_60s"), - api_metrics.get("hashrate_15m"), - ) - hashrate_positive = any( - isinstance(value, (int, float)) and value > 0 for value in rates + current_hashrate = api_metrics.get("hashrate_10s") + current_hashrate_positive = ( + isinstance(current_hashrate, (int, float)) + and not isinstance(current_hashrate, bool) + and current_hashrate > 0 ) pool_connected = api_metrics.get("pool_connected") is True - if pool_connected and hashrate_positive: + if pool_connected and current_hashrate_positive: return "ready", None if active_for is not None and active_for < WARMUP_SECONDS: return "warming_up", None if not pool_connected: return "waiting_external", "pool_or_network_unavailable" - return "degraded", "no_hashrate_from_api" + if current_hashrate is None: + return "degraded", "current_hashrate_unavailable" + return "degraded", "no_current_hashrate_from_api" if not journal_available: return "unknown", api_error or "journal_unavailable" From 715a08ef003f8b86b43536df0bbf907d6f90aff1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:14:11 +0700 Subject: [PATCH 213/386] test current hashrate readiness authority --- scripts/test-miner-health-connection-state.py | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/scripts/test-miner-health-connection-state.py b/scripts/test-miner-health-connection-state.py index 2ddda9d7..564db518 100644 --- a/scripts/test-miner-health-connection-state.py +++ b/scripts/test-miner-health-connection-state.py @@ -111,7 +111,59 @@ def test_active_connection_without_hashrate_is_degraded(self): self.assertTrue(metrics["pool_connected"]) self.assertEqual(metrics["connection_ip"], "203.0.113.10") self.assertEqual(metrics["connection_uptime_ms"], 125) - self.assertEqual(classify(self.module, metrics), ("degraded", "no_hashrate_from_api")) + self.assertEqual( + classify(self.module, metrics), + ("degraded", "no_current_hashrate_from_api"), + ) + + def test_active_historical_hashrate_without_current_rate_is_degraded(self): + metrics = self.module.summary_metrics({ + "uptime": 900, + "algo": "rx/0", + "hashrate": {"total": [0, 340.9, 340.7], "highest": 342.1}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 590, + "uptime_ms": 590125, + "accepted": 43, + "rejected": 0, + "failures": 0, + "ping": 109, + }, + "hugepages": [1168, 1168], + }) + + self.assertEqual(metrics["hashrate_10s"], 0) + self.assertGreater(metrics["hashrate_60s"], 0) + self.assertEqual( + classify(self.module, metrics), + ("degraded", "no_current_hashrate_from_api"), + ) + + def test_active_connection_with_unavailable_current_rate_is_degraded(self): + metrics = self.module.summary_metrics({ + "uptime": 900, + "algo": "rx/0", + "hashrate": {"total": [None, 340.9, 340.7], "highest": 342.1}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 590, + "uptime_ms": 590125, + "accepted": 43, + "rejected": 0, + "failures": 0, + "ping": 109, + }, + "hugepages": [1168, 1168], + }) + + self.assertIsNone(metrics["hashrate_10s"]) + self.assertEqual( + classify(self.module, metrics), + ("degraded", "current_hashrate_unavailable"), + ) def test_active_connection_with_hashrate_is_ready(self): metrics = self.module.summary_metrics({ From 24f65b4de3bc933dd63f0378384cb311b2ccb09e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:14:37 +0700 Subject: [PATCH 214/386] verify current hashrate authority in exact image --- scripts/verify-miner-observer-image.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index 3285fcd9..09ce4d50 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -99,6 +99,21 @@ if stale_metrics.get("pool_connected") is not False: if stale_state != ("waiting_external", "pool_or_network_unavailable"): raise SystemExit(f"extracted observer misclassifies disconnected historical hashrate: {stale_state}") +historical_metrics, historical_state = classify({ + "hashrate": {"total": [0, 340.9, 340.7], "highest": 342.1}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 590, + "uptime_ms": 590125, + "failures": 0, + }, +}) +if historical_metrics.get("pool_connected") is not True: + raise SystemExit("extracted observer rejects active pool evidence") +if historical_state != ("degraded", "no_current_hashrate_from_api"): + raise SystemExit(f"extracted observer trusts historical hashrate as current: {historical_state}") + active_metrics, active_state = classify({ "hashrate": {"total": [341.2, 340.9, 340.7], "highest": 342.1}, "connection": { @@ -165,11 +180,13 @@ for required in \ 'connection_ip = connection.get("ip")' \ 'connection_uptime_ms = nonnegative_number(connection.get("uptime_ms"))' \ '"pool_connected": pool_connected' \ - 'if pool_connected and hashrate_positive:' \ + 'current_hashrate = api_metrics.get("hashrate_10s")' \ + 'if pool_connected and current_hashrate_positive:' \ + 'return "degraded", "current_hashrate_unavailable"' \ + 'return "degraded", "no_current_hashrate_from_api"' \ 'def latest_journal_signal(text: str) -> str | None:' \ '"latest_journal_signal": latest_journal_signal(journal)' \ '"source": "xmrig_http_api" if metrics is not None else "journal_fallback"' \ - 'return "degraded", "no_hashrate_from_api"' \ 'return "degraded", api_error or "miner_api_unavailable"' do grep -Fq "$required" "$observer" || die "miner observer API contract is missing: $required" From ba47ba1feb3bab5d09e5519f736fea4ec8c4694c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:15:44 +0700 Subject: [PATCH 215/386] fail closed on api authority errors --- build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index dfbb50c4..c64ae135 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -395,6 +395,8 @@ def classify( return "degraded", "current_hashrate_unavailable" return "degraded", "no_current_hashrate_from_api" + if api_error not in (None, "api_unavailable"): + return "degraded", api_error if not journal_available: return "unknown", api_error or "journal_unavailable" return journal_fallback_state(journal, active_for, api_error) From 56ee101b40830135c9ffb4a9903031393f4573ce Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:15:57 +0700 Subject: [PATCH 216/386] test fail closed api authority errors --- .../test-miner-health-api-authority-errors.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 scripts/test-miner-health-api-authority-errors.py diff --git a/scripts/test-miner-health-api-authority-errors.py b/scripts/test-miner-health-api-authority-errors.py new file mode 100644 index 00000000..1db3865c --- /dev/null +++ b/scripts/test-miner-health-api-authority-errors.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +import importlib.machinery +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MINER_HEALTH = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health" +PROPERTIES = {"ActiveState": "active", "SubState": "running", "MainPID": "123"} + + +def load_source(name: str, path: Path): + loader = importlib.machinery.SourceFileLoader(name, str(path)) + spec = importlib.util.spec_from_loader(name, loader) + if spec is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def classify(module, api_error: str | None, journal: str, journal_available: bool = True): + return module.classify( + PROPERTIES, + "S", + 600, + "r1", + "ready", + "r1", + None, + api_error, + journal, + journal_available, + ) + + +class MinerHealthApiAuthorityErrorTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.module = load_source("rigos_miner_health_api_authority_errors", MINER_HEALTH) + + def test_missing_token_cannot_be_hidden_by_ready_journal(self): + state = classify( + self.module, + "api_token_missing", + "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", + ) + self.assertEqual(state, ("degraded", "api_token_missing")) + + def test_authentication_failure_cannot_be_hidden_by_accepted_share(self): + state = classify( + self.module, + "api_http_status_401", + "cpu accepted (43/0) diff 10000", + ) + self.assertEqual(state, ("degraded", "api_http_status_401")) + + def test_invalid_api_response_cannot_be_hidden_by_journal(self): + state = classify( + self.module, + "api_response_not_object", + "cpu accepted (43/0) diff 10000", + ) + self.assertEqual(state, ("degraded", "api_response_not_object")) + + def test_transient_api_unavailable_still_allows_journal_fallback(self): + state = classify( + self.module, + "api_unavailable", + "cpu accepted (43/0) diff 10000", + ) + self.assertEqual(state, ("ready", None)) + + def test_transient_api_unavailable_without_journal_is_unknown(self): + state = classify(self.module, "api_unavailable", "", journal_available=False) + self.assertEqual(state, ("unknown", "api_unavailable")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From a5ad30fa776494bfacdc0632dc1eb64df9627325 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:16:11 +0700 Subject: [PATCH 217/386] gate build on api authority error regression --- scripts/build-usb-image-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index bfcf086f..4d35ad2d 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -28,10 +28,12 @@ python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health \ ./scripts/test-miner-health-api.py \ + ./scripts/test-miner-health-api-authority-errors.py \ ./scripts/test-miner-health-connection-state.py \ ./scripts/test-miner-health-journal-fallback.py \ ./scripts/test-runtime-token-publication.py python3 ./scripts/test-miner-health-api.py +python3 ./scripts/test-miner-health-api-authority-errors.py python3 ./scripts/test-miner-health-connection-state.py python3 ./scripts/test-miner-health-journal-fallback.py python3 ./scripts/test-runtime-token-publication.py From b50be0db16fdc35228140b2d98ee5b89212f0cc7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:16:38 +0700 Subject: [PATCH 218/386] verify api authority errors fail closed in image --- scripts/verify-miner-observer-image.sh | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index 09ce4d50..655a27c1 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -146,6 +146,36 @@ old_external_new_ready = "\n".join([ state = module.journal_fallback_state(old_external_new_ready, 600, "api_unavailable") if state != ("ready", None): raise SystemExit(f"extracted observer ignores newer journal ready evidence: {state}") + +authority_error_state = module.classify( + properties, + "S", + 600, + "r1", + "ready", + "r1", + None, + "api_token_missing", + "cpu accepted (43/0) diff 10000", + True, +) +if authority_error_state != ("degraded", "api_token_missing"): + raise SystemExit(f"extracted observer hides API authority failure: {authority_error_state}") + +transient_state = module.classify( + properties, + "S", + 600, + "r1", + "ready", + "r1", + None, + "api_unavailable", + "cpu accepted (43/0) diff 10000", + True, +) +if transient_state != ("ready", None): + raise SystemExit(f"extracted observer rejects bounded transient fallback: {transient_state}") PY for required in \ @@ -184,6 +214,7 @@ for required in \ 'if pool_connected and current_hashrate_positive:' \ 'return "degraded", "current_hashrate_unavailable"' \ 'return "degraded", "no_current_hashrate_from_api"' \ + 'if api_error not in (None, "api_unavailable"):' \ 'def latest_journal_signal(text: str) -> str | None:' \ '"latest_journal_signal": latest_journal_signal(journal)' \ '"source": "xmrig_http_api" if metrics is not None else "journal_fallback"' \ From 14ed885a8b65a87dbbbf2ceeedd0974361dfa2eb Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:17:58 +0700 Subject: [PATCH 219/386] add authoritative miner observer source gate --- .../tests/miner_observer_authority.rs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 crates/rigos-config/tests/miner_observer_authority.rs diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs new file mode 100644 index 00000000..c037518f --- /dev/null +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -0,0 +1,116 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn repo_path(path: &str) -> PathBuf { + repo_root().join(path) +} + +#[cfg(unix)] +fn run_python(script: &str) { + let python = env::var("RIGOS_PYTHON").unwrap_or_else(|_| "python3".to_string()); + let status = Command::new(&python) + .arg(repo_path(script)) + .current_dir(repo_root()) + .status() + .unwrap_or_else(|error| panic!("failed to execute {script} with {python}: {error}")); + assert!(status.success(), "observer source regression failed: {script}"); +} + +#[cfg(unix)] +#[test] +fn authenticated_miner_observer_behavioral_source_gate() { + for script in [ + "scripts/test-miner-health-api.py", + "scripts/test-miner-health-api-authority-errors.py", + "scripts/test-miner-health-connection-state.py", + "scripts/test-miner-health-journal-fallback.py", + "scripts/test-runtime-token-publication.py", + ] { + run_python(script); + } +} + +#[test] +fn observer_authority_is_wired_into_build_and_exact_image_gates() { + let entrypoint = fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")) + .expect("read performance image entrypoint"); + let image_verifier = fs::read_to_string(repo_path("scripts/verify-miner-observer-image.sh")) + .expect("read observer image verifier"); + let observer = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health", + )) + .expect("read miner observer"); + let renderer = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-render", + )) + .expect("read runtime renderer"); + let publisher = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish", + )) + .expect("read runtime publisher"); + + for script in [ + "test-miner-health-api.py", + "test-miner-health-api-authority-errors.py", + "test-miner-health-connection-state.py", + "test-miner-health-journal-fallback.py", + "test-runtime-token-publication.py", + ] { + assert!( + entrypoint.contains(script), + "performance image entrypoint does not run {script}" + ); + } + + for contract in [ + "RIGOS_XMRIG_API_TOKEN_PATH", + "hashrate_10s", + "current_hashrate_unavailable", + "no_current_hashrate_from_api", + "latest_journal_signal", + "api_error not in (None, \"api_unavailable\")", + ] { + assert!(observer.contains(contract), "observer contract missing: {contract}"); + } + + assert!(renderer.contains("secrets.token_urlsafe(48)")); + assert!(renderer.contains("127.0.0.1")); + assert!(renderer.contains("restricted\": True")); + assert!(publisher.contains("RIGOS_XMRIG_API_TOKEN_PATH=\"$runtime/xmrig-api-token\"")); + + for contract in [ + "extracted observer misclassifies disconnected historical hashrate", + "extracted observer trusts historical hashrate as current", + "extracted observer trusts stale journal ready evidence", + "extracted observer hides API authority failure", + ] { + assert!( + image_verifier.contains(contract), + "exact-image behavioral contract missing: {contract}" + ); + } +} + +#[test] +fn observer_test_files_are_regular_repository_files() { + for path in [ + "scripts/test-miner-health-api.py", + "scripts/test-miner-health-api-authority-errors.py", + "scripts/test-miner-health-connection-state.py", + "scripts/test-miner-health-journal-fallback.py", + "scripts/test-runtime-token-publication.py", + ] { + let metadata = fs::metadata(repo_path(path)).unwrap_or_else(|error| { + panic!("observer test file is unavailable: {path}: {error}") + }); + assert!(metadata.is_file(), "observer test path is not a file: {path}"); + } + + assert!(Path::new(&repo_path("scripts/verify-miner-observer-image.sh")).is_file()); +} From 0f952cf04a41eb1957aa56e3f8b7286b45ffdb0d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:18:23 +0700 Subject: [PATCH 220/386] keep observer authority test clippy clean cross platform --- crates/rigos-config/tests/miner_observer_authority.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index c037518f..27e5eab2 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -1,6 +1,8 @@ +#[cfg(unix)] use std::env; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +#[cfg(unix)] use std::process::Command; fn repo_root() -> PathBuf { @@ -112,5 +114,5 @@ fn observer_test_files_are_regular_repository_files() { assert!(metadata.is_file(), "observer test path is not a file: {path}"); } - assert!(Path::new(&repo_path("scripts/verify-miner-observer-image.sh")).is_file()); + assert!(repo_path("scripts/verify-miner-observer-image.sh").is_file()); } From 5d3c1c9dad6c71b8e36705ac14308def39bfe469 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:18:36 +0700 Subject: [PATCH 221/386] run authoritative observer gate before image build --- scripts/build-usb-image-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 4d35ad2d..5625acae 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -39,6 +39,7 @@ python3 ./scripts/test-miner-health-journal-fallback.py python3 ./scripts/test-runtime-token-publication.py export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target +cargo test --locked -p rigos-config --test miner_observer_authority -- --nocapture cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture From 3a7a746ed926e7dce235923f321344abb12b53dd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:18:51 +0700 Subject: [PATCH 222/386] verify observer source gate is build mandatory --- crates/rigos-config/tests/miner_observer_authority.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index 27e5eab2..74d39cbd 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -69,6 +69,7 @@ fn observer_authority_is_wired_into_build_and_exact_image_gates() { "performance image entrypoint does not run {script}" ); } + assert!(entrypoint.contains("--test miner_observer_authority")); for contract in [ "RIGOS_XMRIG_API_TOKEN_PATH", From f88460a4f8cc8e5cb8134c2ccd40e0947ff29060 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:27:07 +0700 Subject: [PATCH 223/386] reject fractional api counters --- .../usr/lib/rigos/rigos-miner-health | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index c64ae135..57953a6e 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -284,6 +284,12 @@ def nonnegative_number(value: object) -> int | float | None: return value +def nonnegative_integer(value: object) -> int | None: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + return None + return value + + def summary_metrics(summary: dict) -> dict: hashrate = summary.get("hashrate") if not isinstance(hashrate, dict): @@ -301,12 +307,12 @@ def summary_metrics(summary: dict) -> dict: if not isinstance(results, dict): results = {} - accepted = nonnegative_number(connection.get("accepted")) - rejected = nonnegative_number(connection.get("rejected")) + accepted = nonnegative_integer(connection.get("accepted")) + rejected = nonnegative_integer(connection.get("rejected")) if accepted is None: - accepted = nonnegative_number(results.get("shares_good")) + accepted = nonnegative_integer(results.get("shares_good")) if rejected is None: - total = nonnegative_number(results.get("shares_total")) + total = nonnegative_integer(results.get("shares_total")) if total is not None and accepted is not None and total >= accepted: rejected = total - accepted @@ -328,8 +334,8 @@ def summary_metrics(summary: dict) -> dict: hugepages_used = None hugepages_total = None if isinstance(hugepages, list) and len(hugepages) >= 2: - hugepages_used = nonnegative_number(hugepages[0]) - hugepages_total = nonnegative_number(hugepages[1]) + hugepages_used = nonnegative_integer(hugepages[0]) + hugepages_total = nonnegative_integer(hugepages[1]) return { "uptime_seconds": nonnegative_number(summary.get("uptime")), @@ -343,12 +349,12 @@ def summary_metrics(summary: dict) -> dict: "connection_ip": connection_ip, "connection_uptime_seconds": connection_uptime_seconds, "connection_uptime_ms": connection_uptime_ms, - "connection_failures": nonnegative_number(connection.get("failures")), - "pool_ping_ms": nonnegative_number(connection.get("ping")), - "accepted_shares": int(accepted) if accepted is not None else None, - "rejected_shares": int(rejected) if rejected is not None else None, - "hugepages_used": int(hugepages_used) if hugepages_used is not None else None, - "hugepages_total": int(hugepages_total) if hugepages_total is not None else None, + "connection_failures": nonnegative_integer(connection.get("failures")), + "pool_ping_ms": nonnegative_integer(connection.get("ping")), + "accepted_shares": accepted, + "rejected_shares": rejected, + "hugepages_used": hugepages_used, + "hugepages_total": hugepages_total, } From 1155a5f52eb783b1a425e14876e657b6759ff588 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:27:18 +0700 Subject: [PATCH 224/386] test strict xmrig api counter schema --- scripts/test-miner-health-api-schema.py | 87 +++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 scripts/test-miner-health-api-schema.py diff --git a/scripts/test-miner-health-api-schema.py b/scripts/test-miner-health-api-schema.py new file mode 100644 index 00000000..66cdc171 --- /dev/null +++ b/scripts/test-miner-health-api-schema.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import importlib.machinery +import importlib.util +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MINER_HEALTH = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health" + + +def load_source(name: str, path: Path): + loader = importlib.machinery.SourceFileLoader(name, str(path)) + spec = importlib.util.spec_from_loader(name, loader) + if spec is None: + raise RuntimeError(f"could not load {path}") + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +class MinerHealthApiSchemaTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.module = load_source("rigos_miner_health_api_schema", MINER_HEALTH) + + def test_fractional_counters_are_rejected_not_truncated(self): + metrics = self.module.summary_metrics({ + "hashrate": {"total": [341.2, 340.9, 340.7]}, + "connection": { + "pool": "pool.example:1234", + "ip": "203.0.113.10", + "uptime": 10, + "accepted": 43.9, + "rejected": 1.2, + "failures": 2.5, + "ping": 109.7, + }, + "results": { + "shares_good": 42.8, + "shares_total": 44.1, + }, + "hugepages": [1168.5, 1169.5], + }) + + self.assertIsNone(metrics["accepted_shares"]) + self.assertIsNone(metrics["rejected_shares"]) + self.assertIsNone(metrics["connection_failures"]) + self.assertIsNone(metrics["pool_ping_ms"]) + self.assertIsNone(metrics["hugepages_used"]) + self.assertIsNone(metrics["hugepages_total"]) + + def test_boolean_counters_are_rejected(self): + metrics = self.module.summary_metrics({ + "connection": {"accepted": True, "rejected": False}, + "hugepages": [True, False], + }) + + self.assertIsNone(metrics["accepted_shares"]) + self.assertIsNone(metrics["rejected_shares"]) + self.assertIsNone(metrics["hugepages_used"]) + self.assertIsNone(metrics["hugepages_total"]) + + def test_integer_result_fallback_remains_supported(self): + metrics = self.module.summary_metrics({ + "results": {"shares_good": 43, "shares_total": 45}, + "hugepages": [1168, 1168], + "connection": {"failures": 2, "ping": 109}, + }) + + self.assertEqual(metrics["accepted_shares"], 43) + self.assertEqual(metrics["rejected_shares"], 2) + self.assertEqual(metrics["connection_failures"], 2) + self.assertEqual(metrics["pool_ping_ms"], 109) + self.assertEqual(metrics["hugepages_used"], 1168) + self.assertEqual(metrics["hugepages_total"], 1168) + + def test_total_below_accepted_does_not_produce_negative_rejects(self): + metrics = self.module.summary_metrics({ + "results": {"shares_good": 45, "shares_total": 43}, + }) + + self.assertEqual(metrics["accepted_shares"], 45) + self.assertIsNone(metrics["rejected_shares"]) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 45a411e70e2366afa702f94c145e5cce7048c72c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:27:32 +0700 Subject: [PATCH 225/386] gate build on strict api schema regression --- scripts/build-usb-image-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 5625acae..78e1ef70 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -29,11 +29,13 @@ python3 -m py_compile \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health \ ./scripts/test-miner-health-api.py \ ./scripts/test-miner-health-api-authority-errors.py \ + ./scripts/test-miner-health-api-schema.py \ ./scripts/test-miner-health-connection-state.py \ ./scripts/test-miner-health-journal-fallback.py \ ./scripts/test-runtime-token-publication.py python3 ./scripts/test-miner-health-api.py python3 ./scripts/test-miner-health-api-authority-errors.py +python3 ./scripts/test-miner-health-api-schema.py python3 ./scripts/test-miner-health-connection-state.py python3 ./scripts/test-miner-health-journal-fallback.py python3 ./scripts/test-runtime-token-publication.py From ad9f1c2adc5eab3d8363442a8d008989d168f435 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:27:50 +0700 Subject: [PATCH 226/386] include strict api schema in observer authority --- crates/rigos-config/tests/miner_observer_authority.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index 74d39cbd..a1b03204 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -30,6 +30,7 @@ fn authenticated_miner_observer_behavioral_source_gate() { for script in [ "scripts/test-miner-health-api.py", "scripts/test-miner-health-api-authority-errors.py", + "scripts/test-miner-health-api-schema.py", "scripts/test-miner-health-connection-state.py", "scripts/test-miner-health-journal-fallback.py", "scripts/test-runtime-token-publication.py", @@ -60,6 +61,7 @@ fn observer_authority_is_wired_into_build_and_exact_image_gates() { for script in [ "test-miner-health-api.py", "test-miner-health-api-authority-errors.py", + "test-miner-health-api-schema.py", "test-miner-health-connection-state.py", "test-miner-health-journal-fallback.py", "test-runtime-token-publication.py", @@ -74,6 +76,7 @@ fn observer_authority_is_wired_into_build_and_exact_image_gates() { for contract in [ "RIGOS_XMRIG_API_TOKEN_PATH", "hashrate_10s", + "nonnegative_integer", "current_hashrate_unavailable", "no_current_hashrate_from_api", "latest_journal_signal", @@ -92,6 +95,7 @@ fn observer_authority_is_wired_into_build_and_exact_image_gates() { "extracted observer trusts historical hashrate as current", "extracted observer trusts stale journal ready evidence", "extracted observer hides API authority failure", + "extracted observer truncates fractional counters", ] { assert!( image_verifier.contains(contract), @@ -105,6 +109,7 @@ fn observer_test_files_are_regular_repository_files() { for path in [ "scripts/test-miner-health-api.py", "scripts/test-miner-health-api-authority-errors.py", + "scripts/test-miner-health-api-schema.py", "scripts/test-miner-health-connection-state.py", "scripts/test-miner-health-journal-fallback.py", "scripts/test-runtime-token-publication.py", From 06caba5d8197c10ec7abe7faec4a2c09a7b693f9 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:28:23 +0700 Subject: [PATCH 227/386] verify strict api counter schema in image --- scripts/verify-miner-observer-image.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index 655a27c1..85226d5f 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -129,6 +129,30 @@ if active_metrics.get("pool_connected") is not True: if active_state != ("ready", None): raise SystemExit(f"extracted observer rejects active hashrate: {active_state}") +schema_metrics = module.summary_metrics({ + "connection": { + "accepted": 43.9, + "rejected": 1.2, + "failures": 2.5, + "ping": 109.7, + }, + "results": { + "shares_good": 42.8, + "shares_total": 44.1, + }, + "hugepages": [1168.5, 1169.5], +}) +for key in ( + "accepted_shares", + "rejected_shares", + "connection_failures", + "pool_ping_ms", + "hugepages_used", + "hugepages_total", +): + if schema_metrics.get(key) is not None: + raise SystemExit(f"extracted observer truncates fractional counters: {key}") + old_ready_new_external = "\n".join([ "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", "cpu accepted (43/0) diff 10000", @@ -207,6 +231,7 @@ for required in \ '"/2/summary"' \ '"Authorization": f"Bearer {token}"' \ 'API_MAX_BYTES = 256 * 1024' \ + 'def nonnegative_integer(value: object) -> int | None:' \ 'connection_ip = connection.get("ip")' \ 'connection_uptime_ms = nonnegative_number(connection.get("uptime_ms"))' \ '"pool_connected": pool_connected' \ From 13d44045be23e90d841536fd1821c3c85fd0f2a7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:30:28 +0700 Subject: [PATCH 228/386] require integer uptime evidence --- build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health index 57953a6e..b8cf8733 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health @@ -322,8 +322,8 @@ def summary_metrics(summary: dict) -> dict: connection_ip = connection.get("ip") if not isinstance(connection_ip, str) or not connection_ip: connection_ip = None - connection_uptime_seconds = nonnegative_number(connection.get("uptime")) - connection_uptime_ms = nonnegative_number(connection.get("uptime_ms")) + connection_uptime_seconds = nonnegative_integer(connection.get("uptime")) + connection_uptime_ms = nonnegative_integer(connection.get("uptime_ms")) pool_connected = pool is not None and ( connection_ip is not None or (connection_uptime_ms is not None and connection_uptime_ms > 0) @@ -338,7 +338,7 @@ def summary_metrics(summary: dict) -> dict: hugepages_total = nonnegative_integer(hugepages[1]) return { - "uptime_seconds": nonnegative_number(summary.get("uptime")), + "uptime_seconds": nonnegative_integer(summary.get("uptime")), "algorithm": summary.get("algo") if isinstance(summary.get("algo"), str) else None, "hashrate_10s": rates[0], "hashrate_60s": rates[1], From bdb2c28e0937b11f6365b6d270a9b920a25243e4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:30:51 +0700 Subject: [PATCH 229/386] test strict uptime schema --- scripts/test-miner-health-api-schema.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/test-miner-health-api-schema.py b/scripts/test-miner-health-api-schema.py index 66cdc171..297f08c7 100644 --- a/scripts/test-miner-health-api-schema.py +++ b/scripts/test-miner-health-api-schema.py @@ -49,6 +49,22 @@ def test_fractional_counters_are_rejected_not_truncated(self): self.assertIsNone(metrics["hugepages_used"]) self.assertIsNone(metrics["hugepages_total"]) + def test_fractional_uptime_cannot_assert_active_connection(self): + metrics = self.module.summary_metrics({ + "uptime": 900.5, + "connection": { + "pool": "pool.example:1234", + "ip": None, + "uptime": 0.1, + "uptime_ms": 0.5, + }, + }) + + self.assertIsNone(metrics["uptime_seconds"]) + self.assertIsNone(metrics["connection_uptime_seconds"]) + self.assertIsNone(metrics["connection_uptime_ms"]) + self.assertFalse(metrics["pool_connected"]) + def test_boolean_counters_are_rejected(self): metrics = self.module.summary_metrics({ "connection": {"accepted": True, "rejected": False}, From 87839f0dbcaefabf1edf119b916639c7223e272d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:31:43 +0700 Subject: [PATCH 230/386] verify strict uptime authority in image --- scripts/verify-miner-observer-image.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/verify-miner-observer-image.sh b/scripts/verify-miner-observer-image.sh index 85226d5f..ad941e79 100644 --- a/scripts/verify-miner-observer-image.sh +++ b/scripts/verify-miner-observer-image.sh @@ -130,7 +130,12 @@ if active_state != ("ready", None): raise SystemExit(f"extracted observer rejects active hashrate: {active_state}") schema_metrics = module.summary_metrics({ + "uptime": 900.5, "connection": { + "pool": "pool.example:1234", + "ip": None, + "uptime": 0.1, + "uptime_ms": 0.5, "accepted": 43.9, "rejected": 1.2, "failures": 2.5, @@ -143,6 +148,9 @@ schema_metrics = module.summary_metrics({ "hugepages": [1168.5, 1169.5], }) for key in ( + "uptime_seconds", + "connection_uptime_seconds", + "connection_uptime_ms", "accepted_shares", "rejected_shares", "connection_failures", @@ -152,6 +160,8 @@ for key in ( ): if schema_metrics.get(key) is not None: raise SystemExit(f"extracted observer truncates fractional counters: {key}") +if schema_metrics.get("pool_connected") is not False: + raise SystemExit("extracted observer trusts fractional uptime as active") old_ready_new_external = "\n".join([ "miner speed 10s/60s/15m 341.2 340.9 340.7 H/s", @@ -233,7 +243,7 @@ for required in \ 'API_MAX_BYTES = 256 * 1024' \ 'def nonnegative_integer(value: object) -> int | None:' \ 'connection_ip = connection.get("ip")' \ - 'connection_uptime_ms = nonnegative_number(connection.get("uptime_ms"))' \ + 'connection_uptime_ms = nonnegative_integer(connection.get("uptime_ms"))' \ '"pool_connected": pool_connected' \ 'current_hashrate = api_metrics.get("hashrate_10s")' \ 'if pool_connected and current_hashrate_positive:' \ From 55fec76b7351f7427f735b9ab97e372a8373e457 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:36:01 +0700 Subject: [PATCH 231/386] gate miner stability integration on unix --- crates/rigos-config/tests/miner_stability.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/rigos-config/tests/miner_stability.rs b/crates/rigos-config/tests/miner_stability.rs index 37a6405c..d156d07a 100644 --- a/crates/rigos-config/tests/miner_stability.rs +++ b/crates/rigos-config/tests/miner_stability.rs @@ -1,3 +1,5 @@ +#![cfg(unix)] + use serde_json::Value; use std::fs; use std::os::unix::fs::{PermissionsExt, symlink}; From affbf577896686db6200403fc1943aa1d466cb2a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:36:23 +0700 Subject: [PATCH 232/386] gate runtime publication integration on unix --- crates/rigos-config/tests/hive_exit_runtime_publish.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index 55462e35..15e5decb 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -1,3 +1,5 @@ +#![cfg(unix)] + use serde_json::Value; use std::fs; use std::os::unix::fs::{PermissionsExt, symlink}; From 5df80c0df0aba0095ee8a88272b37609d8600b51 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:36:47 +0700 Subject: [PATCH 233/386] avoid windows-only unused permissions warning --- crates/rigos-config/tests/recovery_access.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index 79e156b2..fe6ff850 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -165,11 +165,12 @@ fn alpha8_appliance_wiring_is_explicit() { .expect("read runtime authority service"); assert!(runtime_service.contains("ExecStart=/usr/lib/rigos/rigos-runtime-authority")); - let authority = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority"); - let mode = fs::metadata(authority).unwrap().permissions(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; + let authority = + repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority"); + let mode = fs::metadata(authority).unwrap().permissions(); assert_ne!(mode.mode() & 0o111, 0); } From 201fa37624a888540a3cfaa0b8d68e4f9d00493c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:37:18 +0700 Subject: [PATCH 234/386] guard unix integration platform gates --- .../tests/miner_observer_authority.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index a1b03204..2262dec4 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -104,6 +104,34 @@ fn observer_authority_is_wired_into_build_and_exact_image_gates() { } } +#[test] +fn unix_only_integrations_are_explicitly_platform_gated() { + for path in [ + "crates/rigos-config/tests/miner_stability.rs", + "crates/rigos-config/tests/hive_exit_runtime_publish.rs", + ] { + let source = fs::read_to_string(repo_path(path)) + .unwrap_or_else(|error| panic!("read Unix integration {path}: {error}")); + assert!( + source.starts_with("#![cfg(unix)]\n"), + "Unix integration is not explicitly platform gated: {path}" + ); + } + + let recovery = fs::read_to_string(repo_path("crates/rigos-config/tests/recovery_access.rs")) + .expect("read recovery access integration"); + let unix_scope = recovery + .find("#[cfg(unix)]\n {") + .expect("recovery executable-mode assertion is not Unix scoped"); + let permissions_import = recovery + .find("use std::os::unix::fs::PermissionsExt;") + .expect("recovery Unix permissions import is missing"); + let mode_binding = recovery + .find("let mode = fs::metadata(authority).unwrap().permissions();") + .expect("recovery Unix mode binding is missing"); + assert!(unix_scope < permissions_import && permissions_import < mode_binding); +} + #[test] fn observer_test_files_are_regular_repository_files() { for path in [ From 27421dc5f3ff6fdce358ad061917dd6be96045cc Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:42:04 +0700 Subject: [PATCH 235/386] gate RandomX MSR integration on unix --- crates/rigos-config/tests/randomx_msr_authority.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/rigos-config/tests/randomx_msr_authority.rs b/crates/rigos-config/tests/randomx_msr_authority.rs index 8197feb6..4cd8e11e 100644 --- a/crates/rigos-config/tests/randomx_msr_authority.rs +++ b/crates/rigos-config/tests/randomx_msr_authority.rs @@ -1,3 +1,5 @@ +#![cfg(unix)] + use serde_json::Value; use std::fs::{self, File, OpenOptions}; use std::io::{Seek, SeekFrom, Write}; From 12f64751ffa88ee4ec2f59630687b41caa0f581a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:42:33 +0700 Subject: [PATCH 236/386] guard RandomX MSR Unix platform gate --- crates/rigos-config/tests/miner_observer_authority.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index 2262dec4..3aabf6eb 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -109,6 +109,7 @@ fn unix_only_integrations_are_explicitly_platform_gated() { for path in [ "crates/rigos-config/tests/miner_stability.rs", "crates/rigos-config/tests/hive_exit_runtime_publish.rs", + "crates/rigos-config/tests/randomx_msr_authority.rs", ] { let source = fs::read_to_string(repo_path(path)) .unwrap_or_else(|error| panic!("read Unix integration {path}: {error}")); From b28aeee021b1249e7ab6c7a7571d7284f6ee7142 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:43:11 +0700 Subject: [PATCH 237/386] document Windows and Linux source gate separation --- docs/developer/windows-source-gate.txt | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/developer/windows-source-gate.txt diff --git a/docs/developer/windows-source-gate.txt b/docs/developer/windows-source-gate.txt new file mode 100644 index 00000000..55d54061 --- /dev/null +++ b/docs/developer/windows-source-gate.txt @@ -0,0 +1,67 @@ +DBYTE RIGOS WINDOWS SOURCE GATE +=============================== + +PURPOSE +------- + +Windows may compile and run cross-platform source tests. Linux remains the +authoritative environment for Unix runtime integration, image construction, +filesystem permission semantics, systemd verification, and physical gates. + +POWERSHELL RULE +--------------- + +PowerShell does not use a trailing backslash as a line continuation. Run Cargo +commands on one line, or use PowerShell's backtick continuation explicitly. + +SAFE BRANCH RECOVERY +-------------------- + +When a local performance branch still points at the historical ADDED BASED +commit, preserve that history before aligning the working branch with origin: + + git status --short + git branch backup/local-based-b25f0bf b25f0bf1ab902a22bb06a8e363fcdf51d66cafe7 + git fetch origin performance/randomx-sustained-hash + git reset --hard origin/performance/randomx-sustained-hash + +Do not rebase the ADDED BASED commits onto the performance branch. That history +contains the raw HiveOS reference tree intentionally removed from PR #15. + +WINDOWS COMMANDS +---------------- + + cargo test --locked -p rigos-config --test miner_observer_authority -- --nocapture + cargo test --locked -p rigos-config --no-run + +The following integration targets are intentionally Unix-only and must retain a +file-level #![cfg(unix)] gate: + + crates/rigos-config/tests/miner_stability.rs + crates/rigos-config/tests/hive_exit_runtime_publish.rs + crates/rigos-config/tests/randomx_msr_authority.rs + +LINUX COMMANDS +-------------- + +Run these inside an actual WSL/Linux shell with Linux Rust installed: + + cd /mnt/d/TECHNICAL/dbyte-rigos + source "$HOME/.cargo/env" + cargo --version + bash ./scripts/verify.sh + +Typing /mnt/d paths or source commands directly into PowerShell does not enter +WSL. From PowerShell, launch a Linux shell first with: + + wsl.exe + +or invoke the command explicitly: + + wsl.exe bash -lc 'cd /mnt/d/TECHNICAL/dbyte-rigos && source "$HOME/.cargo/env" && bash ./scripts/verify.sh' + +EVIDENCE RULE +------------- + +A Windows compile PASS is not a Linux runtime PASS. An exact image PASS and +physical PASS remain separate gates. From f4603918b6bf949072357203fdd7ad2d68827f0b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Wed, 8 Jul 2026 23:55:31 +0700 Subject: [PATCH 238/386] format miner observer authority test --- .../tests/miner_observer_authority.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index 3aabf6eb..b6a43a3c 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -21,7 +21,10 @@ fn run_python(script: &str) { .current_dir(repo_root()) .status() .unwrap_or_else(|error| panic!("failed to execute {script} with {python}: {error}")); - assert!(status.success(), "observer source regression failed: {script}"); + assert!( + status.success(), + "observer source regression failed: {script}" + ); } #[cfg(unix)] @@ -82,7 +85,10 @@ fn observer_authority_is_wired_into_build_and_exact_image_gates() { "latest_journal_signal", "api_error not in (None, \"api_unavailable\")", ] { - assert!(observer.contains(contract), "observer contract missing: {contract}"); + assert!( + observer.contains(contract), + "observer contract missing: {contract}" + ); } assert!(renderer.contains("secrets.token_urlsafe(48)")); @@ -143,10 +149,12 @@ fn observer_test_files_are_regular_repository_files() { "scripts/test-miner-health-journal-fallback.py", "scripts/test-runtime-token-publication.py", ] { - let metadata = fs::metadata(repo_path(path)).unwrap_or_else(|error| { - panic!("observer test file is unavailable: {path}: {error}") - }); - assert!(metadata.is_file(), "observer test path is not a file: {path}"); + let metadata = fs::metadata(repo_path(path)) + .unwrap_or_else(|error| panic!("observer test file is unavailable: {path}: {error}")); + assert!( + metadata.is_file(), + "observer test path is not a file: {path}" + ); } assert!(repo_path("scripts/verify-miner-observer-image.sh").is_file()); From 4b92ee9ae124f70f3a461e5cbf4057dcd529eb36 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:22:41 +0700 Subject: [PATCH 239/386] add fail-closed WSL source gate launcher --- scripts/verify-wsl.ps1 | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 scripts/verify-wsl.ps1 diff --git a/scripts/verify-wsl.ps1 b/scripts/verify-wsl.ps1 new file mode 100644 index 00000000..3deb6218 --- /dev/null +++ b/scripts/verify-wsl.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +param( + [string]$Repository = (Split-Path -Parent $PSScriptRoot), + [string]$Distribution = $env:RIGOS_WSL_DISTRO +) + +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $false + +$WslPrefix = @() +if (-not [string]::IsNullOrWhiteSpace($Distribution)) { + $WslPrefix += @("-d", $Distribution) +} + +$Repository = (Resolve-Path -LiteralPath $Repository).Path +$LinuxRepoOutput = & wsl.exe @WslPrefix -- wslpath -a $Repository 2>&1 +if ($LASTEXITCODE -ne 0) { + throw "WSL_PATH_CONVERSION_FAILED: $LinuxRepoOutput" +} + +$LinuxRepo = ($LinuxRepoOutput | Select-Object -Last 1).Trim() +if ([string]::IsNullOrWhiteSpace($LinuxRepo)) { + throw "WSL_PATH_CONVERSION_EMPTY" +} + +$Shell = @' +set -euo pipefail + +repo="$1" +cd "$repo" + +if [ -f "$HOME/.cargo/env" ]; then + . "$HOME/.cargo/env" +fi + +missing=0 +for tool in cargo rustc python3 bash rg; do + if ! command -v "$tool" >/dev/null 2>&1; then + printf 'RIGOS_WSL_TOOL_MISSING=%s\n' "$tool" >&2 + missing=1 + fi +done + +if [ "$missing" -ne 0 ]; then + printf '%s\n' 'Install the missing tool inside this WSL distribution, then rerun scripts/verify-wsl.ps1.' >&2 + exit 127 +fi + +printf 'RIGOS_WSL_REPO=%s\n' "$repo" +printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" +exec bash ./scripts/verify.sh +'@ + +& wsl.exe @WslPrefix -- bash -lc $Shell -- $LinuxRepo +$ExitCode = $LASTEXITCODE +if ($ExitCode -ne 0) { + throw "RIGOS_WSL_SOURCE_GATE_FAILED: exit $ExitCode" +} + +Write-Host "RIGOS_WSL_SOURCE_GATE=PASS" From 9cc5d0a6381945e78b0c02e64cc437b58b3cefef Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:22:49 +0700 Subject: [PATCH 240/386] test WSL source gate launcher contract --- crates/rigos-config/tests/wsl_source_gate.rs | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 crates/rigos-config/tests/wsl_source_gate.rs diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs new file mode 100644 index 00000000..2d310b58 --- /dev/null +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -0,0 +1,41 @@ +use std::fs; +use std::path::PathBuf; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +#[test] +fn wsl_launcher_is_path_safe_and_fail_closed() { + let launcher = fs::read_to_string(repo_path("scripts/verify-wsl.ps1")) + .expect("read WSL source gate launcher"); + + for required in [ + "$PSScriptRoot", + "wslpath -a", + "RIGOS_WSL_DISTRO", + "command -v \"$tool\"", + "RIGOS_WSL_TOOL_MISSING", + "exec bash ./scripts/verify.sh", + "RIGOS_WSL_SOURCE_GATE=PASS", + ] { + assert!( + launcher.contains(required), + "WSL launcher contract missing: {required}" + ); + } + + for forbidden in [ + "/mnt/d/TECHNICAL/dbyte-rigos", + "curl | sh", + "Invoke-WebRequest", + "rustup-init", + ] { + assert!( + !launcher.contains(forbidden), + "WSL launcher contains forbidden bootstrap or hard-coded path: {forbidden}" + ); + } +} From 524eafb20f68f79f8b28fb6133292d44079ba7eb Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:23:05 +0700 Subject: [PATCH 241/386] document fail-closed WSL source verification --- docs/developer/windows-source-gate.txt | 70 +++++++++++++++++--------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/docs/developer/windows-source-gate.txt b/docs/developer/windows-source-gate.txt index 55d54061..a8de08f5 100644 --- a/docs/developer/windows-source-gate.txt +++ b/docs/developer/windows-source-gate.txt @@ -14,24 +14,22 @@ POWERSHELL RULE PowerShell does not use a trailing backslash as a line continuation. Run Cargo commands on one line, or use PowerShell's backtick continuation explicitly. -SAFE BRANCH RECOVERY --------------------- +BASED RECOVERY RULE +------------------- -When a local performance branch still points at the historical ADDED BASED -commit, preserve that history before aligning the working branch with origin: +The historical raw HiveOS filesystem import must not remain reachable from any +production branch, tag, or local Git backup branch. Preserve any required +reference outside Git in encrypted or offline storage, then use a fresh +single-branch clone as the canonical development checkout. - git status --short - git branch backup/local-based-b25f0bf b25f0bf1ab902a22bb06a8e363fcdf51d66cafe7 - git fetch origin performance/randomx-sustained-hash - git reset --hard origin/performance/randomx-sustained-hash - -Do not rebase the ADDED BASED commits onto the performance branch. That history -contains the raw HiveOS reference tree intentionally removed from PR #15. +Never rebase the historical BASED commits onto a production branch. WINDOWS COMMANDS ---------------- + cargo fmt --all -- --check cargo test --locked -p rigos-config --test miner_observer_authority -- --nocapture + cargo test --locked -p rigos-config --test wsl_source_gate -- --nocapture cargo test --locked -p rigos-config --no-run The following integration targets are intentionally Unix-only and must retain a @@ -41,27 +39,49 @@ file-level #![cfg(unix)] gate: crates/rigos-config/tests/hive_exit_runtime_publish.rs crates/rigos-config/tests/randomx_msr_authority.rs -LINUX COMMANDS --------------- +LINUX AUTHORITY FROM POWERSHELL +------------------------------- -Run these inside an actual WSL/Linux shell with Linux Rust installed: +Run the fail-closed launcher from the repository root: - cd /mnt/d/TECHNICAL/dbyte-rigos - source "$HOME/.cargo/env" - cargo --version - bash ./scripts/verify.sh + powershell -ExecutionPolicy Bypass -File .\scripts\verify-wsl.ps1 + +The launcher: + + - derives the repository from its own location + - converts the Windows path through wslpath + - sources $HOME/.cargo/env when present + - checks cargo, rustc, python3, bash, and rg + - does not download or install tools + - executes scripts/verify.sh only after every prerequisite is present + +To select a specific installed WSL distribution: + + powershell -ExecutionPolicy Bypass -File .\scripts\verify-wsl.ps1 -Distribution Ubuntu -Typing /mnt/d paths or source commands directly into PowerShell does not enter -WSL. From PowerShell, launch a Linux shell first with: +or set: - wsl.exe + $env:RIGOS_WSL_DISTRO = "Ubuntu" -or invoke the command explicitly: +A missing dependency is reported as: - wsl.exe bash -lc 'cd /mnt/d/TECHNICAL/dbyte-rigos && source "$HOME/.cargo/env" && bash ./scripts/verify.sh' + RIGOS_WSL_TOOL_MISSING= + +Install that tool inside the selected WSL distribution and rerun the launcher. +Do not type /mnt paths or source commands directly into PowerShell. + +DIRECT LINUX COMMANDS +--------------------- + +Inside an actual WSL/Linux shell with Linux Rust installed: + + cd /mnt/d/TECHNICAL/dbyte-rigos + source "$HOME/.cargo/env" + cargo --version + bash ./scripts/verify.sh EVIDENCE RULE ------------- -A Windows compile PASS is not a Linux runtime PASS. An exact image PASS and -physical PASS remain separate gates. +A Windows compile PASS is not a Linux runtime PASS. A Linux source PASS is not +an exact-image PASS. Exact-image and physical PASS remain separate gates. From cd4d422508a9d78efb94f6906835e43fc7645a38 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:24:08 +0700 Subject: [PATCH 242/386] harden WSL source gate prerequisite checks --- scripts/verify-wsl.ps1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/verify-wsl.ps1 b/scripts/verify-wsl.ps1 index 3deb6218..dafbdd8b 100644 --- a/scripts/verify-wsl.ps1 +++ b/scripts/verify-wsl.ps1 @@ -34,7 +34,7 @@ if [ -f "$HOME/.cargo/env" ]; then fi missing=0 -for tool in cargo rustc python3 bash rg; do +for tool in cargo rustc python3 bash sh git grep rg mktemp; do if ! command -v "$tool" >/dev/null 2>&1; then printf 'RIGOS_WSL_TOOL_MISSING=%s\n' "$tool" >&2 missing=1 @@ -46,6 +46,18 @@ if [ "$missing" -ne 0 ]; then exit 127 fi +for component in fmt clippy; do + if ! cargo "$component" --version >/dev/null 2>&1; then + printf 'RIGOS_WSL_CARGO_COMPONENT_MISSING=%s\n' "$component" >&2 + missing=1 + fi +done + +if [ "$missing" -ne 0 ]; then + printf '%s\n' 'Install the missing Rust component inside this WSL distribution, then rerun scripts/verify-wsl.ps1.' >&2 + exit 127 +fi + printf 'RIGOS_WSL_REPO=%s\n' "$repo" printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" exec bash ./scripts/verify.sh From b0d6f2f9a81dccbd1dbe7c844191094ad6c32a6e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:24:22 +0700 Subject: [PATCH 243/386] lock WSL tool and Rust component preflight --- crates/rigos-config/tests/wsl_source_gate.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index 2d310b58..7925b44a 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -16,8 +16,11 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { "$PSScriptRoot", "wslpath -a", "RIGOS_WSL_DISTRO", + "for tool in cargo rustc python3 bash sh git grep rg mktemp", "command -v \"$tool\"", "RIGOS_WSL_TOOL_MISSING", + "for component in fmt clippy", + "RIGOS_WSL_CARGO_COMPONENT_MISSING", "exec bash ./scripts/verify.sh", "RIGOS_WSL_SOURCE_GATE=PASS", ] { From e0deca8831be6feef41418cd7a32bd4e0fef62d5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:24:38 +0700 Subject: [PATCH 244/386] document complete WSL prerequisite preflight --- docs/developer/windows-source-gate.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/developer/windows-source-gate.txt b/docs/developer/windows-source-gate.txt index a8de08f5..6ac5667c 100644 --- a/docs/developer/windows-source-gate.txt +++ b/docs/developer/windows-source-gate.txt @@ -51,7 +51,8 @@ The launcher: - derives the repository from its own location - converts the Windows path through wslpath - sources $HOME/.cargo/env when present - - checks cargo, rustc, python3, bash, and rg + - checks cargo, rustc, python3, bash, sh, git, grep, rg, and mktemp + - checks the Rust fmt and clippy cargo components - does not download or install tools - executes scripts/verify.sh only after every prerequisite is present @@ -63,12 +64,14 @@ or set: $env:RIGOS_WSL_DISTRO = "Ubuntu" -A missing dependency is reported as: +A missing executable or Rust component is reported as: RIGOS_WSL_TOOL_MISSING= + RIGOS_WSL_CARGO_COMPONENT_MISSING= -Install that tool inside the selected WSL distribution and rerun the launcher. -Do not type /mnt paths or source commands directly into PowerShell. +Install the missing prerequisite inside the selected WSL distribution and rerun +the launcher. Do not type /mnt paths or source commands directly into +PowerShell. DIRECT LINUX COMMANDS --------------------- From f14d9ae41c1c9186083cacdfd82aaaf68279fa43 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:27:17 +0700 Subject: [PATCH 245/386] fix PowerShell script root resolution --- scripts/verify-wsl.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/verify-wsl.ps1 b/scripts/verify-wsl.ps1 index dafbdd8b..0d8dda8a 100644 --- a/scripts/verify-wsl.ps1 +++ b/scripts/verify-wsl.ps1 @@ -1,12 +1,19 @@ [CmdletBinding()] param( - [string]$Repository = (Split-Path -Parent $PSScriptRoot), + [string]$Repository, [string]$Distribution = $env:RIGOS_WSL_DISTRO ) $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $false +if ([string]::IsNullOrWhiteSpace($Repository)) { + if ([string]::IsNullOrWhiteSpace($PSScriptRoot)) { + throw "RIGOS_WSL_SCRIPT_ROOT_UNAVAILABLE" + } + $Repository = Split-Path -Parent $PSScriptRoot +} + $WslPrefix = @() if (-not [string]::IsNullOrWhiteSpace($Distribution)) { $WslPrefix += @("-d", $Distribution) From d9d0da6f91a65cfe3c7b8af9c61196c41251b428 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:27:46 +0700 Subject: [PATCH 246/386] resolve observer test repository root at runtime --- .../tests/miner_observer_authority.rs | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index b6a43a3c..bfe65e57 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -1,12 +1,39 @@ #[cfg(unix)] -use std::env; -use std::fs; -use std::path::PathBuf; -#[cfg(unix)] use std::process::Command; +use std::{env, fs}; +use std::path::{Path, PathBuf}; + +fn is_repo_root(path: &Path) -> bool { + path.join("Cargo.toml").is_file() + && path.join("crates/rigos-config/Cargo.toml").is_file() + && path.join("scripts/verify.sh").is_file() +} fn repo_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") + let mut starts = Vec::new(); + + if let Ok(current) = env::current_dir() { + starts.push(current); + } + if let Ok(executable) = env::current_exe() { + if let Some(parent) = executable.parent() { + starts.push(parent.to_path_buf()); + } + } + + for start in starts { + let mut candidate = start; + loop { + if is_repo_root(&candidate) { + return candidate; + } + if !candidate.pop() { + break; + } + } + } + + panic!("unable to locate the RIGOS repository root at runtime"); } fn repo_path(path: &str) -> PathBuf { @@ -159,3 +186,15 @@ fn observer_test_files_are_regular_repository_files() { assert!(repo_path("scripts/verify-miner-observer-image.sh").is_file()); } + +#[test] +fn repository_root_is_resolved_at_runtime() { + let root = repo_root(); + assert!(is_repo_root(&root)); + + let source = fs::read_to_string(root.join( + "crates/rigos-config/tests/miner_observer_authority.rs", + )) + .expect("read observer authority source"); + assert!(!source.contains("CARGO_MANIFEST_DIR")); +} From a867e35bd4fa0597f351723eb40d2afe414e367c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:28:00 +0700 Subject: [PATCH 247/386] make WSL launcher test relocation-safe --- crates/rigos-config/tests/wsl_source_gate.rs | 55 ++++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index 7925b44a..189b1ea9 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -1,10 +1,42 @@ +use std::env; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; + +fn is_repo_root(path: &Path) -> bool { + path.join("Cargo.toml").is_file() + && path.join("crates/rigos-config/Cargo.toml").is_file() + && path.join("scripts/verify.sh").is_file() +} + +fn repo_root() -> PathBuf { + let mut starts = Vec::new(); + + if let Ok(current) = env::current_dir() { + starts.push(current); + } + if let Ok(executable) = env::current_exe() { + if let Some(parent) = executable.parent() { + starts.push(parent.to_path_buf()); + } + } + + for start in starts { + let mut candidate = start; + loop { + if is_repo_root(&candidate) { + return candidate; + } + if !candidate.pop() { + break; + } + } + } + + panic!("unable to locate the RIGOS repository root at runtime"); +} fn repo_path(path: &str) -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(path) + repo_root().join(path) } #[test] @@ -13,7 +45,9 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { .expect("read WSL source gate launcher"); for required in [ + "[string]$Repository,", "$PSScriptRoot", + "RIGOS_WSL_SCRIPT_ROOT_UNAVAILABLE", "wslpath -a", "RIGOS_WSL_DISTRO", "for tool in cargo rustc python3 bash sh git grep rg mktemp", @@ -31,6 +65,7 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { } for forbidden in [ + "[string]$Repository = (Split-Path -Parent $PSScriptRoot)", "/mnt/d/TECHNICAL/dbyte-rigos", "curl | sh", "Invoke-WebRequest", @@ -38,7 +73,17 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { ] { assert!( !launcher.contains(forbidden), - "WSL launcher contains forbidden bootstrap or hard-coded path: {forbidden}" + "WSL launcher contains forbidden bootstrap, default expression, or hard-coded path: {forbidden}" ); } } + +#[test] +fn repository_root_is_resolved_at_runtime() { + let root = repo_root(); + assert!(is_repo_root(&root)); + + let source = fs::read_to_string(root.join("crates/rigos-config/tests/wsl_source_gate.rs")) + .expect("read WSL source gate test"); + assert!(!source.contains("CARGO_MANIFEST_DIR")); +} From 1c4097d8c13b210ad32b16d199deb0ae244995f1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:28:31 +0700 Subject: [PATCH 248/386] fix observer relocation regression assertion --- crates/rigos-config/tests/miner_observer_authority.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index bfe65e57..4f75ea83 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -1,7 +1,7 @@ #[cfg(unix)] use std::process::Command; -use std::{env, fs}; use std::path::{Path, PathBuf}; +use std::{env, fs}; fn is_repo_root(path: &Path) -> bool { path.join("Cargo.toml").is_file() @@ -196,5 +196,6 @@ fn repository_root_is_resolved_at_runtime() { "crates/rigos-config/tests/miner_observer_authority.rs", )) .expect("read observer authority source"); - assert!(!source.contains("CARGO_MANIFEST_DIR")); + let compile_time_manifest = ["CARGO", "_MANIFEST_DIR"].concat(); + assert!(!source.contains(&compile_time_manifest)); } From 8060bb3da6967208b60f0dca5b192cb992ea8f2d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:28:46 +0700 Subject: [PATCH 249/386] fix WSL relocation regression assertion --- crates/rigos-config/tests/wsl_source_gate.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index 189b1ea9..c1efdfdf 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -85,5 +85,6 @@ fn repository_root_is_resolved_at_runtime() { let source = fs::read_to_string(root.join("crates/rigos-config/tests/wsl_source_gate.rs")) .expect("read WSL source gate test"); - assert!(!source.contains("CARGO_MANIFEST_DIR")); + let compile_time_manifest = ["CARGO", "_MANIFEST_DIR"].concat(); + assert!(!source.contains(&compile_time_manifest)); } From 568135e18116593e4b283e9e167ab185a0eed5e5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:35:11 +0700 Subject: [PATCH 250/386] preserve Windows paths across WSL transport --- scripts/verify-wsl.ps1 | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/verify-wsl.ps1 b/scripts/verify-wsl.ps1 index 0d8dda8a..2b3bc82c 100644 --- a/scripts/verify-wsl.ps1 +++ b/scripts/verify-wsl.ps1 @@ -19,13 +19,30 @@ if (-not [string]::IsNullOrWhiteSpace($Distribution)) { $WslPrefix += @("-d", $Distribution) } -$Repository = (Resolve-Path -LiteralPath $Repository).Path -$LinuxRepoOutput = & wsl.exe @WslPrefix -- wslpath -a $Repository 2>&1 -if ($LASTEXITCODE -ne 0) { - throw "WSL_PATH_CONVERSION_FAILED: $LinuxRepoOutput" +$Repository = (Resolve-Path -LiteralPath $Repository -ErrorAction Stop).Path +$PathConverter = @' +set -euo pipefail +IFS= read -r windows_path +windows_path="${windows_path%$'\r'}" +wslpath -a "$windows_path" +'@ + +$SavedErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "Continue" +try { + $LinuxRepoOutput = $Repository | + & wsl.exe @WslPrefix -- bash -lc $PathConverter 2>&1 + $PathExitCode = $LASTEXITCODE +} finally { + $ErrorActionPreference = $SavedErrorActionPreference } -$LinuxRepo = ($LinuxRepoOutput | Select-Object -Last 1).Trim() +$LinuxRepoLines = @($LinuxRepoOutput | ForEach-Object { $_.ToString() }) +if ($PathExitCode -ne 0) { + throw "WSL_PATH_CONVERSION_FAILED: $($LinuxRepoLines -join ' | ')" +} + +$LinuxRepo = ($LinuxRepoLines | Select-Object -Last 1).Trim() if ([string]::IsNullOrWhiteSpace($LinuxRepo)) { throw "WSL_PATH_CONVERSION_EMPTY" } @@ -70,8 +87,16 @@ printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" exec bash ./scripts/verify.sh '@ -& wsl.exe @WslPrefix -- bash -lc $Shell -- $LinuxRepo -$ExitCode = $LASTEXITCODE +$SavedErrorActionPreference = $ErrorActionPreference +$ErrorActionPreference = "Continue" +try { + & wsl.exe @WslPrefix -- bash -lc $Shell -- $LinuxRepo 2>&1 | + ForEach-Object { Write-Host $_ } + $ExitCode = $LASTEXITCODE +} finally { + $ErrorActionPreference = $SavedErrorActionPreference +} + if ($ExitCode -ne 0) { throw "RIGOS_WSL_SOURCE_GATE_FAILED: exit $ExitCode" } From 01b35d4365ee5dbe15685234c96dc892c9f90ecb Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:35:28 +0700 Subject: [PATCH 251/386] guard raw Windows path transport into WSL --- crates/rigos-config/tests/wsl_source_gate.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index c1efdfdf..7d7b0f49 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -48,7 +48,13 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { "[string]$Repository,", "$PSScriptRoot", "RIGOS_WSL_SCRIPT_ROOT_UNAVAILABLE", - "wslpath -a", + "$Repository |", + "bash -lc $PathConverter", + "IFS= read -r windows_path", + "windows_path=\"${windows_path%$'\\r'}\"", + "wslpath -a \"$windows_path\"", + "$PathExitCode = $LASTEXITCODE", + "$ErrorActionPreference = \"Continue\"", "RIGOS_WSL_DISTRO", "for tool in cargo rustc python3 bash sh git grep rg mktemp", "command -v \"$tool\"", @@ -66,6 +72,7 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { for forbidden in [ "[string]$Repository = (Split-Path -Parent $PSScriptRoot)", + "wslpath -a $Repository", "/mnt/d/TECHNICAL/dbyte-rigos", "curl | sh", "Invoke-WebRequest", @@ -73,7 +80,7 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { ] { assert!( !launcher.contains(forbidden), - "WSL launcher contains forbidden bootstrap, default expression, or hard-coded path: {forbidden}" + "WSL launcher contains forbidden bootstrap, direct path argument, default expression, or hard-coded path: {forbidden}" ); } } From b81f7c66fddee6091f1fd3f4998d7d66aa5080f2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:35:52 +0700 Subject: [PATCH 252/386] format relocation-safe observer authority test --- crates/rigos-config/tests/miner_observer_authority.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/rigos-config/tests/miner_observer_authority.rs b/crates/rigos-config/tests/miner_observer_authority.rs index 4f75ea83..298653a7 100644 --- a/crates/rigos-config/tests/miner_observer_authority.rs +++ b/crates/rigos-config/tests/miner_observer_authority.rs @@ -1,6 +1,6 @@ +use std::path::{Path, PathBuf}; #[cfg(unix)] use std::process::Command; -use std::path::{Path, PathBuf}; use std::{env, fs}; fn is_repo_root(path: &Path) -> bool { @@ -192,10 +192,9 @@ fn repository_root_is_resolved_at_runtime() { let root = repo_root(); assert!(is_repo_root(&root)); - let source = fs::read_to_string(root.join( - "crates/rigos-config/tests/miner_observer_authority.rs", - )) - .expect("read observer authority source"); + let source = + fs::read_to_string(root.join("crates/rigos-config/tests/miner_observer_authority.rs")) + .expect("read observer authority source"); let compile_time_manifest = ["CARGO", "_MANIFEST_DIR"].concat(); assert!(!source.contains(&compile_time_manifest)); } From 64a16f8a4b9a2a7402f44ca15fac3c816cfb5b7b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:39:18 +0700 Subject: [PATCH 253/386] add WSL source gate entrypoint --- scripts/verify-wsl-entrypoint.sh | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 scripts/verify-wsl-entrypoint.sh diff --git a/scripts/verify-wsl-entrypoint.sh b/scripts/verify-wsl-entrypoint.sh new file mode 100644 index 00000000..f4c3ba35 --- /dev/null +++ b/scripts/verify-wsl-entrypoint.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: verify-wsl-entrypoint.sh REPOSITORY" >&2 + exit 64 +fi + +repo="$1" +if [[ ! -f "$repo/Cargo.toml" || ! -f "$repo/scripts/verify.sh" ]]; then + echo "RIGOS_WSL_REPOSITORY_INVALID=$repo" >&2 + exit 66 +fi + +cd "$repo" + +if [[ -f "$HOME/.cargo/env" ]]; then + # shellcheck disable=SC1091 + . "$HOME/.cargo/env" +fi + +missing=0 +for tool in cargo rustc python3 bash sh git grep rg mktemp; do + if ! command -v "$tool" >/dev/null 2>&1; then + printf 'RIGOS_WSL_TOOL_MISSING=%s\n' "$tool" >&2 + missing=1 + fi +done + +if [[ "$missing" -ne 0 ]]; then + echo "Install the missing tool inside this WSL distribution, then rerun scripts/verify-wsl.ps1." >&2 + exit 127 +fi + +for component in fmt clippy; do + if ! cargo "$component" --version >/dev/null 2>&1; then + printf 'RIGOS_WSL_CARGO_COMPONENT_MISSING=%s\n' "$component" >&2 + missing=1 + fi +done + +if [[ "$missing" -ne 0 ]]; then + echo "Install the missing Rust component inside this WSL distribution, then rerun scripts/verify-wsl.ps1." >&2 + exit 127 +fi + +printf 'RIGOS_WSL_REPO=%s\n' "$repo" +printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" +exec bash ./scripts/verify.sh From b4c2c21a3df7a1c6ff4faecc6770933cb0a01f95 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:39:30 +0700 Subject: [PATCH 254/386] use file entrypoint for WSL source gate --- scripts/verify-wsl.ps1 | 52 ++++-------------------------------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/scripts/verify-wsl.ps1 b/scripts/verify-wsl.ps1 index 2b3bc82c..aa947abd 100644 --- a/scripts/verify-wsl.ps1 +++ b/scripts/verify-wsl.ps1 @@ -20,18 +20,12 @@ if (-not [string]::IsNullOrWhiteSpace($Distribution)) { } $Repository = (Resolve-Path -LiteralPath $Repository -ErrorAction Stop).Path -$PathConverter = @' -set -euo pipefail -IFS= read -r windows_path -windows_path="${windows_path%$'\r'}" -wslpath -a "$windows_path" -'@ +$RepositoryForWsl = $Repository.Replace([char]92, [char]47) $SavedErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { - $LinuxRepoOutput = $Repository | - & wsl.exe @WslPrefix -- bash -lc $PathConverter 2>&1 + $LinuxRepoOutput = & wsl.exe @WslPrefix -- wslpath -a -- $RepositoryForWsl 2>&1 $PathExitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $SavedErrorActionPreference @@ -47,50 +41,12 @@ if ([string]::IsNullOrWhiteSpace($LinuxRepo)) { throw "WSL_PATH_CONVERSION_EMPTY" } -$Shell = @' -set -euo pipefail - -repo="$1" -cd "$repo" - -if [ -f "$HOME/.cargo/env" ]; then - . "$HOME/.cargo/env" -fi - -missing=0 -for tool in cargo rustc python3 bash sh git grep rg mktemp; do - if ! command -v "$tool" >/dev/null 2>&1; then - printf 'RIGOS_WSL_TOOL_MISSING=%s\n' "$tool" >&2 - missing=1 - fi -done - -if [ "$missing" -ne 0 ]; then - printf '%s\n' 'Install the missing tool inside this WSL distribution, then rerun scripts/verify-wsl.ps1.' >&2 - exit 127 -fi - -for component in fmt clippy; do - if ! cargo "$component" --version >/dev/null 2>&1; then - printf 'RIGOS_WSL_CARGO_COMPONENT_MISSING=%s\n' "$component" >&2 - missing=1 - fi -done - -if [ "$missing" -ne 0 ]; then - printf '%s\n' 'Install the missing Rust component inside this WSL distribution, then rerun scripts/verify-wsl.ps1.' >&2 - exit 127 -fi - -printf 'RIGOS_WSL_REPO=%s\n' "$repo" -printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" -exec bash ./scripts/verify.sh -'@ +$LinuxEntrypoint = "$LinuxRepo/scripts/verify-wsl-entrypoint.sh" $SavedErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { - & wsl.exe @WslPrefix -- bash -lc $Shell -- $LinuxRepo 2>&1 | + & wsl.exe @WslPrefix -- bash $LinuxEntrypoint $LinuxRepo 2>&1 | ForEach-Object { Write-Host $_ } $ExitCode = $LASTEXITCODE } finally { From 03df37b1a0905ccb4a60303ed0abeab71e07ff9d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:39:43 +0700 Subject: [PATCH 255/386] test file-based WSL source gate transport --- crates/rigos-config/tests/wsl_source_gate.rs | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index 7d7b0f49..d050791a 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -43,36 +43,49 @@ fn repo_path(path: &str) -> PathBuf { fn wsl_launcher_is_path_safe_and_fail_closed() { let launcher = fs::read_to_string(repo_path("scripts/verify-wsl.ps1")) .expect("read WSL source gate launcher"); + let entrypoint = fs::read_to_string(repo_path("scripts/verify-wsl-entrypoint.sh")) + .expect("read WSL source gate entrypoint"); for required in [ "[string]$Repository,", "$PSScriptRoot", "RIGOS_WSL_SCRIPT_ROOT_UNAVAILABLE", - "$Repository |", - "bash -lc $PathConverter", - "IFS= read -r windows_path", - "windows_path=\"${windows_path%$'\\r'}\"", - "wslpath -a \"$windows_path\"", + "$Repository.Replace([char]92, [char]47)", + "wslpath -a -- $RepositoryForWsl", "$PathExitCode = $LASTEXITCODE", "$ErrorActionPreference = \"Continue\"", - "RIGOS_WSL_DISTRO", + "verify-wsl-entrypoint.sh", + "& wsl.exe @WslPrefix -- bash $LinuxEntrypoint $LinuxRepo", + "RIGOS_WSL_SOURCE_GATE=PASS", + ] { + assert!( + launcher.contains(required), + "WSL launcher contract missing: {required}" + ); + } + + for required in [ + "set -euo pipefail", + "RIGOS_WSL_REPOSITORY_INVALID", "for tool in cargo rustc python3 bash sh git grep rg mktemp", "command -v \"$tool\"", "RIGOS_WSL_TOOL_MISSING", "for component in fmt clippy", "RIGOS_WSL_CARGO_COMPONENT_MISSING", "exec bash ./scripts/verify.sh", - "RIGOS_WSL_SOURCE_GATE=PASS", ] { assert!( - launcher.contains(required), - "WSL launcher contract missing: {required}" + entrypoint.contains(required), + "WSL entrypoint contract missing: {required}" ); } for forbidden in [ "[string]$Repository = (Split-Path -Parent $PSScriptRoot)", "wslpath -a $Repository", + "$PathConverter", + "$Shell = @'", + "bash -lc", "/mnt/d/TECHNICAL/dbyte-rigos", "curl | sh", "Invoke-WebRequest", @@ -80,7 +93,7 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { ] { assert!( !launcher.contains(forbidden), - "WSL launcher contains forbidden bootstrap, direct path argument, default expression, or hard-coded path: {forbidden}" + "WSL launcher contains forbidden bootstrap, multiline shell transport, direct path argument, default expression, or hard-coded path: {forbidden}" ); } } From c11a15f6725ffe4158d90db34a8b4175869d1b12 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:40:00 +0700 Subject: [PATCH 256/386] document file-based WSL gate transport --- docs/developer/windows-source-gate.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/developer/windows-source-gate.txt b/docs/developer/windows-source-gate.txt index 6ac5667c..9e20bd5c 100644 --- a/docs/developer/windows-source-gate.txt +++ b/docs/developer/windows-source-gate.txt @@ -49,7 +49,9 @@ Run the fail-closed launcher from the repository root: The launcher: - derives the repository from its own location - - converts the Windows path through wslpath + - normalizes Windows backslashes before invoking wslpath + - invokes scripts/verify-wsl-entrypoint.sh as a real file + - never transports multiline Bash source through a native command argument - sources $HOME/.cargo/env when present - checks cargo, rustc, python3, bash, sh, git, grep, rg, and mktemp - checks the Rust fmt and clippy cargo components From 6aee41f320e73c0455ddf79f322a20885e2ad2cd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:53:29 +0700 Subject: [PATCH 257/386] make state-ready ordering test semantic --- crates/rigos-config/tests/firstboot_tty.rs | 65 +++++++++++++++------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_tty.rs b/crates/rigos-config/tests/firstboot_tty.rs index 3fe2e886..414101a1 100644 --- a/crates/rigos-config/tests/firstboot_tty.rs +++ b/crates/rigos-config/tests/firstboot_tty.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::fs; use std::path::PathBuf; @@ -9,12 +10,19 @@ fn unit(name: &str) -> String { .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) } +fn directive_tokens<'a>(unit: &'a str, prefix: &str) -> BTreeSet<&'a str> { + unit.lines() + .filter_map(|line| line.strip_prefix(prefix)) + .flat_map(str::split_whitespace) + .collect() +} + #[test] fn firstboot_releases_tty1_to_getty_after_exit() { let unit = unit("rigos-firstboot.service"); assert!( - unit.lines().any(|line| line == "Before=getty@tty1.service"), + directive_tokens(&unit, "Before=").contains("getty@tty1.service"), "firstboot must finish before tty1 getty starts" ); assert!( @@ -36,12 +44,14 @@ fn firstboot_releases_tty1_to_getty_after_exit() { #[test] fn recovery_access_does_not_hang_up_the_following_firstboot_session() { let unit = unit("rigos-recovery-access.service"); + let before = directive_tokens(&unit, "Before="); - assert!( - unit.lines() - .any(|line| line.contains("Before=rigos-state-ready.service rigos-firstboot.service")), - "recovery access must complete before firstboot" - ); + for required in ["rigos-state-ready.service", "rigos-firstboot.service"] { + assert!( + before.contains(required), + "recovery access must complete before {required}" + ); + } assert!( !unit.lines().any(|line| line == "TTYVHangup=yes"), "recovery access must not hang up tty1 before firstboot starts" @@ -51,26 +61,41 @@ fn recovery_access_does_not_hang_up_the_following_firstboot_session() { #[test] fn state_ready_stays_out_of_the_local_fs_transaction() { let unit = unit("rigos-state-ready.service"); + let after = directive_tokens(&unit, "After="); + let requires = directive_tokens(&unit, "Requires="); + let before = directive_tokens(&unit, "Before="); assert!(!unit.lines().any(|line| line == "DefaultDependencies=no")); assert!( - !unit - .lines() - .any(|line| line.contains("Before=local-fs.target")) - ); - assert!( - unit.lines() - .any(|line| line == "WantedBy=multi-user.target") + !before.contains("local-fs.target"), + "state readiness must stay out of the local-fs transaction" ); assert!( - unit.lines() - .any(|line| line == "After=rigos-state.service rigos-recovery-access.service") + directive_tokens(&unit, "WantedBy=").contains("multi-user.target"), + "state readiness must be installed under multi-user.target" ); + + for required in ["rigos-state.service", "rigos-recovery-access.service"] { + assert!( + after.contains(required), + "state readiness must start after {required}" + ); + } assert!( - unit.lines() - .any(|line| line == "Requires=rigos-state.service") + requires.contains("rigos-state.service"), + "state readiness must require persistent state" ); - assert!(unit.lines().any(|line| { - line == "Before=rigos-profile-apply.service rigos-firstboot.service rigos-hugepages.service rigos-miner.service" - })); + + for required in [ + "rigos-ssh-hostkeys.service", + "rigos-profile-apply.service", + "rigos-firstboot.service", + "rigos-hugepages.service", + "rigos-miner.service", + ] { + assert!( + before.contains(required), + "state readiness must complete before {required}" + ); + } } From cf2b95bff42843c7be8040ecd646964feda0d5c5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 00:58:05 +0700 Subject: [PATCH 258/386] test exact runtime HTTP authority --- .../tests/hive_exit_runtime_publish.rs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/hive_exit_runtime_publish.rs b/crates/rigos-config/tests/hive_exit_runtime_publish.rs index 15e5decb..55b7dec2 100644 --- a/crates/rigos-config/tests/hive_exit_runtime_publish.rs +++ b/crates/rigos-config/tests/hive_exit_runtime_publish.rs @@ -77,6 +77,9 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { }], "http": { "enabled": false, + "host": "0.0.0.0", + "port": 9999, + "restricted": false, "access-token": "TOKEN_SENTINEL", "future_http": "HTTP_SENTINEL" } @@ -114,10 +117,26 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { assert_eq!(private["future_top"], "TOP_SENTINEL"); assert_eq!(private["cpu"]["future_cpu"], "CPU_SENTINEL"); assert_eq!(private["pools"][0]["future_pool"], "POOL_SENTINEL"); - assert_eq!(private["http"]["future_http"], "HTTP_SENTINEL"); assert_eq!(private["cpu"]["max-threads-hint"], 100); assert_eq!(private["cpu"]["rx"], serde_json::json!([-1, -1])); + let private_http = private["http"].as_object().expect("private HTTP authority"); + let private_http_keys = private_http.keys().cloned().collect::>(); + assert_eq!( + private_http_keys, + vec!["access-token", "enabled", "host", "port", "restricted"] + ); + assert_eq!(private_http["enabled"], true); + assert_eq!(private_http["host"], "127.0.0.1"); + assert_eq!(private_http["port"], 18080); + assert_eq!(private_http["restricted"], true); + let api_token = private_http["access-token"] + .as_str() + .expect("private API token"); + assert!(api_token.len() >= 32); + assert_ne!(api_token, "TOKEN_SENTINEL"); + assert!(private_http.get("future_http").is_none()); + let public = read_json(&runtime.join("xmrig-public.json")); let public_keys = public .as_object() @@ -144,6 +163,11 @@ fn staged_runtime_publication_is_allowlisted_atomic_and_fail_closed() { assert_eq!(public["randomx"]["huge-pages"].as_bool(), Some(true)); assert_eq!(public["pools"][0]["url"], "pool.test:1"); assert_eq!(public["rigos-public-view"]["construction"], "allowlist"); + assert_eq!(public["http"]["enabled"], true); + assert_eq!(public["http"]["host"], "127.0.0.1"); + assert_eq!(public["http"]["port"], 18080); + assert_eq!(public["http"]["restricted"], true); + assert!(public["http"].get("access-token").is_none()); let public_text = serde_json::to_string(&public).unwrap(); for sentinel in [ "TOP_SENTINEL", From a3bb28af3e10ae97ae91f787aca947936a46e781 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:05:21 +0700 Subject: [PATCH 259/386] exercise miner stability through authenticated API --- crates/rigos-config/tests/miner_stability.rs | 164 +++++++++++++++++-- 1 file changed, 152 insertions(+), 12 deletions(-) diff --git a/crates/rigos-config/tests/miner_stability.rs b/crates/rigos-config/tests/miner_stability.rs index d156d07a..5ac6b66f 100644 --- a/crates/rigos-config/tests/miner_stability.rs +++ b/crates/rigos-config/tests/miner_stability.rs @@ -2,11 +2,17 @@ use serde_json::Value; use std::fs; +use std::io::{Read, Write}; +use std::net::TcpListener; use std::os::unix::fs::{PermissionsExt, symlink}; use std::path::{Path, PathBuf}; use std::process::Command; +use std::thread; +use std::time::Duration; use uuid::Uuid; +const API_TOKEN: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + fn repo_path(path: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..") @@ -33,13 +39,64 @@ fn write_runtime_status(path: &Path, revision: &str) { .unwrap(); } +fn api_port_and_server(summary: Option) -> (u16, Option>) { + let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); + let port = listener.local_addr().unwrap().port(); + + let Some(summary) = summary else { + drop(listener); + return (port, None); + }; + + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + stream + .set_write_timeout(Some(Duration::from_secs(5))) + .unwrap(); + + let mut request = Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let count = stream.read(&mut buffer).unwrap(); + if count == 0 { + break; + } + request.extend_from_slice(&buffer[..count]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + assert!(request.len() <= 8192, "observer HTTP request is oversized"); + } + + let request = String::from_utf8(request).unwrap(); + assert!(request.starts_with("GET /2/summary HTTP/1.1\r\n")); + assert!(request.contains(&format!("Authorization: Bearer {API_TOKEN}\r\n"))); + + let body = serde_json::to_vec(&summary).unwrap(); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(headers.as_bytes()).unwrap(); + stream.write_all(&body).unwrap(); + stream.flush().unwrap(); + }); + + (port, Some(server)) +} + fn run_observer( root: &Path, systemctl: &Path, journalctl: &Path, systemctl_fixture: &Path, journal_fixture: &Path, + api_summary: Option, ) -> Value { + let (api_port, server) = api_port_and_server(api_summary); let status = Command::new("python3") .arg(repo_path( "build/usb/includes.chroot/usr/lib/rigos/rigos-miner-health", @@ -52,12 +109,83 @@ fn run_observer( .env("RIGOS_JOURNALCTL", journalctl) .env("RIGOS_SYSTEMCTL_FIXTURE", systemctl_fixture) .env("RIGOS_JOURNAL_FIXTURE", journal_fixture) + .env( + "RIGOS_XMRIG_API_TOKEN_PATH", + root.join("run/xmrig-api-token"), + ) + .env("RIGOS_XMRIG_API_HOST", "127.0.0.1") + .env("RIGOS_XMRIG_API_PORT", api_port.to_string()) + .env("RIGOS_XMRIG_API_TIMEOUT_SECONDS", "0.5") + .env("RIGOS_MINER_HEALTH_TEST_MODE", "1") .status() .unwrap(); + + if let Some(server) = server { + server.join().unwrap(); + } assert!(status.success()); serde_json::from_slice(&fs::read(root.join("run/miner-health-status.json")).unwrap()).unwrap() } +fn ready_summary() -> Value { + serde_json::json!({ + "uptime": 600, + "algo": "rx/0", + "hashrate": { + "total": [340.0, 341.0, 339.0], + "highest": 342.0 + }, + "connection": { + "pool": "pool.test:1", + "ip": "127.0.0.1", + "uptime": 590, + "accepted": 7, + "rejected": 0, + "failures": 0, + "ping": 20 + }, + "hugepages": [4, 4] + }) +} + +fn disconnected_summary() -> Value { + serde_json::json!({ + "uptime": 600, + "algo": "rx/0", + "hashrate": { + "total": [0.0, 341.0, 339.0], + "highest": 342.0 + }, + "connection": { + "accepted": 7, + "rejected": 0, + "failures": 1 + }, + "hugepages": [4, 4] + }) +} + +fn missing_current_hashrate_summary() -> Value { + serde_json::json!({ + "uptime": 600, + "algo": "rx/0", + "hashrate": { + "total": [null, 341.0, 339.0], + "highest": 342.0 + }, + "connection": { + "pool": "pool.test:1", + "ip": "127.0.0.1", + "uptime": 590, + "accepted": 7, + "rejected": 0, + "failures": 0, + "ping": 20 + }, + "hugepages": [4, 4] + }) +} + #[test] fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() { let root = std::env::temp_dir().join(format!("rigos-miner-health-{}", Uuid::new_v4())); @@ -68,6 +196,12 @@ fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() fs::write(root.join("boot-id"), "boot-test\n").unwrap(); fs::write(root.join("proc/uptime"), "1000.0 0.0\n").unwrap(); fs::write(root.join("proc/123/stat"), "123 (xmrig) S\n").unwrap(); + fs::write(root.join("run/xmrig-api-token"), format!("{API_TOKEN}\n")).unwrap(); + fs::set_permissions( + root.join("run/xmrig-api-token"), + fs::Permissions::from_mode(0o600), + ) + .unwrap(); write_runtime_status(&root.join("run/runtime-config-status.json"), "r1"); let systemctl_fixture = root.join("systemctl.txt"); @@ -92,58 +226,63 @@ fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() write_executable(&journalctl, "#!/bin/sh\ncat \"$RIGOS_JOURNAL_FIXTURE\"\n"); write_executable(&journalctl_fail, "#!/bin/sh\nexit 1\n"); - fs::write( - &journal_fixture, - concat!( - "miner speed 10s/60s/15m 340.0 341.0 n/a H/s\n", - "cpu accepted (7/0) diff 10000\n" - ), - ) - .unwrap(); + fs::write(&journal_fixture, "net connect error: stale journal evidence\n").unwrap(); let ready = run_observer( &root, &systemctl, &journalctl, &systemctl_fixture, &journal_fixture, + Some(ready_summary()), ); assert_eq!(ready["state"], "ready"); + assert_eq!(ready["reason"], Value::Null); assert_eq!(ready["unit"]["restart_count"], 2); + assert_eq!(ready["evidence"]["source"], "xmrig_http_api"); assert_eq!(ready["evidence"]["accepted_shares"], 7); assert_eq!(ready["evidence"]["rejected_shares"], 0); assert_eq!(ready["remediation"], "observe_only"); - fs::write(&journal_fixture, "net connect error: connection refused\n").unwrap(); + fs::write( + &journal_fixture, + "miner speed 10s/60s/15m 340.0 341.0 339.0 H/s\n", + ) + .unwrap(); let waiting = run_observer( &root, &systemctl, &journalctl, &systemctl_fixture, &journal_fixture, + Some(disconnected_summary()), ); assert_eq!(waiting["state"], "waiting_external"); assert_eq!(waiting["reason"], "pool_or_network_unavailable"); + assert_eq!(waiting["evidence"]["source"], "xmrig_http_api"); - fs::write(&journal_fixture, "").unwrap(); let degraded = run_observer( &root, &systemctl, &journalctl, &systemctl_fixture, &journal_fixture, + Some(missing_current_hashrate_summary()), ); assert_eq!(degraded["state"], "degraded"); - assert_eq!(degraded["reason"], "no_recent_speed_evidence"); + assert_eq!(degraded["reason"], "current_hashrate_unavailable"); + assert_eq!(degraded["evidence"]["source"], "xmrig_http_api"); + fs::write(&journal_fixture, "").unwrap(); let unknown = run_observer( &root, &systemctl, &journalctl_fail, &systemctl_fixture, &journal_fixture, + None, ); assert_eq!(unknown["state"], "unknown"); - assert_eq!(unknown["reason"], "journal_unavailable"); + assert_eq!(unknown["reason"], "api_unavailable"); assert_eq!(unknown["evidence"]["journal_available"], false); write_runtime_status(&root.join("run/runtime-config-status.json"), "r2"); @@ -153,6 +292,7 @@ fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() &journalctl, &systemctl_fixture, &journal_fixture, + Some(ready_summary()), ); assert_eq!(blocked["state"], "blocked"); assert_eq!(blocked["reason"], "runtime_revision_mismatch"); From 9e73cb747c4f47bcd6e3abab4e005a706966210a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:14:38 +0700 Subject: [PATCH 260/386] isolate WSL Python bytecode artifacts --- scripts/verify-wsl-entrypoint.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/verify-wsl-entrypoint.sh b/scripts/verify-wsl-entrypoint.sh index f4c3ba35..55090d9f 100644 --- a/scripts/verify-wsl-entrypoint.sh +++ b/scripts/verify-wsl-entrypoint.sh @@ -44,6 +44,14 @@ if [[ "$missing" -ne 0 ]]; then exit 127 fi +pycache_root=$(mktemp -d) +cleanup() { + rm -rf "$pycache_root" +} +trap cleanup EXIT HUP INT TERM +export PYTHONPYCACHEPREFIX="$pycache_root" +export PYTHONDONTWRITEBYTECODE=1 + printf 'RIGOS_WSL_REPO=%s\n' "$repo" printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" -exec bash ./scripts/verify.sh +bash ./scripts/verify.sh From d0a041c663bfef3166fad1ee7d095e3a30208b18 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:14:45 +0700 Subject: [PATCH 261/386] ignore Python bytecode artifacts --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 19d2cb47..11610b1e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ *.age.partial *.pem *.key +__pycache__/ +*.py[cod] From 407d55508e19a266981c0193461f2d6523963f1a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:14:59 +0700 Subject: [PATCH 262/386] guard WSL pycache isolation --- crates/rigos-config/tests/wsl_source_gate.rs | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index d050791a..21bb22fb 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -72,7 +72,11 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { "RIGOS_WSL_TOOL_MISSING", "for component in fmt clippy", "RIGOS_WSL_CARGO_COMPONENT_MISSING", - "exec bash ./scripts/verify.sh", + "pycache_root=$(mktemp -d)", + "trap cleanup EXIT HUP INT TERM", + "export PYTHONPYCACHEPREFIX=\"$pycache_root\"", + "export PYTHONDONTWRITEBYTECODE=1", + "bash ./scripts/verify.sh", ] { assert!( entrypoint.contains(required), @@ -90,10 +94,22 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { "curl | sh", "Invoke-WebRequest", "rustup-init", + "exec bash ./scripts/verify.sh", ] { assert!( - !launcher.contains(forbidden), - "WSL launcher contains forbidden bootstrap, multiline shell transport, direct path argument, default expression, or hard-coded path: {forbidden}" + !launcher.contains(forbidden) && !entrypoint.contains(forbidden), + "WSL gate contains forbidden bootstrap, multiline shell transport, direct path argument, cleanup-bypassing exec, default expression, or hard-coded path: {forbidden}" + ); + } +} + +#[test] +fn python_bytecode_artifacts_are_ignored() { + let ignore = fs::read_to_string(repo_path(".gitignore")).expect("read repository ignore rules"); + for required in ["__pycache__/", "*.py[cod]"] { + assert!( + ignore.lines().any(|line| line == required), + "Python bytecode ignore rule missing: {required}" ); } } From cb9e88220121f8b3403b860ce2b20504b2acb9a1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:15:28 +0700 Subject: [PATCH 263/386] format authenticated miner stability integration --- crates/rigos-config/tests/miner_stability.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/miner_stability.rs b/crates/rigos-config/tests/miner_stability.rs index 5ac6b66f..9ac3c585 100644 --- a/crates/rigos-config/tests/miner_stability.rs +++ b/crates/rigos-config/tests/miner_stability.rs @@ -226,7 +226,11 @@ fn miner_health_distinguishes_ready_external_wait_degraded_blocked_and_unknown() write_executable(&journalctl, "#!/bin/sh\ncat \"$RIGOS_JOURNAL_FIXTURE\"\n"); write_executable(&journalctl_fail, "#!/bin/sh\nexit 1\n"); - fs::write(&journal_fixture, "net connect error: stale journal evidence\n").unwrap(); + fs::write( + &journal_fixture, + "net connect error: stale journal evidence\n", + ) + .unwrap(); let ready = run_observer( &root, &systemctl, From 01a0840a79800c534de473a3ce409d7cbea7ea08 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:19:57 +0700 Subject: [PATCH 264/386] lock lifecycle scripts to LF --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index aaf2ab4c..3b002266 100644 --- a/.gitattributes +++ b/.gitattributes @@ -19,3 +19,4 @@ build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-miner-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-remote-* text eol=lf +build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-* text eol=lf From 695bbcdedb2bb05faf446d6cb2efffacd70bf3a8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:20:06 +0700 Subject: [PATCH 265/386] mark lifecycle script LF contract --- build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles b/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles index aae63039..5d452324 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles @@ -1,4 +1,5 @@ #!/bin/sh +# Repository contract: LF line endings are required for appliance execution. set -eu cycles="${1:-20}" From 8741c9d25b064d20da74f6c98d24c72f9e433070 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:20:13 +0700 Subject: [PATCH 266/386] cover lifecycle LF and POSIX syntax --- crates/rigos-config/tests/hive_exit_shell_syntax.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index f8087c5e..71cbd5fb 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -13,6 +13,7 @@ fn runtime_authority_shells_are_lf_and_parse_cleanly() { for path in [ "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-publish", "build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-authority", + "build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-cycles", ] { let full_path = repo_path(path); let bytes = fs::read(&full_path).unwrap(); @@ -28,3 +29,11 @@ fn runtime_authority_shells_are_lf_and_parse_cleanly() { assert!(status.success(), "shell syntax failed: {path}"); } } + +#[test] +fn lifecycle_shell_has_explicit_lf_attribute() { + let attributes = fs::read_to_string(repo_path(".gitattributes")).unwrap(); + assert!(attributes.lines().any(|line| { + line == "build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-* text eol=lf" + })); +} From 2b98c52acd9e30b8755d749a556e4baf6165077b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:27:09 +0700 Subject: [PATCH 267/386] remove obsolete checkout literal from WSL gate test --- crates/rigos-config/tests/wsl_source_gate.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/wsl_source_gate.rs b/crates/rigos-config/tests/wsl_source_gate.rs index 21bb22fb..1f8dbc8b 100644 --- a/crates/rigos-config/tests/wsl_source_gate.rs +++ b/crates/rigos-config/tests/wsl_source_gate.rs @@ -84,18 +84,20 @@ fn wsl_launcher_is_path_safe_and_fail_closed() { ); } - for forbidden in [ + let hard_coded_checkout = ["/mnt/d/TECHNICAL/", "dbyte", "-rigos"].concat(); + let forbidden = [ "[string]$Repository = (Split-Path -Parent $PSScriptRoot)", "wslpath -a $Repository", "$PathConverter", "$Shell = @'", "bash -lc", - "/mnt/d/TECHNICAL/dbyte-rigos", + hard_coded_checkout.as_str(), "curl | sh", "Invoke-WebRequest", "rustup-init", "exec bash ./scripts/verify.sh", - ] { + ]; + for forbidden in forbidden { assert!( !launcher.contains(forbidden) && !entrypoint.contains(forbidden), "WSL gate contains forbidden bootstrap, multiline shell transport, direct path argument, cleanup-bypassing exec, default expression, or hard-coded path: {forbidden}" From 4c8b4acf0d8298b255d54d7498638d6bcb3f4424 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:27:20 +0700 Subject: [PATCH 268/386] remove obsolete namespace from Windows source gate docs --- docs/developer/windows-source-gate.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/developer/windows-source-gate.txt b/docs/developer/windows-source-gate.txt index 9e20bd5c..0ed96166 100644 --- a/docs/developer/windows-source-gate.txt +++ b/docs/developer/windows-source-gate.txt @@ -1,5 +1,5 @@ -DBYTE RIGOS WINDOWS SOURCE GATE -=============================== +RIGOS WINDOWS SOURCE GATE +========================= PURPOSE ------- @@ -78,9 +78,10 @@ PowerShell. DIRECT LINUX COMMANDS --------------------- -Inside an actual WSL/Linux shell with Linux Rust installed: +Inside an actual WSL/Linux shell with Linux Rust installed, change to the +repository checkout selected by the operator: - cd /mnt/d/TECHNICAL/dbyte-rigos + cd /path/to/rigos-checkout source "$HOME/.cargo/env" cargo --version bash ./scripts/verify.sh From c239e9d40c58005bf362ed72f968ccc62cb28fcf Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:33:18 +0700 Subject: [PATCH 269/386] add token-aware runtime dependency scanner --- scripts/verify-runtime-dependencies.sh | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 scripts/verify-runtime-dependencies.sh diff --git a/scripts/verify-runtime-dependencies.sh b/scripts/verify-runtime-dependencies.sh new file mode 100644 index 00000000..8e5ad0ff --- /dev/null +++ b/scripts/verify-runtime-dependencies.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +target=${1:-build/usb/includes.chroot} +if [[ ! -d "$target" ]]; then + echo "runtime dependency scan target is missing: $target" >&2 + exit 66 +fi + +pattern='(^|[^[:alnum:]_])(curl|wget|Invoke-WebRequest|latest)([^[:alnum:]_]|$)' + +set +e +rg -n -i -- "$pattern" "$target" +status=$? +set -e + +case "$status" in + 0) + echo "runtime miner download or floating dependency detected" >&2 + exit 1 + ;; + 1) + exit 0 + ;; + *) + echo "runtime dependency scan failed: exit $status" >&2 + exit "$status" + ;; +esac From d5ad66324b7b31435bfb9082e303dd078116581a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:33:32 +0700 Subject: [PATCH 270/386] test token-aware runtime dependency scan --- .../tests/hive_exit_shell_syntax.rs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index 71cbd5fb..f5ff34dc 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -1,6 +1,7 @@ use std::fs; use std::path::PathBuf; use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; fn repo_path(path: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -37,3 +38,68 @@ fn lifecycle_shell_has_explicit_lf_attribute() { line == "build/usb/includes.chroot/usr/lib/rigos/rigos-lifecycle-* text eol=lf" })); } + +#[cfg(unix)] +#[test] +fn runtime_dependency_scan_is_token_aware_and_fail_closed() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-runtime-dependency-scan-{unique}")); + fs::create_dir_all(&root).unwrap(); + let fixture = root.join("fixture.txt"); + let scanner = repo_path("scripts/verify-runtime-dependencies.sh"); + + fs::write( + &fixture, + "latest_journal_signal\nlatest_marker_index\ncalculate_latest_value\n", + ) + .unwrap(); + let benign = Command::new("bash") + .arg(&scanner) + .arg(&root) + .status() + .unwrap(); + assert!( + benign.success(), + "benign identifiers must not look like floating dependencies" + ); + + for dangerous in [ + "curl -fsSL https://example.invalid/miner\n", + "wget https://example.invalid/miner\n", + "Invoke-WebRequest https://example.invalid/miner\n", + "image:latest\n", + "https://example.invalid/releases/latest/download/miner\n", + "version=latest\n", + "xmrig-latest.tar.gz\n", + ] { + fs::write(&fixture, dangerous).unwrap(); + let denied = Command::new("bash") + .arg(&scanner) + .arg(&root) + .status() + .unwrap(); + assert_eq!( + denied.code(), + Some(1), + "floating dependency was not rejected: {dangerous:?}" + ); + } + + let missing = Command::new("bash") + .arg(&scanner) + .arg(root.join("missing")) + .status() + .unwrap(); + assert_eq!(missing.code(), Some(66)); + + let verify = fs::read_to_string(repo_path("scripts/verify.sh")).unwrap(); + assert!(verify.contains( + "bash scripts/verify-runtime-dependencies.sh build/usb/includes.chroot" + )); + assert!(!verify.contains("curl|wget|Invoke-WebRequest|latest")); + + let _ = fs::remove_dir_all(root); +} From ec2ce3010af88cee37ee9d09a34ed57f41db3ad8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:34:14 +0700 Subject: [PATCH 271/386] use token-aware runtime dependency scanner --- scripts/verify.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/verify.sh b/scripts/verify.sh index 5fae12cc..38baa2cf 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -293,10 +293,7 @@ if rg -n 'if\s+.*(moneroocean|2miners|nicehash|supportxmr|herominers|hashvault|n echo "pool-name conditional leaked into runtime core" >&2 exit 1 fi -if rg -n 'curl|wget|Invoke-WebRequest|latest' build/usb/includes.chroot; then - echo "runtime miner download or floating dependency detected" >&2 - exit 1 -fi +bash scripts/verify-runtime-dependencies.sh build/usb/includes.chroot for required in build/usb/THIRD_PARTY_NOTICES docs/miner-provenance.md docs/third-party-components.md; do [[ -s "$required" ]] || { echo "missing third-party disclosure: $required" >&2; exit 1; } done From 01bf2c1766d141d30292a63c920dcd2610af6b82 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:37:05 +0700 Subject: [PATCH 272/386] format runtime dependency scanner test --- crates/rigos-config/tests/hive_exit_shell_syntax.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index f5ff34dc..585f87c6 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -96,9 +96,9 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { assert_eq!(missing.code(), Some(66)); let verify = fs::read_to_string(repo_path("scripts/verify.sh")).unwrap(); - assert!(verify.contains( - "bash scripts/verify-runtime-dependencies.sh build/usb/includes.chroot" - )); + assert!( + verify.contains("bash scripts/verify-runtime-dependencies.sh build/usb/includes.chroot") + ); assert!(!verify.contains("curl|wget|Invoke-WebRequest|latest")); let _ = fs::remove_dir_all(root); From 1ffb9be2343618596942df372ed1dc01dc82b9e1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:41:10 +0700 Subject: [PATCH 273/386] execute runtime dependency scanner directly in tests --- .../tests/hive_exit_shell_syntax.rs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index 585f87c6..3962028a 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -1,4 +1,6 @@ use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; @@ -49,18 +51,22 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { let root = std::env::temp_dir().join(format!("rigos-runtime-dependency-scan-{unique}")); fs::create_dir_all(&root).unwrap(); let fixture = root.join("fixture.txt"); - let scanner = repo_path("scripts/verify-runtime-dependencies.sh"); + let scanner = root.join("verify-runtime-dependencies.sh"); + fs::copy( + repo_path("scripts/verify-runtime-dependencies.sh"), + &scanner, + ) + .unwrap(); + let mut permissions = fs::metadata(&scanner).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&scanner, permissions).unwrap(); fs::write( &fixture, "latest_journal_signal\nlatest_marker_index\ncalculate_latest_value\n", ) .unwrap(); - let benign = Command::new("bash") - .arg(&scanner) - .arg(&root) - .status() - .unwrap(); + let benign = Command::new(&scanner).arg(&root).status().unwrap(); assert!( benign.success(), "benign identifiers must not look like floating dependencies" @@ -76,11 +82,7 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { "xmrig-latest.tar.gz\n", ] { fs::write(&fixture, dangerous).unwrap(); - let denied = Command::new("bash") - .arg(&scanner) - .arg(&root) - .status() - .unwrap(); + let denied = Command::new(&scanner).arg(&root).status().unwrap(); assert_eq!( denied.code(), Some(1), @@ -88,8 +90,7 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { ); } - let missing = Command::new("bash") - .arg(&scanner) + let missing = Command::new(&scanner) .arg(root.join("missing")) .status() .unwrap(); From 527fdbdc49f7b94b26aab2c2386463d261672cf4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 01:44:46 +0700 Subject: [PATCH 274/386] separate scanner from dependency scan fixture --- crates/rigos-config/tests/hive_exit_shell_syntax.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/rigos-config/tests/hive_exit_shell_syntax.rs b/crates/rigos-config/tests/hive_exit_shell_syntax.rs index 3962028a..dff771c4 100644 --- a/crates/rigos-config/tests/hive_exit_shell_syntax.rs +++ b/crates/rigos-config/tests/hive_exit_shell_syntax.rs @@ -49,8 +49,9 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { .unwrap() .as_nanos(); let root = std::env::temp_dir().join(format!("rigos-runtime-dependency-scan-{unique}")); - fs::create_dir_all(&root).unwrap(); - let fixture = root.join("fixture.txt"); + let scan_root = root.join("scan-root"); + fs::create_dir_all(&scan_root).unwrap(); + let fixture = scan_root.join("fixture.txt"); let scanner = root.join("verify-runtime-dependencies.sh"); fs::copy( repo_path("scripts/verify-runtime-dependencies.sh"), @@ -66,7 +67,7 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { "latest_journal_signal\nlatest_marker_index\ncalculate_latest_value\n", ) .unwrap(); - let benign = Command::new(&scanner).arg(&root).status().unwrap(); + let benign = Command::new(&scanner).arg(&scan_root).status().unwrap(); assert!( benign.success(), "benign identifiers must not look like floating dependencies" @@ -82,7 +83,7 @@ fn runtime_dependency_scan_is_token_aware_and_fail_closed() { "xmrig-latest.tar.gz\n", ] { fs::write(&fixture, dangerous).unwrap(); - let denied = Command::new(&scanner).arg(&root).status().unwrap(); + let denied = Command::new(&scanner).arg(&scan_root).status().unwrap(); assert_eq!( denied.code(), Some(1), From 713ce162304c24befec96f765dfe5eaabf8a5e67 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 06:49:58 +0700 Subject: [PATCH 275/386] Add 2009-style firstboot whiptail wrapper --- .../usr/lib/rigos/rigos-firstboot-whiptail | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail b/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail new file mode 100644 index 00000000..933cea07 --- /dev/null +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail @@ -0,0 +1,29 @@ +#!/bin/sh +# RIGOS firstboot presentation wrapper. +# Keeps the configuration engine and dialog return-code contract unchanged. +set -eu + +TERM=${TERM:-linux} +export TERM + +NEWT_COLORS=${NEWT_COLORS:-'root=white,blue:border=blue,lightgray:window=black,lightgray:shadow=black,black:title=white,blue:button=black,lightgray:actbutton=white,blue:compactbutton=black,lightgray:checkbox=black,lightgray:actcheckbox=white,blue:entry=black,white:disentry=gray,lightgray:label=black,lightgray:listbox=black,lightgray:actlistbox=white,blue:sellistbox=black,cyan:actsellistbox=white,blue:textbox=black,lightgray:acttextbox=white,blue:helpline=white,blue:roottext=white,blue'} +export NEWT_COLORS + +backtitle=${RIGOS_FIRSTBOOT_BACKTITLE:-'RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY'} + +case " $* " in + *' --menu '*) + set -- --ok-button SELECT --cancel-button BACK "$@" + ;; + *' --yesno '*) + set -- --yes-button APPLY --no-button BACK "$@" + ;; + *' --inputbox '*|*' --passwordbox '*) + set -- --ok-button NEXT --cancel-button BACK "$@" + ;; + *' --msgbox '*) + set -- --ok-button CONTINUE "$@" + ;; +esac + +exec /usr/bin/whiptail --backtitle "$backtitle" "$@" From 13220fc98a38e30fc0b3d8e0f62d339d39fdd600 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 06:50:06 +0700 Subject: [PATCH 276/386] Wire firstboot console theme --- .../system/rigos-firstboot.service.d/2009-console-theme.conf | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf new file mode 100644 index 00000000..7c5a3fc4 --- /dev/null +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf @@ -0,0 +1,3 @@ +[Service] +Environment=RIGOS_WHIPTAIL=/usr/lib/rigos/rigos-firstboot-whiptail +Environment="RIGOS_FIRSTBOOT_BACKTITLE=RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY" From bf9890c8111fc7eda8d75205b4783e0a527ee152 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 06:50:17 +0700 Subject: [PATCH 277/386] Install firstboot theme wrapper as executable --- build/usb/hooks/010-rigos.chroot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/hooks/010-rigos.chroot b/build/usb/hooks/010-rigos.chroot index acc1d24b..e944fc5a 100644 --- a/build/usb/hooks/010-rigos.chroot +++ b/build/usb/hooks/010-rigos.chroot @@ -9,7 +9,7 @@ install -d -m 0755 /usr/lib/rigos /usr/local/bin systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf rm -f /etc/ssh/ssh_host_*_key /etc/ssh/ssh_host_*_key.pub -chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-randomx-msr /usr/lib/rigos/rigos-ssh-hostkeys /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig +chmod 0755 /usr/local/bin/rigosd /usr/local/bin/rigosctl /usr/local/sbin/rigos-firstboot /usr/local/sbin/rigos-recovery-access /usr/local/sbin/rigos-state-orchestrate /usr/lib/rigos/rigos-firstboot-whiptail /usr/lib/rigos/rigos-miner-gate /usr/lib/rigos/rigos-miner-health /usr/lib/rigos/rigos-runtime-render /usr/lib/rigos/rigos-runtime-publish /usr/lib/rigos/rigos-runtime-gate /usr/lib/rigos/rigos-randomx-msr /usr/lib/rigos/rigos-ssh-hostkeys /usr/lib/rigos/rigos-state-init /usr/lib/rigos/rigos-state-ready /usr/lib/rigos/rigos-config /usr/lib/rigos/rigos-performance /usr/lib/rigos/rigos-lifecycle-cycles /usr/lib/rigos/rigos-identity-seed /usr/lib/rigos/xmrig systemctl enable NetworkManager.service ssh.service rigos-state.service rigos-recovery-access.service rigos-state-ready.service rigos-ssh-hostkeys.service rigos-profile-apply.service rigos-runtime-render.service rigos-hugepages.service rigos-firstboot.service rigos-randomx-msr.service rigos-miner.service rigos-miner-health.timer tmp.mount systemctl disable ssh.socket 2>/dev/null || true systemctl disable apt-daily.timer apt-daily-upgrade.timer logrotate.timer fstrim.timer 2>/dev/null || true From 44a8b68a3a52201aef5c03487f2dc3d320c46042 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 06:50:28 +0700 Subject: [PATCH 278/386] Add firstboot console theme wiring tests --- crates/rigos-config/tests/firstboot_theme.rs | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 crates/rigos-config/tests/firstboot_theme.rs diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs new file mode 100644 index 00000000..c2f82da9 --- /dev/null +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -0,0 +1,62 @@ +use std::fs; +use std::path::PathBuf; + +fn repo_file(path: &str) -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path); + fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) +} + +#[test] +fn firstboot_uses_the_dedicated_console_theme_wrapper() { + let drop_in = repo_file( + "build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf", + ); + + assert!( + drop_in.contains( + "Environment=RIGOS_WHIPTAIL=/usr/lib/rigos/rigos-firstboot-whiptail" + ), + "firstboot must select the dedicated presentation wrapper" + ); + assert!( + drop_in.contains("RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY"), + "firstboot must publish the static non-secret backtitle" + ); +} + +#[test] +fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { + let wrapper = repo_file( + "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail", + ); + + assert!(wrapper.is_ascii(), "console theme must remain ASCII-only"); + assert!(wrapper.contains("NEWT_COLORS=")); + assert!(wrapper.contains("root=white,blue")); + assert!(wrapper.contains("window=black,lightgray")); + assert!(wrapper.contains("actlistbox=white,blue")); + assert!(wrapper.contains("--backtitle")); + assert!(wrapper.contains("--ok-button SELECT --cancel-button BACK")); + assert!(wrapper.contains("--yes-button APPLY --no-button BACK")); + assert!(wrapper.contains("exec /usr/bin/whiptail")); + assert!( + !wrapper.contains("xterm") + && !wrapper.contains("Xorg") + && !wrapper.contains("wayland") + && !wrapper.contains("electron"), + "theme must not introduce a graphical runtime" + ); +} + +#[test] +fn image_hook_installs_the_theme_wrapper_as_executable() { + let hook = repo_file("build/usb/hooks/010-rigos.chroot"); + + assert!( + hook.contains("/usr/lib/rigos/rigos-firstboot-whiptail"), + "image construction must install the wrapper as executable" + ); +} From 7030f3ba8586e4fe3b7c54ac81182431049b49db Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 07:08:07 +0700 Subject: [PATCH 279/386] Harden firstboot theme backend execution --- .../usr/lib/rigos/rigos-firstboot-whiptail | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail b/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail index 933cea07..529fa7d2 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail @@ -10,6 +10,12 @@ NEWT_COLORS=${NEWT_COLORS:-'root=white,blue:border=blue,lightgray:window=black,l export NEWT_COLORS backtitle=${RIGOS_FIRSTBOOT_BACKTITLE:-'RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY'} +whiptail_real=${RIGOS_WHIPTAIL_REAL:-/usr/bin/whiptail} + +if [ ! -x "$whiptail_real" ]; then + echo "rigos-firstboot-whiptail: backend is not executable: $whiptail_real" >&2 + exit 127 +fi case " $* " in *' --menu '*) @@ -26,4 +32,4 @@ case " $* " in ;; esac -exec /usr/bin/whiptail --backtitle "$backtitle" "$@" +exec "$whiptail_real" --backtitle "$backtitle" "$@" From 47f10f21518b19e6178968cc02618cbda47d4e9b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 07:08:24 +0700 Subject: [PATCH 280/386] Update theme tests for backend hardening --- crates/rigos-config/tests/firstboot_theme.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index c2f82da9..a4a7d952 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -41,7 +41,9 @@ fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { assert!(wrapper.contains("--backtitle")); assert!(wrapper.contains("--ok-button SELECT --cancel-button BACK")); assert!(wrapper.contains("--yes-button APPLY --no-button BACK")); - assert!(wrapper.contains("exec /usr/bin/whiptail")); + assert!(wrapper.contains("RIGOS_WHIPTAIL_REAL:-/usr/bin/whiptail")); + assert!(wrapper.contains("exec \"$whiptail_real\"")); + assert!(wrapper.contains("backend is not executable")); assert!( !wrapper.contains("xterm") && !wrapper.contains("Xorg") From b98e9e5b8b680149e1fd2050b352b25dcee97d90 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 07:08:56 +0700 Subject: [PATCH 281/386] Add behavioral firstboot theme wrapper test --- crates/rigos-config/tests/firstboot_theme.rs | 91 +++++++++++++++++++- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index a4a7d952..03d44ab5 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -1,10 +1,14 @@ use std::fs; use std::path::PathBuf; -fn repo_file(path: &str) -> String { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..") - .join(path); + .join(path) +} + +fn repo_file(path: &str) -> String { + let path = repo_path(path); fs::read_to_string(&path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) } @@ -62,3 +66,84 @@ fn image_hook_installs_the_theme_wrapper_as_executable() { "image construction must install the wrapper as executable" ); } + +#[cfg(unix)] +#[test] +fn firstboot_theme_wrapper_maps_buttons_and_preserves_exit_status() { + use std::os::unix::fs::PermissionsExt; + use std::process::Command; + use std::time::{SystemTime, UNIX_EPOCH}; + + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock is before Unix epoch") + .as_nanos(); + let temporary = std::env::temp_dir().join(format!( + "rigos-firstboot-theme-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(&temporary).expect("failed to create theme test directory"); + + let backend = temporary.join("fake-whiptail"); + let capture = temporary.join("arguments.txt"); + fs::write( + &backend, + "#!/bin/sh\nprintf '%s\\n' \"$@\" >\"$RIGOS_THEME_CAPTURE\"\nexit \"${RIGOS_THEME_EXIT:-0}\"\n", + ) + .expect("failed to write fake whiptail backend"); + let mut permissions = fs::metadata(&backend) + .expect("failed to stat fake whiptail backend") + .permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&backend, permissions) + .expect("failed to make fake whiptail backend executable"); + + let wrapper = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail", + ); + let status = Command::new("sh") + .arg(&wrapper) + .args([ + "--title", + "RIGOS FIRST BOOT", + "--menu", + "Select Flight Sheet", + "20", + "76", + "2", + "manual", + "Configure manually", + "none", + "Leave mining unconfigured", + ]) + .env("RIGOS_WHIPTAIL_REAL", &backend) + .env("RIGOS_THEME_CAPTURE", &capture) + .env("RIGOS_THEME_EXIT", "7") + .env("RIGOS_FIRSTBOOT_BACKTITLE", "TEST BACKTITLE") + .status() + .expect("failed to execute firstboot theme wrapper"); + + assert_eq!(status.code(), Some(7), "wrapper must preserve backend exit status"); + + let arguments = fs::read_to_string(&capture) + .expect("fake whiptail backend did not capture arguments"); + let arguments: Vec<_> = arguments.lines().collect(); + assert_eq!( + &arguments[..8], + [ + "--backtitle", + "TEST BACKTITLE", + "--ok-button", + "SELECT", + "--cancel-button", + "BACK", + "--title", + "RIGOS FIRST BOOT", + ] + ); + assert!(arguments.windows(2).any(|pair| pair == ["--menu", "Select Flight Sheet"])); + assert!(arguments.windows(2).any(|pair| pair == ["manual", "Configure manually"])); + assert!(arguments.windows(2).any(|pair| pair == ["none", "Leave mining unconfigured"])); + + fs::remove_dir_all(&temporary).expect("failed to clean theme test directory"); +} From 2dc0b51160fe0b7ad0e5e45bd9f39d7a63b542ae Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 07:09:05 +0700 Subject: [PATCH 282/386] Add local firstboot theme preview harness --- scripts/preview-firstboot-theme.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 scripts/preview-firstboot-theme.sh diff --git a/scripts/preview-firstboot-theme.sh b/scripts/preview-firstboot-theme.sh new file mode 100644 index 00000000..5ab24c67 --- /dev/null +++ b/scripts/preview-firstboot-theme.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +wrapper="$root/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail" + +if [[ ! -f "$wrapper" ]]; then + echo "preview-firstboot-theme: wrapper is missing: $wrapper" >&2 + exit 1 +fi + +if [[ ! -x /usr/bin/whiptail ]]; then + echo "preview-firstboot-theme: /usr/bin/whiptail is not installed" >&2 + exit 127 +fi + +RIGOS_WHIPTAIL_REAL=/usr/bin/whiptail \ +RIGOS_FIRSTBOOT_BACKTITLE='RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY' \ +sh "$wrapper" \ + --title 'RIGOS FIRST BOOT' \ + --menu $'SELECT FLIGHT SHEET\n\nChoose a local mining configuration source.\n[UP/DOWN] MOVE [ENTER] SELECT [ESC] BACK' \ + 20 76 6 \ + manual 'Configure this node manually' \ + none 'Leave mining unconfigured' \ + native:xmr 'Use native XMR flight sheet' \ + import:legacy 'Import an external flight sheet' From 3b868af7bc102cb2e214dc954767e241b2fd335a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:24:37 +0700 Subject: [PATCH 283/386] Add authoritative Linux source gate workflow --- .github/workflows/source-gate.yml | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/source-gate.yml diff --git a/.github/workflows/source-gate.yml b/.github/workflows/source-gate.yml new file mode 100644 index 00000000..98293b3a --- /dev/null +++ b/.github/workflows/source-gate.yml @@ -0,0 +1,60 @@ +name: RIGOS Source Gate + +on: + pull_request: + branches: + - performance/randomx-sustained-hash + - stability/hive-exit-gates + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: source-gate-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Authoritative Linux source verification + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Check out exact pull-request head + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install required host tools + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install --yes --no-install-recommends ripgrep + + - name: Install locked Rust toolchain + run: | + set -euo pipefail + rustup toolchain install 1.85.1 \ + --profile minimal \ + --component rustfmt \ + --component clippy + rustup override set 1.85.1 + rustc --version + cargo --version + cargo fmt --version + cargo clippy --version + + - name: Run authoritative RIGOS source gate + env: + CARGO_TERM_COLOR: always + run: | + set -euo pipefail + bash ./scripts/verify-wsl-entrypoint.sh "$GITHUB_WORKSPACE" + + - name: Record exact verified revision + if: always() + run: | + set -euo pipefail + printf 'RIGOS_VERIFIED_HEAD=%s\n' "$(git rev-parse HEAD)" + git status --short From 6bc5f6093f8247a3f7f06658acd92e137b4a3351 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:25:43 +0700 Subject: [PATCH 284/386] Expand firstboot theme preview modes --- scripts/preview-firstboot-theme.sh | 84 ++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/scripts/preview-firstboot-theme.sh b/scripts/preview-firstboot-theme.sh index 5ab24c67..255a3c6c 100644 --- a/scripts/preview-firstboot-theme.sh +++ b/scripts/preview-firstboot-theme.sh @@ -3,6 +3,7 @@ set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) wrapper="$root/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail" +mode=${1:-menu} if [[ ! -f "$wrapper" ]]; then echo "preview-firstboot-theme: wrapper is missing: $wrapper" >&2 @@ -14,13 +15,76 @@ if [[ ! -x /usr/bin/whiptail ]]; then exit 127 fi -RIGOS_WHIPTAIL_REAL=/usr/bin/whiptail \ -RIGOS_FIRSTBOOT_BACKTITLE='RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY' \ -sh "$wrapper" \ - --title 'RIGOS FIRST BOOT' \ - --menu $'SELECT FLIGHT SHEET\n\nChoose a local mining configuration source.\n[UP/DOWN] MOVE [ENTER] SELECT [ESC] BACK' \ - 20 76 6 \ - manual 'Configure this node manually' \ - none 'Leave mining unconfigured' \ - native:xmr 'Use native XMR flight sheet' \ - import:legacy 'Import an external flight sheet' +run_theme() { + set +e + RIGOS_WHIPTAIL_REAL=/usr/bin/whiptail \ + RIGOS_FIRSTBOOT_BACKTITLE='RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY' \ + sh "$wrapper" "$@" + status=$? + set -e + printf 'RIGOS_THEME_PREVIEW_EXIT=%s\n' "$status" +} + +preview_menu() { + run_theme \ + --title 'RIGOS FIRST BOOT // FLIGHT CONTROL' \ + --menu $'SELECT FLIGHT SHEET\n\nChoose the local mining authority for this node.\n\n[UP/DOWN] MOVE [ENTER] SELECT [ESC] BACK' \ + 20 76 6 \ + manual 'Configure this node manually' \ + none 'Leave mining unconfigured' \ + native:xmr 'Use native XMR flight sheet' \ + import:legacy 'Import an external flight sheet' +} + +preview_confirm() { + run_theme \ + --title 'RIGOS FIRST BOOT // COMMIT AUTHORITY' \ + --yesno $'CONFIGURATION SUMMARY\n\nNode rig01\nFlight Sheet xmr\nAlgorithm rx/0\nThreads exact 2\nHuge Pages enabled\nStart Policy on boot\n\nApply this configuration to local persistent state?' \ + 20 76 +} + +preview_input() { + run_theme \ + --title 'RIGOS FIRST BOOT // NODE IDENTITY' \ + --inputbox $'NODE NAME\n\nEnter the local appliance name.\nAllowed: A-Z, a-z, 0-9 and hyphen.' \ + 14 72 'rig01' +} + +preview_message() { + run_theme \ + --title 'RIGOS FIRST BOOT // SYSTEM READY' \ + --msgbox $'CONFIGURATION COMMITTED\n\nPersistent state verified.\nRuntime config published.\nMiner activation requested.\n\nThe local console will now return to the system.' \ + 16 72 +} + +case "$mode" in + menu) + preview_menu + ;; + confirm) + preview_confirm + ;; + input) + preview_input + ;; + message) + preview_message + ;; + all) + preview_menu + preview_input + preview_confirm + preview_message + ;; + -h|--help) + cat <<'EOF' +usage: scripts/preview-firstboot-theme.sh [menu|input|confirm|message|all] + +Displays the RIGOS firstboot theme without reading or changing persistent state. +EOF + ;; + *) + echo "preview-firstboot-theme: unknown mode: $mode" >&2 + exit 64 + ;; +esac From 1752ab00d10c34e9308e3611b8647c14b2346f71 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:26:05 +0700 Subject: [PATCH 285/386] Add read-only firstboot theme preview contracts --- .../tests/firstboot_theme_preview.rs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 crates/rigos-config/tests/firstboot_theme_preview.rs diff --git a/crates/rigos-config/tests/firstboot_theme_preview.rs b/crates/rigos-config/tests/firstboot_theme_preview.rs new file mode 100644 index 00000000..4180d74e --- /dev/null +++ b/crates/rigos-config/tests/firstboot_theme_preview.rs @@ -0,0 +1,60 @@ +use std::fs; +use std::path::PathBuf; + +fn preview_script() -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("scripts/preview-firstboot-theme.sh"); + fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) +} + +#[test] +fn preview_covers_every_firstboot_dialog_class() { + let script = preview_script(); + + for required in [ + "preview_menu", + "preview_input", + "preview_confirm", + "preview_message", + "menu|input|confirm|message|all", + ] { + assert!( + script.contains(required), + "preview harness is missing required mode marker: {required}" + ); + } + + for required in ["--menu", "--inputbox", "--yesno", "--msgbox"] { + assert!( + script.contains(required), + "preview harness is missing dialog class: {required}" + ); + } +} + +#[test] +fn preview_is_read_only_and_does_not_touch_appliance_state() { + let script = preview_script(); + + for forbidden in [ + "/var/lib/rigos", + "systemctl", + "rigos-config", + "rigos-miner", + "rigos-randomx-msr", + "mount ", + "umount ", + "rm -rf", + "sudo ", + ] { + assert!( + !script.contains(forbidden), + "preview harness contains forbidden mutation surface: {forbidden}" + ); + } + + assert!(script.contains("RIGOS_WHIPTAIL_REAL=/usr/bin/whiptail")); + assert!(script.contains("RIGOS_THEME_PREVIEW_EXIT=")); +} From 490d1391bbd1d209194ddcd70d8984755012f23e Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:26:20 +0700 Subject: [PATCH 286/386] Document firstboot console theme contract --- docs/developer/firstboot-theme.txt | 93 ++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/developer/firstboot-theme.txt diff --git a/docs/developer/firstboot-theme.txt b/docs/developer/firstboot-theme.txt new file mode 100644 index 00000000..bae5b633 --- /dev/null +++ b/docs/developer/firstboot-theme.txt @@ -0,0 +1,93 @@ +RIGOS FIRSTBOOT CONSOLE THEME +============================= + +PURPOSE +------- + +Give the local first-boot selector the look of a late-2000s or early-2010s +firmware utility without adding a graphical desktop, browser, display server, +or background daemon. + +The theme is presentation only. Configuration, identity, persistent state, +miner, MSR, SSH, runtime publication, and activation behavior stay unchanged. + +VISUAL CONTRACT +--------------- + +Root background deep blue +Dialog window light gray / silver +Window border blue +Active row white on blue +Input field black on white +Shadow black +Backtitle static ASCII, no private values +Buttons SELECT BACK NEXT APPLY CONTINUE + +The source remains ASCII-only so it renders on the Linux console without a +Unicode-font dependency. + +ARCHITECTURE +------------ + +Firstboot still executes the Python workflow at: + + /usr/local/sbin/rigos-firstboot + +The service sets RIGOS_WHIPTAIL to: + + /usr/lib/rigos/rigos-firstboot-whiptail + +The wrapper exports NEWT_COLORS, adds a static backtitle, maps button labels, +and execs the existing /usr/bin/whiptail backend. Backend exit status and +selected values retain the original whiptail contract. + +PREVIEW +------- + +Run from a Linux or WSL checkout with whiptail installed: + + bash scripts/preview-firstboot-theme.sh menu + bash scripts/preview-firstboot-theme.sh input + bash scripts/preview-firstboot-theme.sh confirm + bash scripts/preview-firstboot-theme.sh message + bash scripts/preview-firstboot-theme.sh all + +The preview harness does not read or modify /var/lib/rigos and does not call +systemctl, the configuration engine, miner services, or MSR authority. + +VERIFICATION +------------ + +Targeted tests: + + cargo test --locked -p rigos-config --test firstboot_theme + cargo test --locked -p rigos-config --test firstboot_theme_preview + +Authoritative Linux source gate: + + bash scripts/verify-wsl-entrypoint.sh "$PWD" + +A PASS may only be claimed for the exact Git head printed by the gate. + +BRANCH BOUNDARY +--------------- + +UI work belongs on: + + ui/firstboot-2009-theme + +It is stacked on performance/randomx-sustained-hash only for review. Do not +merge it into PR #15 while the physical performance and endurance evidence is +still tied to the exact performance head. + +NON-GOALS +--------- + +No X11. +No Wayland. +No Electron. +No browser UI. +No framebuffer daemon. +No persistent-state format change. +No miner behavior change. +No image-release claim before source and image verification pass. From 777b48e29602f2bba56164a9ceab1455a0be5a95 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:38:55 +0700 Subject: [PATCH 287/386] Enforce LF for firstboot runtime scripts --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 3b002266..b7852997 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,7 @@ build/usb/hooks/* text eol=lf build/usb/package-lists/rigos.list.chroot text eol=lf build/usb/includes.chroot/usr/local/bin/* text eol=lf build/usb/includes.chroot/usr/local/sbin/rigos-* text eol=lf +build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-runtime-* text eol=lf build/usb/includes.chroot/usr/lib/rigos/rigos-miner-* text eol=lf From 6394d1ae39c15baa49f95b50ab8d2c0f7c186282 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:40:20 +0700 Subject: [PATCH 288/386] Format firstboot theme tests --- crates/rigos-config/tests/firstboot_theme.rs | 40 ++++++++++++-------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index 03d44ab5..e2c4b73f 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -20,9 +20,7 @@ fn firstboot_uses_the_dedicated_console_theme_wrapper() { ); assert!( - drop_in.contains( - "Environment=RIGOS_WHIPTAIL=/usr/lib/rigos/rigos-firstboot-whiptail" - ), + drop_in.contains("Environment=RIGOS_WHIPTAIL=/usr/lib/rigos/rigos-firstboot-whiptail"), "firstboot must select the dedicated presentation wrapper" ); assert!( @@ -33,9 +31,7 @@ fn firstboot_uses_the_dedicated_console_theme_wrapper() { #[test] fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { - let wrapper = repo_file( - "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail", - ); + let wrapper = repo_file("build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"); assert!(wrapper.is_ascii(), "console theme must remain ASCII-only"); assert!(wrapper.contains("NEWT_COLORS=")); @@ -98,9 +94,7 @@ fn firstboot_theme_wrapper_maps_buttons_and_preserves_exit_status() { fs::set_permissions(&backend, permissions) .expect("failed to make fake whiptail backend executable"); - let wrapper = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail", - ); + let wrapper = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"); let status = Command::new("sh") .arg(&wrapper) .args([ @@ -123,10 +117,14 @@ fn firstboot_theme_wrapper_maps_buttons_and_preserves_exit_status() { .status() .expect("failed to execute firstboot theme wrapper"); - assert_eq!(status.code(), Some(7), "wrapper must preserve backend exit status"); + assert_eq!( + status.code(), + Some(7), + "wrapper must preserve backend exit status" + ); - let arguments = fs::read_to_string(&capture) - .expect("fake whiptail backend did not capture arguments"); + let arguments = + fs::read_to_string(&capture).expect("fake whiptail backend did not capture arguments"); let arguments: Vec<_> = arguments.lines().collect(); assert_eq!( &arguments[..8], @@ -141,9 +139,21 @@ fn firstboot_theme_wrapper_maps_buttons_and_preserves_exit_status() { "RIGOS FIRST BOOT", ] ); - assert!(arguments.windows(2).any(|pair| pair == ["--menu", "Select Flight Sheet"])); - assert!(arguments.windows(2).any(|pair| pair == ["manual", "Configure manually"])); - assert!(arguments.windows(2).any(|pair| pair == ["none", "Leave mining unconfigured"])); + assert!( + arguments + .windows(2) + .any(|pair| pair == ["--menu", "Select Flight Sheet"]) + ); + assert!( + arguments + .windows(2) + .any(|pair| pair == ["manual", "Configure manually"]) + ); + assert!( + arguments + .windows(2) + .any(|pair| pair == ["none", "Leave mining unconfigured"]) + ); fs::remove_dir_all(&temporary).expect("failed to clean theme test directory"); } From 5359482a8f5c54d5338b68bb632b85516b96bdfd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:40:53 +0700 Subject: [PATCH 289/386] Lock firstboot wrapper LF contract in tests --- crates/rigos-config/tests/firstboot_theme.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index e2c4b73f..d2fc7084 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -29,6 +29,18 @@ fn firstboot_uses_the_dedicated_console_theme_wrapper() { ); } +#[test] +fn firstboot_wrapper_is_pinned_to_lf_in_git() { + let attributes = repo_file(".gitattributes"); + + assert!( + attributes + .lines() + .any(|line| line == "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-* text eol=lf"), + "firstboot runtime wrappers must remain LF on Windows and WSL checkouts" + ); +} + #[test] fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { let wrapper = repo_file("build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"); From 8f9c3c74dc6543c3ec119d8e8e29914845f380d1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:59:42 +0700 Subject: [PATCH 290/386] Remove synthetic separators from firstboot theme --- .../usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail b/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail index 529fa7d2..ab6ad464 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail @@ -9,7 +9,7 @@ export TERM NEWT_COLORS=${NEWT_COLORS:-'root=white,blue:border=blue,lightgray:window=black,lightgray:shadow=black,black:title=white,blue:button=black,lightgray:actbutton=white,blue:compactbutton=black,lightgray:checkbox=black,lightgray:actcheckbox=white,blue:entry=black,white:disentry=gray,lightgray:label=black,lightgray:listbox=black,lightgray:actlistbox=white,blue:sellistbox=black,cyan:actsellistbox=white,blue:textbox=black,lightgray:acttextbox=white,blue:helpline=white,blue:roottext=white,blue'} export NEWT_COLORS -backtitle=${RIGOS_FIRSTBOOT_BACKTITLE:-'RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY'} +backtitle=${RIGOS_FIRSTBOOT_BACKTITLE:-'RIGOS SETUP UTILITY LOCAL NODE CONFIGURATION'} whiptail_real=${RIGOS_WHIPTAIL_REAL:-/usr/bin/whiptail} if [ ! -x "$whiptail_real" ]; then From ba758be0665171eeb6436c2b6af4256e6805d245 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 14:59:50 +0700 Subject: [PATCH 291/386] Use plain setup utility backtitle --- .../system/rigos-firstboot.service.d/2009-console-theme.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf index 7c5a3fc4..78da015e 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf @@ -1,3 +1,3 @@ [Service] Environment=RIGOS_WHIPTAIL=/usr/lib/rigos/rigos-firstboot-whiptail -Environment="RIGOS_FIRSTBOOT_BACKTITLE=RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY" +Environment="RIGOS_FIRSTBOOT_BACKTITLE=RIGOS SETUP UTILITY LOCAL NODE CONFIGURATION" From aa5b548eb954f6528cc6297ce7ae2ce7df973e39 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:00:03 +0700 Subject: [PATCH 292/386] Make firstboot preview read like a real setup utility --- scripts/preview-firstboot-theme.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/preview-firstboot-theme.sh b/scripts/preview-firstboot-theme.sh index 255a3c6c..19f154de 100644 --- a/scripts/preview-firstboot-theme.sh +++ b/scripts/preview-firstboot-theme.sh @@ -18,7 +18,7 @@ fi run_theme() { set +e RIGOS_WHIPTAIL_REAL=/usr/bin/whiptail \ - RIGOS_FIRSTBOOT_BACKTITLE='RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY' \ + RIGOS_FIRSTBOOT_BACKTITLE='RIGOS SETUP UTILITY LOCAL NODE CONFIGURATION' \ sh "$wrapper" "$@" status=$? set -e @@ -27,8 +27,8 @@ run_theme() { preview_menu() { run_theme \ - --title 'RIGOS FIRST BOOT // FLIGHT CONTROL' \ - --menu $'SELECT FLIGHT SHEET\n\nChoose the local mining authority for this node.\n\n[UP/DOWN] MOVE [ENTER] SELECT [ESC] BACK' \ + --title 'FLIGHT SHEET SELECTION' \ + --menu $'SELECT FLIGHT SHEET\n\nChoose how this node should be configured.\n\n[UP/DOWN] MOVE [ENTER] SELECT [ESC] BACK' \ 20 76 6 \ manual 'Configure this node manually' \ none 'Leave mining unconfigured' \ @@ -38,21 +38,21 @@ preview_menu() { preview_confirm() { run_theme \ - --title 'RIGOS FIRST BOOT // COMMIT AUTHORITY' \ + --title 'COMMIT CONFIGURATION' \ --yesno $'CONFIGURATION SUMMARY\n\nNode rig01\nFlight Sheet xmr\nAlgorithm rx/0\nThreads exact 2\nHuge Pages enabled\nStart Policy on boot\n\nApply this configuration to local persistent state?' \ 20 76 } preview_input() { run_theme \ - --title 'RIGOS FIRST BOOT // NODE IDENTITY' \ + --title 'NODE IDENTITY' \ --inputbox $'NODE NAME\n\nEnter the local appliance name.\nAllowed: A-Z, a-z, 0-9 and hyphen.' \ 14 72 'rig01' } preview_message() { run_theme \ - --title 'RIGOS FIRST BOOT // SYSTEM READY' \ + --title 'SETUP COMPLETE' \ --msgbox $'CONFIGURATION COMMITTED\n\nPersistent state verified.\nRuntime config published.\nMiner activation requested.\n\nThe local console will now return to the system.' \ 16 72 } From 6aa4fb01c53339c0cbcdb7074ada6bc92fc72269 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:00:28 +0700 Subject: [PATCH 293/386] Format theme tests and ban slash separators --- crates/rigos-config/tests/firstboot_theme.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index d2fc7084..2df186e4 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -1,6 +1,9 @@ use std::fs; use std::path::PathBuf; +const WRAPPER_PATH: &str = + "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"; + fn repo_path(path: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..") @@ -24,26 +27,30 @@ fn firstboot_uses_the_dedicated_console_theme_wrapper() { "firstboot must select the dedicated presentation wrapper" ); assert!( - drop_in.contains("RIGOS SYSTEM CONFIGURATION // LOCAL NODE SETUP // OFFLINE AUTHORITY"), + drop_in.contains("RIGOS SETUP UTILITY LOCAL NODE CONFIGURATION"), "firstboot must publish the static non-secret backtitle" ); + assert!( + !drop_in.contains("//"), + "setup utility titles must not use synthetic slash separators" + ); } #[test] fn firstboot_wrapper_is_pinned_to_lf_in_git() { let attributes = repo_file(".gitattributes"); + let required = + "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-* text eol=lf"; assert!( - attributes - .lines() - .any(|line| line == "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-* text eol=lf"), + attributes.lines().any(|line| line == required), "firstboot runtime wrappers must remain LF on Windows and WSL checkouts" ); } #[test] fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { - let wrapper = repo_file("build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"); + let wrapper = repo_file(WRAPPER_PATH); assert!(wrapper.is_ascii(), "console theme must remain ASCII-only"); assert!(wrapper.contains("NEWT_COLORS=")); @@ -56,6 +63,7 @@ fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { assert!(wrapper.contains("RIGOS_WHIPTAIL_REAL:-/usr/bin/whiptail")); assert!(wrapper.contains("exec \"$whiptail_real\"")); assert!(wrapper.contains("backend is not executable")); + assert!(!wrapper.contains("//")); assert!( !wrapper.contains("xterm") && !wrapper.contains("Xorg") @@ -106,7 +114,7 @@ fn firstboot_theme_wrapper_maps_buttons_and_preserves_exit_status() { fs::set_permissions(&backend, permissions) .expect("failed to make fake whiptail backend executable"); - let wrapper = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"); + let wrapper = repo_path(WRAPPER_PATH); let status = Command::new("sh") .arg(&wrapper) .args([ From b3900e2379984bbb9b3e4b5e7ccf52b4637b8639 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:00:46 +0700 Subject: [PATCH 294/386] Ban slash separators from theme previews --- crates/rigos-config/tests/firstboot_theme_preview.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/rigos-config/tests/firstboot_theme_preview.rs b/crates/rigos-config/tests/firstboot_theme_preview.rs index 4180d74e..91016fd7 100644 --- a/crates/rigos-config/tests/firstboot_theme_preview.rs +++ b/crates/rigos-config/tests/firstboot_theme_preview.rs @@ -32,6 +32,11 @@ fn preview_covers_every_firstboot_dialog_class() { "preview harness is missing dialog class: {required}" ); } + + assert!( + !script.contains("//"), + "preview titles must not use synthetic slash separators" + ); } #[test] From f2660f62c9cedfe8389f3d8e12bb65ea5aac33dd Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:15:33 +0700 Subject: [PATCH 295/386] Match firstboot theme tests to rustfmt output --- crates/rigos-config/tests/firstboot_theme.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index 2df186e4..47535c60 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -1,8 +1,7 @@ use std::fs; use std::path::PathBuf; -const WRAPPER_PATH: &str = - "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"; +const WRAPPER_PATH: &str = "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail"; fn repo_path(path: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -39,8 +38,7 @@ fn firstboot_uses_the_dedicated_console_theme_wrapper() { #[test] fn firstboot_wrapper_is_pinned_to_lf_in_git() { let attributes = repo_file(".gitattributes"); - let required = - "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-* text eol=lf"; + let required = "build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-* text eol=lf"; assert!( attributes.lines().any(|line| line == required), From 951b95084d7930372c6dc080bac6eb3fc999b457 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:24:20 +0700 Subject: [PATCH 296/386] Move firstboot wrapper behavior out of Rust crate tests --- crates/rigos-config/tests/firstboot_theme.rs | 95 -------------------- 1 file changed, 95 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index 47535c60..1db399a6 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -80,98 +80,3 @@ fn image_hook_installs_the_theme_wrapper_as_executable() { "image construction must install the wrapper as executable" ); } - -#[cfg(unix)] -#[test] -fn firstboot_theme_wrapper_maps_buttons_and_preserves_exit_status() { - use std::os::unix::fs::PermissionsExt; - use std::process::Command; - use std::time::{SystemTime, UNIX_EPOCH}; - - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock is before Unix epoch") - .as_nanos(); - let temporary = std::env::temp_dir().join(format!( - "rigos-firstboot-theme-{}-{nonce}", - std::process::id() - )); - fs::create_dir_all(&temporary).expect("failed to create theme test directory"); - - let backend = temporary.join("fake-whiptail"); - let capture = temporary.join("arguments.txt"); - fs::write( - &backend, - "#!/bin/sh\nprintf '%s\\n' \"$@\" >\"$RIGOS_THEME_CAPTURE\"\nexit \"${RIGOS_THEME_EXIT:-0}\"\n", - ) - .expect("failed to write fake whiptail backend"); - let mut permissions = fs::metadata(&backend) - .expect("failed to stat fake whiptail backend") - .permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&backend, permissions) - .expect("failed to make fake whiptail backend executable"); - - let wrapper = repo_path(WRAPPER_PATH); - let status = Command::new("sh") - .arg(&wrapper) - .args([ - "--title", - "RIGOS FIRST BOOT", - "--menu", - "Select Flight Sheet", - "20", - "76", - "2", - "manual", - "Configure manually", - "none", - "Leave mining unconfigured", - ]) - .env("RIGOS_WHIPTAIL_REAL", &backend) - .env("RIGOS_THEME_CAPTURE", &capture) - .env("RIGOS_THEME_EXIT", "7") - .env("RIGOS_FIRSTBOOT_BACKTITLE", "TEST BACKTITLE") - .status() - .expect("failed to execute firstboot theme wrapper"); - - assert_eq!( - status.code(), - Some(7), - "wrapper must preserve backend exit status" - ); - - let arguments = - fs::read_to_string(&capture).expect("fake whiptail backend did not capture arguments"); - let arguments: Vec<_> = arguments.lines().collect(); - assert_eq!( - &arguments[..8], - [ - "--backtitle", - "TEST BACKTITLE", - "--ok-button", - "SELECT", - "--cancel-button", - "BACK", - "--title", - "RIGOS FIRST BOOT", - ] - ); - assert!( - arguments - .windows(2) - .any(|pair| pair == ["--menu", "Select Flight Sheet"]) - ); - assert!( - arguments - .windows(2) - .any(|pair| pair == ["manual", "Configure manually"]) - ); - assert!( - arguments - .windows(2) - .any(|pair| pair == ["none", "Leave mining unconfigured"]) - ); - - fs::remove_dir_all(&temporary).expect("failed to clean theme test directory"); -} From 2524971db4faaed267a93b02fedce972a80cb770 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:24:36 +0700 Subject: [PATCH 297/386] Add shell behavioral verification for firstboot theme wrapper --- scripts/verify-firstboot-theme-wrapper.sh | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 scripts/verify-firstboot-theme-wrapper.sh diff --git a/scripts/verify-firstboot-theme-wrapper.sh b/scripts/verify-firstboot-theme-wrapper.sh new file mode 100644 index 00000000..a390eca0 --- /dev/null +++ b/scripts/verify-firstboot-theme-wrapper.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +wrapper="$root/build/usb/includes.chroot/usr/lib/rigos/rigos-firstboot-whiptail" + +test -f "$wrapper" + +tmp=$(mktemp -d) +cleanup() { + rm -rf "$tmp" +} +trap cleanup EXIT HUP INT TERM + +backend="$tmp/fake-whiptail" +capture="$tmp/arguments.txt" +expected="$tmp/expected.txt" + +cat >"$backend" <<'EOF' +#!/bin/sh +printf '%s\n' "$@" >"$RIGOS_THEME_CAPTURE" +exit "${RIGOS_THEME_EXIT:-0}" +EOF +chmod 0755 "$backend" + +set +e +RIGOS_WHIPTAIL_REAL="$backend" \ +RIGOS_THEME_CAPTURE="$capture" \ +RIGOS_THEME_EXIT=7 \ +RIGOS_FIRSTBOOT_BACKTITLE='TEST BACKTITLE' \ +sh "$wrapper" \ + --title 'RIGOS FIRST BOOT' \ + --menu 'Select Flight Sheet' \ + 20 76 2 \ + manual 'Configure manually' \ + none 'Leave mining unconfigured' +status=$? +set -e + +if [[ "$status" -ne 7 ]]; then + echo "firstboot theme wrapper did not preserve backend exit status: $status" >&2 + exit 1 +fi + +cat >"$expected" <<'EOF' +--backtitle +TEST BACKTITLE +--ok-button +SELECT +--cancel-button +BACK +--title +RIGOS FIRST BOOT +--menu +Select Flight Sheet +20 +76 +2 +manual +Configure manually +none +Leave mining unconfigured +EOF + +if ! cmp -s "$expected" "$capture"; then + echo "firstboot theme wrapper changed argument order or values" >&2 + diff -u "$expected" "$capture" >&2 || true + exit 1 +fi + +set +e +RIGOS_WHIPTAIL_REAL="$tmp/missing-backend" sh "$wrapper" --msgbox test 8 40 >/dev/null 2>&1 +missing_status=$? +set -e + +if [[ "$missing_status" -ne 127 ]]; then + echo "firstboot theme wrapper did not fail closed for a missing backend: $missing_status" >&2 + exit 1 +fi + +echo "RIGOS firstboot theme wrapper verification passed" From e9f1bbe1ad1f8d28875f0f9e53f26e86485882f0 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:25:47 +0700 Subject: [PATCH 298/386] Run firstboot wrapper behavior in authoritative WSL gate --- scripts/verify-wsl-entrypoint.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/verify-wsl-entrypoint.sh b/scripts/verify-wsl-entrypoint.sh index 55090d9f..27e1693c 100644 --- a/scripts/verify-wsl-entrypoint.sh +++ b/scripts/verify-wsl-entrypoint.sh @@ -20,7 +20,7 @@ if [[ -f "$HOME/.cargo/env" ]]; then fi missing=0 -for tool in cargo rustc python3 bash sh git grep rg mktemp; do +for tool in cargo rustc python3 bash sh git grep rg mktemp cmp diff; do if ! command -v "$tool" >/dev/null 2>&1; then printf 'RIGOS_WSL_TOOL_MISSING=%s\n' "$tool" >&2 missing=1 @@ -55,3 +55,4 @@ export PYTHONDONTWRITEBYTECODE=1 printf 'RIGOS_WSL_REPO=%s\n' "$repo" printf 'RIGOS_WSL_CARGO=%s\n' "$(command -v cargo)" bash ./scripts/verify.sh +bash ./scripts/verify-firstboot-theme-wrapper.sh From fdce587592d36cde6eafc90cece7ba1693bddc7b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Thu, 9 Jul 2026 15:26:14 +0700 Subject: [PATCH 299/386] Lock wrapper behavior verification into WSL authority --- crates/rigos-config/tests/firstboot_theme.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index 1db399a6..6a494b4d 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -71,6 +71,17 @@ fn firstboot_theme_is_ascii_and_preserves_whiptail_as_the_ui_engine() { ); } +#[test] +fn authoritative_wsl_gate_runs_the_wrapper_behavior_test() { + let entrypoint = repo_file("scripts/verify-wsl-entrypoint.sh"); + let verifier = repo_file("scripts/verify-firstboot-theme-wrapper.sh"); + + assert!(entrypoint.contains("bash ./scripts/verify-firstboot-theme-wrapper.sh")); + assert!(verifier.contains("RIGOS firstboot theme wrapper verification passed")); + assert!(verifier.contains("RIGOS_THEME_EXIT=7")); + assert!(verifier.contains("missing-backend")); +} + #[test] fn image_hook_installs_the_theme_wrapper_as_executable() { let hook = repo_file("build/usb/hooks/010-rigos.chroot"); From cf5aed54ccfd8fbc18ed0cac1e9cbfce19d1c201 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:08:34 +0700 Subject: [PATCH 300/386] Keep firstboot reachable when state readiness fails --- .../etc/systemd/system/rigos-firstboot.service | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service index 4a483155..9b87321a 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-firstboot.service @@ -1,15 +1,14 @@ [Unit] Description=RIGOS first-boot configuration -After=network-online.target rigos-state-ready.service rigos-profile-apply.service -Wants=network-online.target -Requires=rigos-state-ready.service +After=rigos-state.service rigos-state-ready.service rigos-profile-apply.service +Wants=rigos-state-ready.service Before=getty@tty1.service [Service] Type=oneshot ExecCondition=/usr/lib/rigos/rigos-config needs-activation ExecStart=/usr/local/sbin/rigos-firstboot -StandardInput=tty +StandardInput=tty-force StandardOutput=tty StandardError=tty TTYPath=/dev/tty1 From 0426dd86c17ebb29eb7c2948b782bcbef9de9edc Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:08:51 +0700 Subject: [PATCH 301/386] Test firstboot diagnostics survive state readiness failure --- crates/rigos-config/tests/firstboot_tty.rs | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/rigos-config/tests/firstboot_tty.rs b/crates/rigos-config/tests/firstboot_tty.rs index 414101a1..4f3d1aa9 100644 --- a/crates/rigos-config/tests/firstboot_tty.rs +++ b/crates/rigos-config/tests/firstboot_tty.rs @@ -41,6 +41,46 @@ fn firstboot_releases_tty1_to_getty_after_exit() { ); } +#[test] +fn firstboot_remains_visible_when_state_readiness_fails() { + let unit = unit("rigos-firstboot.service"); + let after = directive_tokens(&unit, "After="); + let wants = directive_tokens(&unit, "Wants="); + let requires = directive_tokens(&unit, "Requires="); + + for ordered in [ + "rigos-state.service", + "rigos-state-ready.service", + "rigos-profile-apply.service", + ] { + assert!( + after.contains(ordered), + "firstboot must start after {ordered} finishes, including failure" + ); + } + + assert!( + wants.contains("rigos-state-ready.service"), + "firstboot must request state verification without depending on success" + ); + assert!( + !requires.contains("rigos-state-ready.service"), + "state verification failure must not suppress firstboot diagnostics" + ); + assert!( + !unit.contains("network-online.target"), + "local firstboot must remain available without network connectivity" + ); + assert!( + unit.lines().any(|line| line == "StandardInput=tty-force"), + "firstboot must acquire tty1 for the local setup flow" + ); + assert!( + unit.lines().any(|line| line == "TTYPath=/dev/tty1"), + "firstboot must remain bound to tty1" + ); +} + #[test] fn recovery_access_does_not_hang_up_the_following_firstboot_session() { let unit = unit("rigos-recovery-access.service"); From 8b80006ccb0e48bdf891b94b627714439dd5bec1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:09:44 +0700 Subject: [PATCH 302/386] Add exact-image gate for firstboot configuration path --- scripts/verify-firstboot-image.sh | 113 ++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 scripts/verify-firstboot-image.sh diff --git a/scripts/verify-firstboot-image.sh b/scripts/verify-firstboot-image.sh new file mode 100644 index 00000000..3e73c191 --- /dev/null +++ b/scripts/verify-firstboot-image.sh @@ -0,0 +1,113 @@ +#!/bin/bash +set -euo pipefail + +die() { + printf 'verify-firstboot-image: %s\n' "$*" >&2 + exit 1 +} + +[[ $# -eq 1 ]] || die 'usage: verify-firstboot-image.sh ' +image="$(readlink -f "$1")" +[[ -f "$image" ]] || die "image is missing: $image" +[[ "$(id -u)" -eq 0 ]] || die 'must run as root' + +partition_json="$(sfdisk --json "$image")" +start="$(jq -r '.partitiontable.partitions[1].start' <<<"$partition_json")" +size="$(jq -r '.partitiontable.partitions[1].size' <<<"$partition_json")" +[[ "$start" =~ ^[0-9]+$ && "$size" =~ ^[0-9]+$ ]] || die 'ROOT_A geometry is invalid' + +loop="$( + losetup --find --show --read-only \ + --offset $((start * 512)) \ + --sizelimit $((size * 512)) \ + "$image" +)" +temporary="$(mktemp -d)" +cleanup() { + set +e + mountpoint -q "$temporary/root-a" && umount "$temporary/root-a" + losetup -d "$loop" 2>/dev/null + rm -rf "$temporary" +} +trap cleanup EXIT + +mkdir -p "$temporary/root-a" "$temporary/squash" +mount -o ro "$loop" "$temporary/root-a" +squashfs="$temporary/root-a/live/filesystem.squashfs" +[[ -f "$squashfs" ]] || die 'ROOT_A squashfs is missing' + +unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ + usr/bin/python3 usr/bin/python3.11 usr/bin/whiptail \ + usr/local/sbin/rigos-firstboot \ + usr/lib/rigos/rigos-firstboot-whiptail \ + etc/systemd/system/rigos-firstboot.service \ + etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf \ + etc/systemd/system/multi-user.target.wants/rigos-firstboot.service \ + >/dev/null + +root="$temporary/squash" +service="$root/etc/systemd/system/rigos-firstboot.service" +dropin="$root/etc/systemd/system/rigos-firstboot.service.d/2009-console-theme.conf" +firstboot="$root/usr/local/sbin/rigos-firstboot" +wrapper="$root/usr/lib/rigos/rigos-firstboot-whiptail" + +[[ -L "$root/etc/systemd/system/multi-user.target.wants/rigos-firstboot.service" ]] \ + || die 'firstboot service is not enabled in the appliance' +[[ -x "$root/usr/bin/python3" ]] || die 'Python runtime for firstboot is missing' +[[ -x "$root/usr/bin/whiptail" ]] || die 'whiptail runtime is missing' +[[ -x "$firstboot" ]] || die 'firstboot program is missing or not executable' +[[ -x "$wrapper" ]] || die 'firstboot theme wrapper is missing or not executable' +[[ -f "$dropin" ]] || die 'firstboot theme drop-in is missing' + +python3 -m py_compile "$firstboot" +sh -n "$wrapper" + +for required in \ + 'After=rigos-state.service rigos-state-ready.service rigos-profile-apply.service' \ + 'Wants=rigos-state-ready.service' \ + 'Before=getty@tty1.service' \ + 'ExecCondition=/usr/lib/rigos/rigos-config needs-activation' \ + 'ExecStart=/usr/local/sbin/rigos-firstboot' \ + 'StandardInput=tty-force' \ + 'StandardOutput=tty' \ + 'StandardError=tty' \ + 'TTYPath=/dev/tty1' \ + 'TTYReset=yes' \ + 'TTYVTDisallocate=yes' +do + grep -Fqx "$required" "$service" \ + || die "firstboot service contract is missing: $required" +done + +if grep -Fqx 'Requires=rigos-state-ready.service' "$service"; then + die 'state readiness failure still suppresses firstboot diagnostics' +fi +if grep -Fq 'network-online.target' "$service"; then + die 'firstboot still depends on network-online' +fi + +for required in \ + "stage='state_not_ready'" \ + "raise FirstbootFailure('state_not_ready')" \ + 'def manual_proposal()' \ + "('manual', 'Configure manually')" \ + "raise FirstbootCancelled('mining_left_unconfigured')" +do + grep -Fq "$required" "$firstboot" \ + || die "firstboot recovery/configuration path is missing: $required" +done + +for required in \ + 'Environment=RIGOS_WHIPTAIL=/usr/lib/rigos/rigos-firstboot-whiptail' \ + 'RIGOS SETUP UTILITY LOCAL NODE CONFIGURATION' +do + grep -Fq "$required" "$dropin" \ + || die "firstboot theme drop-in contract is missing: $required" +done + +grep -Fq 'RIGOS_WHIPTAIL_REAL:-/usr/bin/whiptail' "$wrapper" \ + || die 'firstboot theme wrapper does not use packaged whiptail' +grep -Fq 'exec "$whiptail_real"' "$wrapper" \ + || die 'firstboot theme wrapper does not preserve the dialog engine' + +printf 'RIGOS firstboot exact-image verification passed: %s\n' "$image" From a8ca616072c3afc01fef2f79f2719116675bacee Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:09:56 +0700 Subject: [PATCH 303/386] Require firstboot source and exact-image gates during builds --- scripts/build-usb-image-entrypoint.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 78e1ef70..2970c3c6 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -44,9 +44,11 @@ export CARGO_TARGET_DIR=/work/rigos-performance-preflight-target cargo test --locked -p rigos-config --test miner_observer_authority -- --nocapture cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture +cargo test --locked -p rigos-config --test firstboot_tty -- --nocapture ./scripts/build-usb-image.sh image="./dist/usb/${RIGOS_IMAGE_ID}-${RIGOS_IMAGE_VERSION}.img" bash ./scripts/verify-randomx-performance-image.sh "$image" bash ./scripts/verify-miner-observer-image.sh "$image" +bash ./scripts/verify-firstboot-image.sh "$image" From 6e73580b43e3bd4ea376b04e3963d0012f6bf3a4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:10:11 +0700 Subject: [PATCH 304/386] Advance image version for firstboot recovery fix --- build/usb/version.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/version.env b/build/usb/version.env index 9567e611..c85c4412 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.8 -RIGOS_IMAGE_VERSION=0.0.4-alpha.8 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.9 +RIGOS_IMAGE_VERSION=0.0.4-alpha.9 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=8 +RIGOS_BUILD_ORDINAL=9 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 From f40a895899c5c6c268cb86e27616f0d6c53b45f0 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:10:31 +0700 Subject: [PATCH 305/386] Lock alpha9 firstboot image gate into source tests --- .../tests/firstboot_image_gate.rs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 crates/rigos-config/tests/firstboot_image_gate.rs diff --git a/crates/rigos-config/tests/firstboot_image_gate.rs b/crates/rigos-config/tests/firstboot_image_gate.rs new file mode 100644 index 00000000..e351a434 --- /dev/null +++ b/crates/rigos-config/tests/firstboot_image_gate.rs @@ -0,0 +1,55 @@ +use std::fs; +use std::path::PathBuf; + +fn repo_file(path: &str) -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path); + fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) +} + +#[test] +fn alpha9_build_runs_source_and_exact_image_firstboot_gates() { + let version = repo_file("build/usb/version.env"); + let entrypoint = repo_file("scripts/build-usb-image-entrypoint.sh"); + let verifier = repo_file("scripts/verify-firstboot-image.sh"); + let hook = repo_file("build/usb/hooks/010-rigos.chroot"); + + assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.9")); + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.9")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=9")); + + assert!(entrypoint.contains("--test firstboot_tty")); + assert!(entrypoint.contains("bash ./scripts/verify-firstboot-image.sh \"$image\"")); + + for required in [ + "multi-user.target.wants/rigos-firstboot.service", + "firstboot service is not enabled in the appliance", + "state readiness failure still suppresses firstboot diagnostics", + "firstboot still depends on network-online", + "StandardInput=tty-force", + "def manual_proposal()", + ] { + assert!( + verifier.contains(required), + "firstboot exact-image verifier is missing: {required}" + ); + } + + assert!( + hook.contains("rigos-firstboot.service"), + "image construction must enable firstboot" + ); +} + +#[test] +fn firstboot_image_gate_is_read_only_against_the_appliance_image() { + let verifier = repo_file("scripts/verify-firstboot-image.sh"); + + assert!(verifier.contains("losetup --find --show --read-only")); + assert!(verifier.contains("mount -o ro")); + assert!(!verifier.contains("mount -o rw")); + assert!(!verifier.contains("systemctl start")); + assert!(!verifier.contains("systemctl enable")); +} From ea6463349e6685834a5c981ef63a56b2c47f534a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 05:11:03 +0700 Subject: [PATCH 306/386] Verify firstboot remains reachable after state failure --- scripts/verify-systemd-ordering.py | 148 ++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 25 deletions(-) diff --git a/scripts/verify-systemd-ordering.py b/scripts/verify-systemd-ordering.py index a6f84438..67e13d1b 100644 --- a/scripts/verify-systemd-ordering.py +++ b/scripts/verify-systemd-ordering.py @@ -42,51 +42,145 @@ def includes(actual, expected, message): def verify(units): names = { - "rigos-state.service", "rigos-recovery-access.service", - "rigos-state-ready.service", "rigos-ssh-hostkeys.service", - "rigos-profile-apply.service", "rigos-firstboot.service", - "rigos-hugepages.service", "rigos-miner.service", + "rigos-state.service", + "rigos-recovery-access.service", + "rigos-state-ready.service", + "rigos-ssh-hostkeys.service", + "rigos-profile-apply.service", + "rigos-firstboot.service", + "rigos-hugepages.service", + "rigos-miner.service", } includes(set(units), names, "RIGOS unit set is incomplete") state = units["rigos-state.service"] - require(state.scalar("Unit", "DefaultDependencies") == "no", "state must retain early boot dependencies") - includes(state.words("Unit", "Before"), {"local-fs.target", "rigos-state-ready.service"}, "state ordering is incomplete") + require( + state.scalar("Unit", "DefaultDependencies") == "no", + "state must retain early boot dependencies", + ) + includes( + state.words("Unit", "Before"), + {"local-fs.target", "rigos-state-ready.service"}, + "state ordering is incomplete", + ) recovery = units["rigos-recovery-access.service"] - includes(recovery.words("Unit", "After"), {"rigos-state.service"}, "recovery must follow state") - includes(recovery.words("Unit", "Before"), {"rigos-state-ready.service", "rigos-firstboot.service"}, "recovery ordering is incomplete") - require(recovery.scalar("Service", "TTYVHangup") != "yes", "recovery must not hang up tty1") + includes( + recovery.words("Unit", "After"), + {"rigos-state.service"}, + "recovery must follow state", + ) + includes( + recovery.words("Unit", "Before"), + {"rigos-state-ready.service", "rigos-firstboot.service"}, + "recovery ordering is incomplete", + ) + require( + recovery.scalar("Service", "TTYVHangup") != "yes", + "recovery must not hang up tty1", + ) ready = units["rigos-state-ready.service"] - require(ready.scalar("Unit", "DefaultDependencies") != "no", "state-ready must use normal dependencies") - includes(ready.words("Unit", "After"), {"rigos-state.service", "rigos-recovery-access.service"}, "state-ready ordering is incomplete") - includes(ready.words("Unit", "Requires"), {"rigos-state.service"}, "state-ready must require state") + require( + ready.scalar("Unit", "DefaultDependencies") != "no", + "state-ready must use normal dependencies", + ) + includes( + ready.words("Unit", "After"), + {"rigos-state.service", "rigos-recovery-access.service"}, + "state-ready ordering is incomplete", + ) + includes( + ready.words("Unit", "Requires"), + {"rigos-state.service"}, + "state-ready must require state", + ) includes( ready.words("Unit", "Before"), { - "rigos-ssh-hostkeys.service", "rigos-profile-apply.service", - "rigos-firstboot.service", "rigos-hugepages.service", + "rigos-ssh-hostkeys.service", + "rigos-profile-apply.service", + "rigos-firstboot.service", + "rigos-hugepages.service", "rigos-miner.service", }, "state-ready downstream ordering is incomplete", ) - require("local-fs.target" not in ready.words("Unit", "Before"), "state-ready must not order before local-fs") - require("local-fs.target" not in ready.words("Install", "WantedBy"), "state-ready must not be installed under local-fs") - includes(ready.words("Install", "WantedBy"), {"multi-user.target"}, "state-ready must be installed under multi-user") + require( + "local-fs.target" not in ready.words("Unit", "Before"), + "state-ready must not order before local-fs", + ) + require( + "local-fs.target" not in ready.words("Install", "WantedBy"), + "state-ready must not be installed under local-fs", + ) + includes( + ready.words("Install", "WantedBy"), + {"multi-user.target"}, + "state-ready must be installed under multi-user", + ) hostkeys = units["rigos-ssh-hostkeys.service"] - includes(hostkeys.words("Unit", "After"), {"rigos-state-ready.service"}, "SSH host-key authority must follow state readiness") - includes(hostkeys.words("Unit", "Requires"), {"rigos-state-ready.service"}, "SSH host-key authority must require state readiness") - includes(hostkeys.words("Unit", "Before"), {"ssh.service"}, "SSH host-key authority must precede sshd") + includes( + hostkeys.words("Unit", "After"), + {"rigos-state-ready.service"}, + "SSH host-key authority must follow state readiness", + ) + includes( + hostkeys.words("Unit", "Requires"), + {"rigos-state-ready.service"}, + "SSH host-key authority must require state readiness", + ) + includes( + hostkeys.words("Unit", "Before"), + {"ssh.service"}, + "SSH host-key authority must precede sshd", + ) require( hostkeys.scalar("Service", "ExecStart") == "/usr/lib/rigos/rigos-ssh-hostkeys", "SSH host-key authority entrypoint is not exact", ) - includes(hostkeys.words("Install", "WantedBy"), {"multi-user.target"}, "SSH host-key authority must be enabled under multi-user") + includes( + hostkeys.words("Install", "WantedBy"), + {"multi-user.target"}, + "SSH host-key authority must be enabled under multi-user", + ) firstboot = units["rigos-firstboot.service"] - includes(firstboot.words("Unit", "Requires"), {"rigos-state-ready.service"}, "firstboot must require state-ready") - require(firstboot.scalar("Service", "TTYVHangup") != "yes", "firstboot must not hang up tty1") + includes( + firstboot.words("Unit", "After"), + { + "rigos-state.service", + "rigos-state-ready.service", + "rigos-profile-apply.service", + }, + "firstboot ordering is incomplete", + ) + includes( + firstboot.words("Unit", "Wants"), + {"rigos-state-ready.service"}, + "firstboot must request state verification", + ) + require( + "rigos-state-ready.service" not in firstboot.words("Unit", "Requires"), + "firstboot diagnostics must survive state readiness failure", + ) + require( + "network-online.target" not in firstboot.words("Unit", "After") + and "network-online.target" not in firstboot.words("Unit", "Wants"), + "firstboot must remain available offline", + ) + require( + firstboot.scalar("Service", "StandardInput") == "tty-force", + "firstboot must acquire tty1", + ) + require( + firstboot.scalar("Service", "TTYPath") == "/dev/tty1", + "firstboot must use tty1", + ) + require( + firstboot.scalar("Service", "TTYVHangup") != "yes", + "firstboot must not hang up tty1", + ) def graph_for(units): @@ -120,7 +214,7 @@ def visit(node): if cycle: return cycle elif state[target] == 1: - return stack[positions[target]:] + [target] + return stack[positions[target] :] + [target] stack.pop() positions.pop(node) state[node] = 2 @@ -136,7 +230,11 @@ def visit(node): def main(): parser = argparse.ArgumentParser() - parser.add_argument("unit_dir", nargs="?", default="build/usb/includes.chroot/etc/systemd/system") + parser.add_argument( + "unit_dir", + nargs="?", + default="build/usb/includes.chroot/etc/systemd/system", + ) directory = Path(parser.parse_args().unit_dir) try: units = {path.name: Unit(path) for path in directory.glob("rigos-*.service")} From b0965f7f9e8a8eade49144a8396e13c517ff615b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 18:01:13 +0700 Subject: [PATCH 307/386] Require jq in WSL source-gate preflight --- scripts/verify-wsl-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-wsl-entrypoint.sh b/scripts/verify-wsl-entrypoint.sh index 27e1693c..7f50da92 100644 --- a/scripts/verify-wsl-entrypoint.sh +++ b/scripts/verify-wsl-entrypoint.sh @@ -20,7 +20,7 @@ if [[ -f "$HOME/.cargo/env" ]]; then fi missing=0 -for tool in cargo rustc python3 bash sh git grep rg mktemp cmp diff; do +for tool in cargo rustc python3 bash sh git grep rg mktemp cmp diff jq; do if ! command -v "$tool" >/dev/null 2>&1; then printf 'RIGOS_WSL_TOOL_MISSING=%s\n' "$tool" >&2 missing=1 From 79e769b4b9fe367e918611dd9096a5396d4c42d4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Fri, 10 Jul 2026 18:01:28 +0700 Subject: [PATCH 308/386] Lock jq source-gate dependency into tests --- crates/rigos-config/tests/firstboot_theme.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/rigos-config/tests/firstboot_theme.rs b/crates/rigos-config/tests/firstboot_theme.rs index 6a494b4d..f6f6e9fc 100644 --- a/crates/rigos-config/tests/firstboot_theme.rs +++ b/crates/rigos-config/tests/firstboot_theme.rs @@ -77,6 +77,10 @@ fn authoritative_wsl_gate_runs_the_wrapper_behavior_test() { let verifier = repo_file("scripts/verify-firstboot-theme-wrapper.sh"); assert!(entrypoint.contains("bash ./scripts/verify-firstboot-theme-wrapper.sh")); + assert!( + entrypoint.contains("cmp diff jq"), + "WSL preflight must require jq before runtime publication tests" + ); assert!(verifier.contains("RIGOS firstboot theme wrapper verification passed")); assert!(verifier.contains("RIGOS_THEME_EXIT=7")); assert!(verifier.contains("missing-backend")); From 5c8d2d3a8c9d3899327baa4a653b5198ec8be7d1 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:13:04 +0700 Subject: [PATCH 309/386] Advance diagnostic SSH image to alpha.10 --- build/usb/version.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/version.env b/build/usb/version.env index c85c4412..09f806af 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.9 -RIGOS_IMAGE_VERSION=0.0.4-alpha.9 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.10 +RIGOS_IMAGE_VERSION=0.0.4-alpha.10 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=9 +RIGOS_BUILD_ORDINAL=10 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 From 32ce9e97d88a0f9cab916d511ca1b65756f0edab Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:13:15 +0700 Subject: [PATCH 310/386] Allow SSH host identity fallback without ready state --- .../etc/systemd/system/rigos-ssh-hostkeys.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service b/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service index fe069df2..b4f7e2f0 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service @@ -1,7 +1,7 @@ [Unit] -Description=Establish persistent RIGOS SSH host identity +Description=Establish RIGOS SSH host identity After=rigos-state-ready.service -Requires=rigos-state-ready.service +Wants=rigos-state-ready.service Before=ssh.service [Service] From c567bed6d4d70dc2a6e18c317f0c434993a9dadc Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:13:23 +0700 Subject: [PATCH 311/386] Use runtime-published SSH host identity for diagnostics --- .../etc/ssh/sshd_config.d/01-rigos-hostkeys.conf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf b/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf index 0532202d..7b9489f5 100644 --- a/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf +++ b/build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf @@ -1 +1,4 @@ -HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key +HostKey /run/rigos/ssh-hostkeys/ssh_host_ed25519_key +PasswordAuthentication yes +PermitRootLogin no +AllowUsers rigosadmin From 0a1aa893f1a766a95d51fede5eaa39240dd07a35 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:14:07 +0700 Subject: [PATCH 312/386] Add boot-scoped SSH host identity fallback --- .../usr/lib/rigos/rigos-ssh-hostkeys | 213 ++++++++++++++++-- 1 file changed, 193 insertions(+), 20 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys index 64bbbe43..83880877 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys @@ -18,6 +18,10 @@ PRIVATE_KEY = KEYS / "ssh_host_ed25519_key" PUBLIC_KEY = KEYS / "ssh_host_ed25519_key.pub" MANIFEST = KEYS / "manifest.json" RUNTIME = Path("/run/rigos") +ACTIVE_KEYS = RUNTIME / "ssh-hostkeys" +ACTIVE_PRIVATE_KEY = ACTIVE_KEYS / "ssh_host_ed25519_key" +ACTIVE_PUBLIC_KEY = ACTIVE_KEYS / "ssh_host_ed25519_key.pub" +ACTIVE_MANIFEST = ACTIVE_KEYS / "manifest.json" STATUS = RUNTIME / "ssh-hostkeys-status.json" STATE_STATUS = RUNTIME / "state-status.json" BOOT_ID = Path("/proc/sys/kernel/random/boot_id") @@ -141,6 +145,20 @@ def ensure_secure_directory(path: Path, mode: int) -> None: raise AuthorityError(f"{path} has unsafe mode") +def ensure_runtime_root() -> None: + if not RUNTIME.exists(): + RUNTIME.mkdir(mode=0o755, parents=True) + os.chown(RUNTIME, 0, 0) + RUNTIME.chmod(0o755) + observed = RUNTIME.lstat() + if stat.S_ISLNK(observed.st_mode) or not stat.S_ISDIR(observed.st_mode): + raise AuthorityError("runtime root is not a real directory") + if observed.st_uid != 0 or observed.st_gid != 0: + raise AuthorityError("runtime root is not owned by root:root") + if stat.S_IMODE(observed.st_mode) != 0o755: + raise AuthorityError("runtime root has unsafe mode") + + def validate_regular_file(path: Path, mode: int) -> None: try: observed = path.lstat() @@ -176,18 +194,21 @@ def key_fingerprint(public_key: Path) -> str: return fields[1] -def validate_keyset() -> str: +def validate_key_pair(private_key: Path, public_key: Path) -> str: + public_fields = public_key.read_text(encoding="ascii").strip().split() + derived_fields = command_output([SSH_KEYGEN, "-y", "-f", str(private_key)]).split() + if len(public_fields) < 2 or derived_fields[:2] != public_fields[:2]: + raise AuthorityError("SSH public and private keys do not match") + return key_fingerprint(public_key) + + +def validate_persistent_keyset() -> str: ensure_secure_directory(KEYS, 0o700) validate_regular_file(PRIVATE_KEY, 0o600) validate_regular_file(PUBLIC_KEY, 0o644) validate_regular_file(MANIFEST, 0o644) - public_fields = PUBLIC_KEY.read_text(encoding="ascii").strip().split() - derived_fields = command_output([SSH_KEYGEN, "-y", "-f", str(PRIVATE_KEY)]).split() - if len(public_fields) < 2 or derived_fields[:2] != public_fields[:2]: - raise AuthorityError("persistent SSH public and private keys do not match") - - fingerprint = key_fingerprint(PUBLIC_KEY) + fingerprint = validate_key_pair(PRIVATE_KEY, PUBLIC_KEY) public_sha256 = hashlib.sha256(PUBLIC_KEY.read_bytes()).hexdigest() manifest = read_json(MANIFEST) if ( @@ -202,21 +223,45 @@ def validate_keyset() -> str: return fingerprint -def remove_stale_staging() -> None: - for candidate in SYSTEM.glob(".ssh-hostkeys.tmp-*"): +def validate_active_keyset( + boot_id: str, mode: str, expected_source_fingerprint: str | None +) -> str: + ensure_secure_directory(ACTIVE_KEYS, 0o700) + validate_regular_file(ACTIVE_PRIVATE_KEY, 0o600) + validate_regular_file(ACTIVE_PUBLIC_KEY, 0o644) + validate_regular_file(ACTIVE_MANIFEST, 0o644) + + fingerprint = validate_key_pair(ACTIVE_PRIVATE_KEY, ACTIVE_PUBLIC_KEY) + public_sha256 = hashlib.sha256(ACTIVE_PUBLIC_KEY.read_bytes()).hexdigest() + manifest = read_json(ACTIVE_MANIFEST) + if ( + manifest.get("schema") != "rigos.ssh-active-hostkeys/v1" + or manifest.get("algorithm") != "ssh-ed25519" + or manifest.get("boot_id") != boot_id + or manifest.get("mode") != mode + or manifest.get("fingerprint") != fingerprint + or manifest.get("public_key_sha256") != public_sha256 + or manifest.get("source_fingerprint") != expected_source_fingerprint + ): + raise AuthorityError("active SSH host identity manifest is invalid") + return fingerprint + + +def remove_stale_staging(parent: Path, prefix: str) -> None: + for candidate in parent.glob(f"{prefix}*"): observed = candidate.lstat() if stat.S_ISLNK(observed.st_mode) or not stat.S_ISDIR(observed.st_mode): raise AuthorityError("unsafe SSH host identity staging path exists") if observed.st_uid != 0 or observed.st_gid != 0: raise AuthorityError("untrusted SSH host identity staging directory exists") shutil.rmtree(candidate) - fsync_directory(SYSTEM) + fsync_directory(parent) -def generate_keyset(boot_id: str) -> str: +def generate_persistent_keyset(boot_id: str) -> str: if KEYS.exists() or KEYS.is_symlink(): raise AuthorityError("persistent SSH host identity exists without a valid manifest") - remove_stale_staging() + remove_stale_staging(SYSTEM, ".ssh-hostkeys.tmp-") temporary = Path(tempfile.mkdtemp(prefix=".ssh-hostkeys.tmp-", dir=SYSTEM)) try: @@ -275,7 +320,92 @@ def generate_keyset(boot_id: str) -> str: finally: if temporary.exists(): shutil.rmtree(temporary) - return validate_keyset() + return validate_persistent_keyset() + + +def install_active_keyset( + boot_id: str, + mode: str, + source_private: Path | None = None, + source_public: Path | None = None, + source_fingerprint: str | None = None, +) -> str: + ensure_runtime_root() + if ACTIVE_KEYS.exists() or ACTIVE_KEYS.is_symlink(): + raise AuthorityError("active SSH host identity exists without a valid manifest") + remove_stale_staging(RUNTIME, ".ssh-hostkeys.active-") + + temporary = Path(tempfile.mkdtemp(prefix=".ssh-hostkeys.active-", dir=RUNTIME)) + try: + os.chown(temporary, 0, 0) + temporary.chmod(0o700) + private_key = temporary / ACTIVE_PRIVATE_KEY.name + public_key = temporary / ACTIVE_PUBLIC_KEY.name + manifest_path = temporary / ACTIVE_MANIFEST.name + + if mode == "persistent": + if source_private is None or source_public is None or source_fingerprint is None: + raise AuthorityError("persistent active identity source is incomplete") + shutil.copyfile(source_private, private_key) + shutil.copyfile(source_public, public_key) + elif mode == "ephemeral": + result = subprocess.run( + [ + SSH_KEYGEN, + "-q", + "-t", + "ed25519", + "-N", + "", + "-C", + "rigos-diagnostic-host", + "-f", + str(private_key), + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + timeout=30, + ) + if result.returncode != 0: + raise AuthorityError("ephemeral SSH host identity generation failed") + else: + raise AuthorityError("unknown active SSH host identity mode") + + os.chown(private_key, 0, 0) + os.chown(public_key, 0, 0) + private_key.chmod(0o600) + public_key.chmod(0o644) + fingerprint = validate_key_pair(private_key, public_key) + if mode == "persistent" and fingerprint != source_fingerprint: + raise AuthorityError("active SSH host identity differs from persistent source") + + manifest = { + "schema": "rigos.ssh-active-hostkeys/v1", + "algorithm": "ssh-ed25519", + "boot_id": boot_id, + "mode": mode, + "fingerprint": fingerprint, + "public_key_sha256": hashlib.sha256(public_key.read_bytes()).hexdigest(), + "source_fingerprint": source_fingerprint, + } + write_atomic_json(manifest_path, manifest, 0o644) + + for path in (private_key, public_key, manifest_path): + descriptor = os.open(path, os.O_RDONLY) + try: + os.fsync(descriptor) + finally: + os.close(descriptor) + fsync_directory(temporary) + os.rename(temporary, ACTIVE_KEYS) + fsync_directory(RUNTIME) + finally: + if temporary.exists(): + shutil.rmtree(temporary) + + return validate_active_keyset(boot_id, mode, source_fingerprint) def write_status(boot_id: str, outcome: str, **extra: object) -> None: @@ -283,6 +413,7 @@ def write_status(boot_id: str, outcome: str, **extra: object) -> None: "schema": "rigos.ssh-hostkeys-status/v1", "boot_id": boot_id, "outcome": outcome, + "active_path": str(ACTIVE_PRIVATE_KEY), "persistent_path": str(PRIVATE_KEY), } value.update(extra) @@ -296,19 +427,61 @@ def main() -> int: boot_id = "unavailable" try: boot_id = current_boot_id() - validate_state_ready(boot_id) - secure_state_root() - ensure_secure_directory(SYSTEM, 0o700) - reused = KEYS.exists() - fingerprint = validate_keyset() if reused else generate_keyset(boot_id) + state_reason = None + try: + validate_state_ready(boot_id) + persistent_state_ready = True + except AuthorityError as error: + persistent_state_ready = False + state_reason = str(error) + + ensure_runtime_root() + active_reused = ACTIVE_KEYS.exists() or ACTIVE_KEYS.is_symlink() + + if persistent_state_ready: + secure_state_root() + ensure_secure_directory(SYSTEM, 0o700) + persistent_reused = KEYS.exists() or KEYS.is_symlink() + persistent_fingerprint = ( + validate_persistent_keyset() + if persistent_reused + else generate_persistent_keyset(boot_id) + ) + fingerprint = ( + validate_active_keyset(boot_id, "persistent", persistent_fingerprint) + if active_reused + else install_active_keyset( + boot_id, + "persistent", + PRIVATE_KEY, + PUBLIC_KEY, + persistent_fingerprint, + ) + ) + mode = "persistent" + else: + persistent_reused = False + fingerprint = ( + validate_active_keyset(boot_id, "ephemeral", None) + if active_reused + else install_active_keyset(boot_id, "ephemeral") + ) + mode = "ephemeral" + write_status( boot_id, "ready", algorithm="ssh-ed25519", fingerprint=fingerprint, - reused=reused, + persistence_mode=mode, + active_reused=active_reused, + persistent_reused=persistent_reused, + state_reason=state_reason, + ) + print( + "RIGOS_SSH_HOSTKEYS=READY " + f"mode={mode} fingerprint={fingerprint} active_reused={str(active_reused).lower()}" ) - print(f"RIGOS_SSH_HOSTKEYS=READY fingerprint={fingerprint} reused={str(reused).lower()}") return 0 except (AuthorityError, OSError, subprocess.SubprocessError) as error: write_status(boot_id, "error", reason=str(error)) From e6a1c1361b71d97bd77ba167bd5f1759c342579c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:14:53 +0700 Subject: [PATCH 313/386] Advance firstboot image gate assertions to alpha.10 --- crates/rigos-config/tests/firstboot_image_gate.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_image_gate.rs b/crates/rigos-config/tests/firstboot_image_gate.rs index e351a434..8b623ffb 100644 --- a/crates/rigos-config/tests/firstboot_image_gate.rs +++ b/crates/rigos-config/tests/firstboot_image_gate.rs @@ -10,15 +10,15 @@ fn repo_file(path: &str) -> String { } #[test] -fn alpha9_build_runs_source_and_exact_image_firstboot_gates() { +fn alpha10_build_runs_source_and_exact_image_firstboot_gates() { let version = repo_file("build/usb/version.env"); let entrypoint = repo_file("scripts/build-usb-image-entrypoint.sh"); let verifier = repo_file("scripts/verify-firstboot-image.sh"); let hook = repo_file("build/usb/hooks/010-rigos.chroot"); - assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.9")); - assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.9")); - assert!(version.contains("RIGOS_BUILD_ORDINAL=9")); + assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.10")); + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.10")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=10")); assert!(entrypoint.contains("--test firstboot_tty")); assert!(entrypoint.contains("bash ./scripts/verify-firstboot-image.sh \"$image\"")); From e4dd778b26e43394a7c51ece8656555153f4502b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:15:19 +0700 Subject: [PATCH 314/386] Test diagnostic SSH host identity fallback --- crates/rigos-config/tests/diagnostic_ssh.rs | 163 ++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 crates/rigos-config/tests/diagnostic_ssh.rs diff --git a/crates/rigos-config/tests/diagnostic_ssh.rs b/crates/rigos-config/tests/diagnostic_ssh.rs new file mode 100644 index 00000000..ad9abcf2 --- /dev/null +++ b/crates/rigos-config/tests/diagnostic_ssh.rs @@ -0,0 +1,163 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +fn repo_file(path: &str) -> String { + let path = repo_path(path); + fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) +} + +#[test] +fn diagnostic_ssh_keeps_host_identity_mandatory_without_requiring_ready_state() { + let service = repo_file( + "build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service", + ); + let ssh_dropin = repo_file( + "build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf", + ); + let policy = repo_file( + "build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf", + ); + let authority = + repo_file("build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys"); + let version = repo_file("build/usb/version.env"); + + assert!(service.contains("After=rigos-state-ready.service")); + assert!(service.contains("Wants=rigos-state-ready.service")); + assert!(!service.contains("Requires=rigos-state-ready.service")); + assert!(service.contains("Before=ssh.service")); + assert!(ssh_dropin.contains("Requires=rigos-ssh-hostkeys.service")); + + for required in [ + "HostKey /run/rigos/ssh-hostkeys/ssh_host_ed25519_key", + "PasswordAuthentication yes", + "PermitRootLogin no", + "AllowUsers rigosadmin", + ] { + assert!(policy.lines().any(|line| line == required)); + } + + for required in [ + "ACTIVE_KEYS = RUNTIME / \"ssh-hostkeys\"", + "mode = \"persistent\"", + "mode = \"ephemeral\"", + "rigos.ssh-active-hostkeys/v1", + "persistent_state_ready", + "install_active_keyset", + ] { + assert!(authority.contains(required), "authority is missing {required}"); + } + + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.10")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=10")); +} + +#[test] +fn hostkey_authority_selects_ephemeral_or_persistent_storage_without_touching_unready_state() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-diagnostic-ssh-{unique}")); + fs::create_dir_all(&root).unwrap(); + + let fixture = r#" +import runpy +import sys +from pathlib import Path + +source = Path(sys.argv[1]) +root = Path(sys.argv[2]) +namespace = runpy.run_path(str(source), run_name='rigos_ssh_hostkeys_test') +g = namespace['main'].__globals__ + + +def configure(case): + runtime = root / case / 'run' + state = root / case / 'state' + runtime.mkdir(parents=True) + g['RUNTIME'] = runtime + g['STATE'] = state + g['SYSTEM'] = state / 'system' + g['KEYS'] = state / 'system' / 'ssh-hostkeys' + g['PRIVATE_KEY'] = g['KEYS'] / 'ssh_host_ed25519_key' + g['PUBLIC_KEY'] = g['KEYS'] / 'ssh_host_ed25519_key.pub' + g['MANIFEST'] = g['KEYS'] / 'manifest.json' + g['ACTIVE_KEYS'] = runtime / 'ssh-hostkeys' + g['ACTIVE_PRIVATE_KEY'] = g['ACTIVE_KEYS'] / 'ssh_host_ed25519_key' + g['ACTIVE_PUBLIC_KEY'] = g['ACTIVE_KEYS'] / 'ssh_host_ed25519_key.pub' + g['ACTIVE_MANIFEST'] = g['ACTIVE_KEYS'] / 'manifest.json' + g['STATUS'] = runtime / 'ssh-hostkeys-status.json' + g['STATE_STATUS'] = runtime / 'state-status.json' + g['current_boot_id'] = lambda: 'boot-test' + g['ensure_runtime_root'] = lambda: None + return state + + +# State unavailable: only a boot-scoped runtime identity may be selected. +state = configure('ephemeral') +recorded = {} +def unavailable(_boot_id): + raise g['AuthorityError']('limited_capacity') +g['validate_state_ready'] = unavailable +g['install_active_keyset'] = lambda boot_id, mode, *args: ( + recorded.update({'boot_id': boot_id, 'mode': mode, 'args': args}) or 'SHA256:ephemeral' +) +g['write_status'] = lambda boot_id, outcome, **extra: recorded.update( + {'status_boot_id': boot_id, 'outcome': outcome, **extra} +) +assert namespace['main']() == 0 +assert recorded['mode'] == 'ephemeral' +assert recorded['persistence_mode'] == 'ephemeral' +assert recorded['state_reason'] == 'limited_capacity' +assert not state.exists(), 'ephemeral fallback touched unverified persistent state' + + +# Verified state: preserve the persistent identity and publish it to runtime. +state = configure('persistent') +recorded = {} +g['validate_state_ready'] = lambda _boot_id: None +g['secure_state_root'] = lambda: None +g['ensure_secure_directory'] = lambda _path, _mode: None +g['generate_persistent_keyset'] = lambda _boot_id: 'SHA256:persistent' +def publish(boot_id, mode, private_key, public_key, source_fingerprint): + recorded.update({ + 'boot_id': boot_id, + 'mode': mode, + 'private_key': private_key, + 'public_key': public_key, + 'source_fingerprint': source_fingerprint, + }) + return source_fingerprint +g['install_active_keyset'] = publish +g['write_status'] = lambda boot_id, outcome, **extra: recorded.update( + {'status_boot_id': boot_id, 'outcome': outcome, **extra} +) +assert namespace['main']() == 0 +assert recorded['mode'] == 'persistent' +assert recorded['persistence_mode'] == 'persistent' +assert recorded['source_fingerprint'] == 'SHA256:persistent' +assert recorded['state_reason'] is None +"#; + + let result = Command::new("python3") + .arg("-c") + .arg(fixture) + .arg(repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys", + )) + .arg(&root) + .status() + .expect("run diagnostic SSH host-key fixture"); + + let _ = fs::remove_dir_all(root); + assert!(result.success(), "diagnostic SSH host-key fixture failed"); +} From 48660f4311da67985cee2f393fa8d0f1367124a4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:16:08 +0700 Subject: [PATCH 315/386] Verify diagnostic SSH fallback in appliance image --- scripts/verify-usb-appliance.sh | 34 +++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/scripts/verify-usb-appliance.sh b/scripts/verify-usb-appliance.sh index f89df2bf..0b564494 100755 --- a/scripts/verify-usb-appliance.sh +++ b/scripts/verify-usb-appliance.sh @@ -141,28 +141,42 @@ hostkey_service="$temporary/root/etc/systemd/system/rigos-ssh-hostkeys.service" hostkey_policy="$temporary/root/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf" ssh_dropin="$temporary/root/etc/systemd/system/ssh.service.d/rigos-observe.conf" hostkey_authority="$temporary/root/usr/lib/rigos/rigos-ssh-hostkeys" -[[ -x "$hostkey_authority" ]] || die 'persistent SSH host-key authority is missing or not executable' -[[ -L "$temporary/root/etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service" ]] || die 'persistent SSH host-key service is not enabled' +[[ -x "$hostkey_authority" ]] || die 'SSH host-key authority is missing or not executable' +[[ -L "$temporary/root/etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service" ]] || die 'SSH host-key service is not enabled' for required in \ 'After=rigos-state-ready.service' \ - 'Requires=rigos-state-ready.service' \ + 'Wants=rigos-state-ready.service' \ 'Before=ssh.service' \ 'ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys' \ 'ReadWritePaths=/var/lib/rigos /run/rigos' do - grep -Fqx "$required" "$hostkey_service" || die "persistent SSH host-key service contract is missing: $required" + grep -Fqx "$required" "$hostkey_service" || die "SSH host-key service contract is missing: $required" done -grep -Fqx 'HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key' "$hostkey_policy" || die 'sshd persistent HostKey policy is missing' -grep -Fqx 'Requires=rigos-ssh-hostkeys.service' "$ssh_dropin" || die 'ssh.service does not require persistent host identity' -grep -Fqx 'After=rigos-recovery-access.service rigos-ssh-hostkeys.service' "$ssh_dropin" || die 'ssh.service ordering bypasses persistent host identity' +if grep -Fqx 'Requires=rigos-state-ready.service' "$hostkey_service"; then + die 'SSH diagnostics are still hard-blocked by persistent state readiness' +fi +for required in \ + 'HostKey /run/rigos/ssh-hostkeys/ssh_host_ed25519_key' \ + 'PasswordAuthentication yes' \ + 'PermitRootLogin no' \ + 'AllowUsers rigosadmin' +do + grep -Fqx "$required" "$hostkey_policy" || die "sshd diagnostic access policy is missing: $required" +done +grep -Fqx 'Requires=rigos-ssh-hostkeys.service' "$ssh_dropin" || die 'ssh.service does not require an established host identity' +grep -Fqx 'After=rigos-recovery-access.service rigos-ssh-hostkeys.service' "$ssh_dropin" || die 'ssh.service ordering bypasses host identity establishment' for required in \ 'STATE = Path("/var/lib/rigos")' \ 'KEYS = SYSTEM / "ssh-hostkeys"' \ + 'ACTIVE_KEYS = RUNTIME / "ssh-hostkeys"' \ '"schema": "rigos.ssh-hostkeys/v1"' \ - 'os.rename(temporary, KEYS)' \ - '"persistent SSH host identity exists without a valid manifest"' + '"schema": "rigos.ssh-active-hostkeys/v1"' \ + 'mode = "persistent"' \ + 'mode = "ephemeral"' \ + 'install_active_keyset' \ + 'persistent_state_ready' do - grep -Fq "$required" "$hostkey_authority" || die "persistent SSH host-key authority contract is missing: $required" + grep -Fq "$required" "$hostkey_authority" || die "SSH host-key authority contract is missing: $required" done [[ -x "$temporary/root/usr/lib/rigos/rigos-performance" ]] || die 'performance authority is missing or not executable' From 02c6552c1473749ce0376b735510f88cb2bda56d Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:16:33 +0700 Subject: [PATCH 316/386] Verify diagnostic SSH fallback with RandomX image gate --- scripts/verify-randomx-performance-image.sh | 38 ++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/scripts/verify-randomx-performance-image.sh b/scripts/verify-randomx-performance-image.sh index 1a774407..8040981a 100644 --- a/scripts/verify-randomx-performance-image.sh +++ b/scripts/verify-randomx-performance-image.sh @@ -86,11 +86,11 @@ root="$temporary/squash" [[ -L "$root/usr/sbin/modprobe" || -x "$root/usr/sbin/modprobe" ]] || die 'modprobe entrypoint is missing' [[ -f "$root/usr/lib/rigos/rigos-randomx-msr" ]] || die 'RandomX MSR authority is missing' [[ -f "$root/usr/lib/rigos/rigos-miner-gate" ]] || die 'miner safety gate is missing' -[[ -f "$root/usr/lib/rigos/rigos-ssh-hostkeys" ]] || die 'persistent SSH host-key authority is missing' +[[ -f "$root/usr/lib/rigos/rigos-ssh-hostkeys" ]] || die 'SSH host-key authority is missing' [[ -L "$root/etc/systemd/system/multi-user.target.wants/rigos-randomx-msr.service" ]] \ || die 'RandomX MSR authority is not enabled in the appliance' [[ -L "$root/etc/systemd/system/multi-user.target.wants/rigos-ssh-hostkeys.service" ]] \ - || die 'persistent SSH host-key authority is not enabled in the appliance' + || die 'SSH host-key authority is not enabled in the appliance' python3 -m py_compile \ "$root/usr/lib/rigos/rigos-randomx-msr" \ "$root/usr/lib/rigos/rigos-miner-gate" \ @@ -127,29 +127,43 @@ grep -Fqx 'ExecCondition=/usr/lib/rigos/rigos-miner-gate' "$miner" \ for required in \ 'After=rigos-state-ready.service' \ - 'Requires=rigos-state-ready.service' \ + 'Wants=rigos-state-ready.service' \ 'Before=ssh.service' \ 'ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys' \ 'ReadWritePaths=/var/lib/rigos /run/rigos' do grep -Fqx "$required" "$hostkey_service" \ - || die "persistent SSH host-key service contract is missing: $required" + || die "SSH host-key service contract is missing: $required" +done +if grep -Fqx 'Requires=rigos-state-ready.service' "$hostkey_service"; then + die 'SSH diagnostics are hard-blocked by persistent state readiness' +fi +for required in \ + 'HostKey /run/rigos/ssh-hostkeys/ssh_host_ed25519_key' \ + 'PasswordAuthentication yes' \ + 'PermitRootLogin no' \ + 'AllowUsers rigosadmin' +do + grep -Fqx "$required" "$hostkey_policy" \ + || die "sshd diagnostic access policy is missing: $required" done -grep -Fqx 'HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key' "$hostkey_policy" \ - || die 'sshd does not use the persistent RIGOS host identity' grep -Fqx 'Requires=rigos-ssh-hostkeys.service' "$ssh_dropin" \ - || die 'ssh.service does not require persistent host identity' + || die 'ssh.service does not require an established host identity' grep -Fqx 'After=rigos-recovery-access.service rigos-ssh-hostkeys.service' "$ssh_dropin" \ - || die 'ssh.service ordering bypasses persistent host identity' + || die 'ssh.service ordering bypasses host identity establishment' for required in \ 'STATE = Path("/var/lib/rigos")' \ 'KEYS = SYSTEM / "ssh-hostkeys"' \ + 'ACTIVE_KEYS = RUNTIME / "ssh-hostkeys"' \ '"schema": "rigos.ssh-hostkeys/v1"' \ - 'os.rename(temporary, KEYS)' \ - '"persistent SSH host identity exists without a valid manifest"' + '"schema": "rigos.ssh-active-hostkeys/v1"' \ + 'mode = "persistent"' \ + 'mode = "ephemeral"' \ + 'install_active_keyset' \ + 'persistent_state_ready' do grep -Fq "$required" "$hostkey_authority" \ - || die "persistent SSH host-key authority contract is missing: $required" + || die "SSH host-key authority contract is missing: $required" done for required in \ @@ -174,5 +188,5 @@ do done printf 'RIGOS RandomX kernel MSR support: %s\n' "$msr_support" -printf 'RIGOS persistent SSH host-key image verification passed\n' +printf 'RIGOS SSH host-key image verification passed\n' printf 'RIGOS RandomX performance image verification passed: %s\n' "$image" From 0e326500576c2b7e96e47baac9153abd52bfac30 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:17:10 +0700 Subject: [PATCH 317/386] Allow diagnostic SSH after failed state readiness --- scripts/verify-systemd-ordering.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/verify-systemd-ordering.py b/scripts/verify-systemd-ordering.py index 67e13d1b..6a61abc5 100644 --- a/scripts/verify-systemd-ordering.py +++ b/scripts/verify-systemd-ordering.py @@ -123,12 +123,16 @@ def verify(units): includes( hostkeys.words("Unit", "After"), {"rigos-state-ready.service"}, - "SSH host-key authority must follow state readiness", + "SSH host-key authority must follow the state readiness attempt", ) includes( - hostkeys.words("Unit", "Requires"), + hostkeys.words("Unit", "Wants"), {"rigos-state-ready.service"}, - "SSH host-key authority must require state readiness", + "SSH host-key authority must request state readiness", + ) + require( + "rigos-state-ready.service" not in hostkeys.words("Unit", "Requires"), + "diagnostic SSH must survive state readiness failure", ) includes( hostkeys.words("Unit", "Before"), From ee8809057b0eeecbaa260408ed26cc114c0dfa25 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:18:32 +0700 Subject: [PATCH 318/386] Verify alpha.10 diagnostic SSH host-key fallback --- scripts/check-alpha8-ssh-hotfix.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py index 5c513ce8..654826e0 100644 --- a/scripts/check-alpha8-ssh-hotfix.py +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -44,8 +44,14 @@ def main() -> int: raise RuntimeError("OpenSSH server package is missing") hostkey_policy = normalized_lf_bytes(HOSTKEY_POLICY) - if hostkey_policy != b"HostKey /var/lib/rigos/system/ssh-hostkeys/ssh_host_ed25519_key\n": - raise RuntimeError("persistent SSH HostKey policy is not exact") + expected_hostkey_policy = ( + b"HostKey /run/rigos/ssh-hostkeys/ssh_host_ed25519_key\n" + b"PasswordAuthentication yes\n" + b"PermitRootLogin no\n" + b"AllowUsers rigosadmin\n" + ) + if hostkey_policy != expected_hostkey_policy: + raise RuntimeError("diagnostic SSH HostKey and authentication policy is not exact") baked_keys = sorted(path for path in SSH_DIRECTORY.glob("ssh_host_*_key*") if path.is_file()) if baked_keys: raise RuntimeError("appliance source contains a baked SSH host private or public key") @@ -67,26 +73,32 @@ def main() -> int: for required in ( 'STATE = Path("/var/lib/rigos")', 'KEYS = SYSTEM / "ssh-hostkeys"', + 'ACTIVE_KEYS = RUNTIME / "ssh-hostkeys"', '"schema": "rigos.ssh-hostkeys/v1"', - 'os.rename(temporary, KEYS)', + '"schema": "rigos.ssh-active-hostkeys/v1"', + 'mode = "persistent"', + 'mode = "ephemeral"', 'or status.get("outcome") != "ready"', - '"persistent SSH public and private keys do not match"', + '"SSH public and private keys do not match"', '"persistent SSH host identity exists without a valid manifest"', + '"ephemeral SSH host identity generation failed"', ): if required not in authority: - raise RuntimeError(f"persistent SSH host-key authority contract is missing: {required}") + raise RuntimeError(f"SSH host-key authority contract is missing: {required}") require_lines( HOSTKEY_UNIT, ( "After=rigos-state-ready.service", - "Requires=rigos-state-ready.service", + "Wants=rigos-state-ready.service", "Before=ssh.service", "ExecStart=/usr/lib/rigos/rigos-ssh-hostkeys", "ReadWritePaths=/var/lib/rigos /run/rigos", "WantedBy=multi-user.target", ), ) + if "Requires=rigos-state-ready.service" in HOSTKEY_UNIT.read_text(encoding="utf-8"): + raise RuntimeError("diagnostic SSH is still hard-blocked by state readiness") require_lines( SSH_DROPIN, ( @@ -96,7 +108,7 @@ def main() -> int: ), ) if "Before=rigos-ssh-hostkeys.service" not in STATE_READY_UNIT.read_text(encoding="utf-8"): - raise RuntimeError("state readiness is not ordered before persistent SSH identity") + raise RuntimeError("state readiness attempt is not ordered before SSH identity selection") dockerfile = DOCKERFILE.read_text(encoding="utf-8") if 'ENV PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:${PATH}"' not in dockerfile: @@ -128,7 +140,7 @@ def main() -> int: if required not in recovery_gate: raise RuntimeError(f"recovery access validator contract is missing: {required}") - print("RIGOS Alpha8 SSH, recovery, and persistent host-key verification passed") + print("RIGOS SSH, recovery, and diagnostic host-key verification passed") return 0 From 1f2981cefff10c1d8fb1a070d454fe7065f74ed7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:18:52 +0700 Subject: [PATCH 319/386] Gate alpha.10 image builds on diagnostic SSH tests --- scripts/build-usb-image-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 2970c3c6..336a12e2 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -45,6 +45,7 @@ cargo test --locked -p rigos-config --test miner_observer_authority -- --nocaptu cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocapture cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture cargo test --locked -p rigos-config --test firstboot_tty -- --nocapture +cargo test --locked -p rigos-config --test diagnostic_ssh -- --nocapture ./scripts/build-usb-image.sh From 33e02e253b9d0691909e22464d0eee34056958f6 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 00:42:37 +0700 Subject: [PATCH 320/386] Format diagnostic SSH tests --- crates/rigos-config/tests/diagnostic_ssh.rs | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/rigos-config/tests/diagnostic_ssh.rs b/crates/rigos-config/tests/diagnostic_ssh.rs index ad9abcf2..9251e5d2 100644 --- a/crates/rigos-config/tests/diagnostic_ssh.rs +++ b/crates/rigos-config/tests/diagnostic_ssh.rs @@ -17,17 +17,13 @@ fn repo_file(path: &str) -> String { #[test] fn diagnostic_ssh_keeps_host_identity_mandatory_without_requiring_ready_state() { - let service = repo_file( - "build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service", - ); - let ssh_dropin = repo_file( - "build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf", - ); - let policy = repo_file( - "build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf", - ); - let authority = - repo_file("build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys"); + let service = + repo_file("build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service"); + let ssh_dropin = + repo_file("build/usb/includes.chroot/etc/systemd/system/ssh.service.d/rigos-observe.conf"); + let policy = + repo_file("build/usb/includes.chroot/etc/ssh/sshd_config.d/01-rigos-hostkeys.conf"); + let authority = repo_file("build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys"); let version = repo_file("build/usb/version.env"); assert!(service.contains("After=rigos-state-ready.service")); @@ -53,7 +49,10 @@ fn diagnostic_ssh_keeps_host_identity_mandatory_without_requiring_ready_state() "persistent_state_ready", "install_active_keyset", ] { - assert!(authority.contains(required), "authority is missing {required}"); + assert!( + authority.contains(required), + "authority is missing {required}" + ); } assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.10")); From f3779e3ffcf2e5231d0d7ae462f5e38a5eb26545 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:54:14 +0700 Subject: [PATCH 321/386] alpha11: advance immutable image version --- build/usb/version.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/version.env b/build/usb/version.env index 09f806af..aa1456cb 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.10 -RIGOS_IMAGE_VERSION=0.0.4-alpha.10 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.11 +RIGOS_IMAGE_VERSION=0.0.4-alpha.11 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=10 +RIGOS_BUILD_ORDINAL=11 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 From 3a6b7e6e300bb48f01720e3b25210966b7f40cc8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:54:46 +0700 Subject: [PATCH 322/386] alpha11: recover bounded resize2fs timeout truthfully --- .../usr/local/sbin/rigos-state-orchestrate | 80 +++++++++++++++++-- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate index ec0bf416..47c04a83 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate @@ -14,6 +14,8 @@ ATTESTATION = RUNTIME / "boot-device.json" BOOT_ID = Path("/proc/sys/kernel/random/boot_id") PARTUUID_ROOT = Path("/dev/disk/by-partuuid") PARTUUID_RE = re.compile(r"^[0-9A-Fa-f-]{1,128}$") +FILESYSTEM_TIMEOUT_SECONDS = 300 +MAX_COMMAND_OUTPUT = 64 * 1024 def read_json(path: Path) -> dict: @@ -128,26 +130,78 @@ def mark_repair_required(message: str) -> None: write_status(status) +def bounded_output(value: str | bytes | None) -> str: + if value is None: + return "" + if isinstance(value, bytes): + value = value.decode("utf-8", errors="replace") + encoded = value.encode("utf-8", errors="replace") + if len(encoded) > MAX_COMMAND_OUTPUT: + encoded = encoded[:MAX_COMMAND_OUTPUT] + value = encoded.decode("utf-8", errors="replace") + " [truncated]" + return value.strip() + + +def run_repair_command(argv: list[str], accepted: tuple[int, ...]) -> tuple[bool, str | None]: + program = Path(argv[0]).name + try: + result = subprocess.run( + argv, + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=FILESYSTEM_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired as error: + detail = bounded_output(error.stderr) or bounded_output(error.stdout) + suffix = f": {detail}" if detail else "" + return False, f"{program}: timeout after {FILESYSTEM_TIMEOUT_SECONDS}s{suffix}" + if result.returncode not in accepted: + detail = bounded_output(result.stderr) or bounded_output(result.stdout) + suffix = f": {detail}" if detail else "" + return False, f"{program}: exit {result.returncode}{suffix}" + return True, None + + def forced_check() -> bool: device, error = verified_state_device() if error is not None or device is None: mark_repair_required(error or "verified state device is unavailable") return False - result = subprocess.run( - ["/usr/sbin/e2fsck", "-f", "-p", str(device)], - check=False, + ok, failure = run_repair_command( + ["/usr/sbin/e2fsck", "-f", "-p", str(device)], (0, 1) ) - if result.returncode in (0, 1): + if ok: return True - mark_repair_required( - f"forced ext4 check requires owner intervention with exit code {result.returncode}" - ) + mark_repair_required(f"forced ext4 check failed: {failure}") return False +def complete_resize_after_timeout() -> bool: + device, error = verified_state_device() + if error is not None or device is None: + mark_repair_required(error or "verified state device is unavailable") + return False + checked, check_failure = run_repair_command( + ["/usr/sbin/e2fsck", "-f", "-p", str(device)], (0, 1) + ) + if not checked: + mark_repair_required(f"post-timeout ext4 check failed: {check_failure}") + return False + resized, resize_failure = run_repair_command( + ["/usr/sbin/resize2fs", str(device)], (0,) + ) + if not resized: + mark_repair_required(f"state filesystem resize failed: {resize_failure}") + return False + return True + + def main() -> int: retried_missing_device = False repaired_filesystem = False + completed_resize_timeout = False for _attempt in range(4): result = run_core() if result != 0: @@ -165,13 +219,23 @@ def main() -> int: time.sleep(1) continue if ( - status.get("outcome") == "limited_capacity" + status.get("outcome") in ("limited_capacity", "repair_required") and not repaired_filesystem and "e2fsck -f" in message ): repaired_filesystem = True if forced_check(): continue + return 0 + if ( + status.get("outcome") in ("limited_capacity", "repair_required") + and not completed_resize_timeout + and "resize2fs: timeout" in message + ): + completed_resize_timeout = True + if complete_resize_after_timeout(): + continue + return 0 return 0 return 0 From 11e4ddf212dd5be2efd3cfe798c546f812486755 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:55:06 +0700 Subject: [PATCH 323/386] alpha11: allow bounded USB filesystem recovery window --- build/usb/includes.chroot/etc/systemd/system/rigos-state.service | 1 + 1 file changed, 1 insertion(+) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service index 97d891ba..aedfef6c 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service @@ -8,6 +8,7 @@ Before=local-fs.target rigos-state-ready.service Type=oneshot ExecStartPre=/usr/bin/systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf ExecStart=/usr/local/sbin/rigos-state-orchestrate +TimeoutStartSec=12min RemainAfterExit=yes [Install] From cabee4bce304a9ee037baca28671d4acf0402057 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:55:37 +0700 Subject: [PATCH 324/386] alpha11: make recovery credential persistence truthful --- .../usr/local/sbin/rigos-recovery-access | 134 ++++++++++++++---- 1 file changed, 103 insertions(+), 31 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access index 92a48545..21a9b007 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access @@ -12,6 +12,7 @@ STATE = Path("/var/lib/rigos") CREDENTIAL_DIRECTORY = STATE / "recovery" CREDENTIAL_FILE = CREDENTIAL_DIRECTORY / "rigosadmin-password.hash" BOOT_ID = Path("/proc/sys/kernel/random/boot_id") +STATE_STATUS = RUNTIME / "state-status.json" MAX_HASH_BYTES = 512 MODULAR_CRYPT = re.compile(r"^\$[A-Za-z0-9][A-Za-z0-9./,_=-]*(?:\$[A-Za-z0-9./,_=-]+){2,}$") @@ -52,6 +53,60 @@ def valid_password_hash(value: str) -> bool: return MODULAR_CRYPT.fullmatch(value) is not None +def read_state_status() -> dict: + try: + raw = STATE_STATUS.read_bytes() + if not raw or len(raw) > 64 * 1024: + return {} + value = json.loads(raw) + return value if isinstance(value, dict) else {} + except (OSError, UnicodeError, json.JSONDecodeError): + return {} + + +def persistent_store_ready(status: dict) -> bool: + try: + boot_id = BOOT_ID.read_text(encoding="ascii").strip() + except OSError: + return False + if ( + status.get("schema") != "rigos.state-status/v1" + or status.get("boot_id") != boot_id + or status.get("outcome") != "ready" + or status.get("mountpoint") != str(STATE) + ): + return False + result = subprocess.run( + [ + "/usr/bin/findmnt", + "--json", + "--target", + str(STATE), + "--output", + "TARGET,FSTYPE,OPTIONS", + ], + check=False, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + if result.returncode != 0: + return False + try: + document = json.loads(result.stdout) + filesystems = document.get("filesystems", []) + mount = filesystems[0] if isinstance(filesystems, list) and filesystems else {} + except (TypeError, json.JSONDecodeError): + return False + options = set(str(mount.get("options", "")).split(",")) + required = {"rw", "nosuid", "nodev", "noexec", "noatime"} + return ( + mount.get("target") == str(STATE) + and mount.get("fstype") == "ext4" + and required.issubset(options) + ) + + def ensure_credential_directory() -> None: CREDENTIAL_DIRECTORY.mkdir(mode=0o700, parents=True, exist_ok=True) info = CREDENTIAL_DIRECTORY.lstat() @@ -116,15 +171,25 @@ def restore_password_hash(value: str) -> bool: return result.returncode == 0 and password_ready() -def prompt_for_password(invalid_stored_hash: bool) -> None: - message = ( - "The saved local credential is invalid and will be replaced. Set the local rig " - "administrator password next." - if invalid_stored_hash - else "Set the local rig administrator password next. This enables tty1 diagnostics only." - ) +def prompt_for_password(invalid_stored_hash: bool, persistent: bool) -> None: + if invalid_stored_hash: + message = ( + "The saved local credential is invalid and will be replaced. " + "Set the local rig administrator password next." + ) + elif persistent: + message = ( + "Set the local rig administrator password next. The encrypted password hash " + "will be stored in verified persistent state." + ) + else: + message = ( + "Persistent state is unavailable. Set a temporary rig administrator password " + "for this boot so local and SSH diagnostics remain available. This password is " + "not persistent and may be requested again after reboot." + ) subprocess.run( - ["/usr/bin/whiptail", "--title", "RIGOS RECOVERY ACCESS", "--msgbox", message, "11", "72"], + ["/usr/bin/whiptail", "--title", "RIGOS RECOVERY ACCESS", "--msgbox", message, "12", "76"], check=False, ) subprocess.run(["/usr/bin/passwd", "rigosadmin"], check=True) @@ -162,49 +227,56 @@ def write_status(value: dict) -> None: def main() -> int: - ensure_credential_directory() - stored_hash, stored_hash_invalid = load_persistent_hash() - credential_action = "existing" + state_status = read_state_status() + state_outcome = str(state_status.get("outcome") or "unavailable") + persistent = persistent_store_ready(state_status) + stored_hash = None + stored_hash_invalid = False + if persistent: + ensure_credential_directory() + stored_hash, stored_hash_invalid = load_persistent_hash() + credential_action = "existing" if password_ready(): live_hash = current_password_hash() if live_hash is None: raise RuntimeError("usable local credential hash is unavailable") - if stored_hash != live_hash: + if persistent and stored_hash != live_hash: persist_password_hash(live_hash) - elif stored_hash is not None and restore_password_hash(stored_hash): + elif persistent and stored_hash is not None and restore_password_hash(stored_hash): credential_action = "restored" else: - prompt_for_password(stored_hash_invalid) + prompt_for_password(stored_hash_invalid, persistent) if not password_ready(): raise RuntimeError("local credential setup did not produce a usable password") live_hash = current_password_hash() if live_hash is None: raise RuntimeError("new local credential hash is unavailable") - persist_password_hash(live_hash) + if persistent: + persist_password_hash(live_hash) credential_action = "created" local_access = password_ready() - try: - state_outcome = json.loads((RUNTIME / "state-status.json").read_text(encoding="utf-8")).get("outcome", "unavailable") - except (OSError, json.JSONDecodeError): - state_outcome = "unavailable" remote_active = any(unit_active(name) for name in ("ssh.service", "sshd.service")) remote_enabled = any(unit_enabled(name) for name in ("ssh.service", "sshd.service")) remote_inactive = not remote_active and not remote_enabled - configured = (STATE / "current").is_symlink() + configured = persistent and (STATE / "current").is_symlink() mode = "operational" if state_outcome == "ready" and remote_inactive else "recovery" - write_status({ - "schema": "rigos.recovery-access-status/v1", - "boot_id": BOOT_ID.read_text(encoding="ascii").strip(), - "mode": mode, - "local_console_access": local_access, - "state_outcome": state_outcome, - "remote_access": "inactive" if remote_inactive else "unexpected_active", - "configuration_commit": "present" if configured else "absent", - "credential_action": credential_action, - "credential_persisted": CREDENTIAL_FILE.is_file(), - }) + credential_persisted = persistent and CREDENTIAL_FILE.is_file() + write_status( + { + "schema": "rigos.recovery-access-status/v1", + "boot_id": BOOT_ID.read_text(encoding="ascii").strip(), + "mode": mode, + "local_console_access": local_access, + "state_outcome": state_outcome, + "remote_access": "inactive" if remote_inactive else "unexpected_active", + "configuration_commit": "present" if configured else "absent", + "credential_action": credential_action, + "credential_scope": "persistent" if persistent else "boot", + "credential_persisted": credential_persisted, + } + ) return 0 if local_access and remote_inactive else 1 From 0b51a4d9bf3bc1a0a65f79d6ea49af3c2908f0d3 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:55:56 +0700 Subject: [PATCH 325/386] alpha11: accept explicit boot-scoped recovery credentials --- .../usr/lib/rigos/rigos-recovery-access-verify | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify b/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify index ac3dd072..cd1b2057 100644 --- a/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify +++ b/build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify @@ -42,11 +42,25 @@ def main() -> int: return deny("recovery_status_stale") if status.get("local_console_access") is not True: return deny("local_credential_unavailable") - if status.get("credential_persisted") is not True: - return deny("credential_not_persisted") if status.get("credential_action") not in ("existing", "created", "restored"): return deny("credential_action_invalid") + scope = status.get("credential_scope") + persisted = status.get("credential_persisted") + state_outcome = status.get("state_outcome") + if scope == "persistent": + if persisted is not True: + return deny("persistent_credential_missing") + if state_outcome != "ready": + return deny("persistent_credential_without_ready_state") + elif scope == "boot": + if persisted is not False: + return deny("boot_credential_claims_persistence") + if state_outcome == "ready": + return deny("ready_state_without_persistent_credential") + else: + return deny("credential_scope_invalid") + emit("allowed") return 0 From 353d75030de22fd4f4a86b567996bbdbec530206 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:56:47 +0700 Subject: [PATCH 326/386] alpha11: test persistent and boot-scoped recovery gates --- .../tests/alpha8_recovery_gate.rs | 75 +++++++++++++------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/crates/rigos-config/tests/alpha8_recovery_gate.rs b/crates/rigos-config/tests/alpha8_recovery_gate.rs index dce3b59b..4f4ca0b7 100644 --- a/crates/rigos-config/tests/alpha8_recovery_gate.rs +++ b/crates/rigos-config/tests/alpha8_recovery_gate.rs @@ -9,8 +9,18 @@ fn repo_path(path: &str) -> PathBuf { .join(path) } +fn run_gate(runtime: &PathBuf, boot: &PathBuf) -> std::process::ExitStatus { + let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify"); + Command::new("python3") + .arg(gate) + .env("RIGOS_RUNTIME_PATH", runtime) + .env("RIGOS_BOOT_ID_PATH", boot) + .status() + .unwrap() +} + #[test] -fn recovery_gate_accepts_only_current_persisted_credential_truth() { +fn recovery_gate_accepts_persistent_or_explicit_boot_scoped_credential_truth() { let root = std::env::temp_dir().join(format!("rigos-recovery-gate-{}", Uuid::new_v4())); let runtime = root.join("run"); let boot_id = root.join("boot-id"); @@ -18,46 +28,69 @@ fn recovery_gate_accepts_only_current_persisted_credential_truth() { fs::write(&boot_id, "boot-test\n").unwrap(); let status = runtime.join("recovery-access-status.json"); - let valid = serde_json::json!({ + let persistent = serde_json::json!({ "schema": "rigos.recovery-access-status/v1", "boot_id": "boot-test", "local_console_access": true, "credential_action": "created", - "credential_persisted": true + "credential_scope": "persistent", + "credential_persisted": true, + "state_outcome": "ready" }); - fs::write(&status, serde_json::to_vec(&valid).unwrap()).unwrap(); - - let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify"); - let run = |runtime_path: &PathBuf, boot_path: &PathBuf| { - Command::new("python3") - .arg(&gate) - .env("RIGOS_RUNTIME_PATH", runtime_path) - .env("RIGOS_BOOT_ID_PATH", boot_path) - .status() - .unwrap() - }; + fs::write(&status, serde_json::to_vec(&persistent).unwrap()).unwrap(); + assert!(run_gate(&runtime, &boot_id).success()); - assert!(run(&runtime, &boot_id).success()); + let boot_scoped = serde_json::json!({ + "schema": "rigos.recovery-access-status/v1", + "boot_id": "boot-test", + "local_console_access": true, + "credential_action": "created", + "credential_scope": "boot", + "credential_persisted": false, + "state_outcome": "repair_required" + }); + fs::write(&status, serde_json::to_vec(&boot_scoped).unwrap()).unwrap(); + assert!(run_gate(&runtime, &boot_id).success()); let stale = serde_json::json!({ "schema": "rigos.recovery-access-status/v1", "boot_id": "old-boot", "local_console_access": true, "credential_action": "created", - "credential_persisted": true + "credential_scope": "persistent", + "credential_persisted": true, + "state_outcome": "ready" }); fs::write(&status, serde_json::to_vec(&stale).unwrap()).unwrap(); - assert_eq!(run(&runtime, &boot_id).code(), Some(2)); + assert_eq!(run_gate(&runtime, &boot_id).code(), Some(2)); + + let missing_persistent_store = serde_json::json!({ + "schema": "rigos.recovery-access-status/v1", + "boot_id": "boot-test", + "local_console_access": true, + "credential_action": "created", + "credential_scope": "persistent", + "credential_persisted": false, + "state_outcome": "ready" + }); + fs::write( + &status, + serde_json::to_vec(&missing_persistent_store).unwrap(), + ) + .unwrap(); + assert_eq!(run_gate(&runtime, &boot_id).code(), Some(2)); - let not_persisted = serde_json::json!({ + let false_boot_claim = serde_json::json!({ "schema": "rigos.recovery-access-status/v1", "boot_id": "boot-test", "local_console_access": true, "credential_action": "created", - "credential_persisted": false + "credential_scope": "boot", + "credential_persisted": true, + "state_outcome": "limited_capacity" }); - fs::write(&status, serde_json::to_vec(¬_persisted).unwrap()).unwrap(); - assert_eq!(run(&runtime, &boot_id).code(), Some(2)); + fs::write(&status, serde_json::to_vec(&false_boot_claim).unwrap()).unwrap(); + assert_eq!(run_gate(&runtime, &boot_id).code(), Some(2)); let _ = fs::remove_dir_all(root); } From c6e631a6e4a95f0806d50f45ab9757900a208c53 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:57:26 +0700 Subject: [PATCH 327/386] alpha11: test truthful recovery credential persistence --- crates/rigos-config/tests/recovery_access.rs | 49 ++++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/crates/rigos-config/tests/recovery_access.rs b/crates/rigos-config/tests/recovery_access.rs index fe6ff850..db144b23 100644 --- a/crates/rigos-config/tests/recovery_access.rs +++ b/crates/rigos-config/tests/recovery_access.rs @@ -39,10 +39,17 @@ g['STATE'] = root / 'state' g['CREDENTIAL_DIRECTORY'] = root / 'state' / 'recovery' g['CREDENTIAL_FILE'] = g['CREDENTIAL_DIRECTORY'] / 'rigosadmin-password.hash' g['BOOT_ID'] = root / 'boot-id' +g['STATE_STATUS'] = g['RUNTIME'] / 'state-status.json' g['BOOT_ID'].write_text('boot-test\n', encoding='ascii') g['RUNTIME'].mkdir() g['STATE'].mkdir() -(g['RUNTIME'] / 'state-status.json').write_text('{"outcome":"ready"}', encoding='utf-8') +g['STATE_STATUS'].write_text(json.dumps({ + 'schema': 'rigos.state-status/v1', + 'boot_id': 'boot-test', + 'outcome': 'ready', + 'mountpoint': str(g['STATE']), +}), encoding='utf-8') +g['persistent_store_ready'] = lambda _status: True valid_hash = '$y$j9T$syntheticSalt$syntheticHashValue' assert namespace['valid_password_hash'](valid_hash) @@ -54,7 +61,8 @@ live_ready = {'value': False} prompts = [] persisted = [] g['password_ready'] = lambda: live_ready['value'] -def prompt(_invalid): +def prompt(_invalid, persistent): + assert persistent is True prompts.append(True) live_ready['value'] = True g['prompt_for_password'] = prompt @@ -72,6 +80,7 @@ assert stat.S_IMODE(g['CREDENTIAL_DIRECTORY'].stat().st_mode) == 0o700 assert stat.S_IMODE(g['CREDENTIAL_FILE'].stat().st_mode) == 0o600 status = json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text()) assert status['credential_action'] == 'created' +assert status['credential_scope'] == 'persistent' assert status['credential_persisted'] is True assert valid_hash not in json.dumps(status) @@ -95,6 +104,8 @@ assert not prompts assert not any('/usr/bin/passwd' in argv for argv, _kwargs in calls) status = json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text()) assert status['credential_action'] == 'restored' +assert status['credential_scope'] == 'persistent' +assert status['credential_persisted'] is True assert valid_hash not in json.dumps(status) # Existing live credential migrates without a prompt. @@ -109,15 +120,43 @@ g['CREDENTIAL_FILE'].write_text('!unsafe\n', encoding='ascii') g['CREDENTIAL_FILE'].chmod(0o600) live_ready['value'] = False invalid_flags = [] -def replacement(invalid): - invalid_flags.append(invalid) +def replacement(invalid, persistent): + invalid_flags.append((invalid, persistent)) live_ready['value'] = True g['prompt_for_password'] = replacement calls.clear() assert namespace['main']() == 0 -assert invalid_flags == [True] +assert invalid_flags == [(True, True)] assert not any(argv == ['/usr/sbin/chpasswd', '--encrypted'] for argv, _kwargs in calls) assert valid_hash not in json.dumps(json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text())) + +# Unready state uses a truthful boot-scoped credential and never touches the store. +g['CREDENTIAL_FILE'].unlink(missing_ok=True) +g['STATE_STATUS'].write_text(json.dumps({ + 'schema': 'rigos.state-status/v1', + 'boot_id': 'boot-test', + 'outcome': 'repair_required', + 'mountpoint': None, +}), encoding='utf-8') +g['persistent_store_ready'] = lambda _status: False +live_ready['value'] = False +boot_prompts = [] +def boot_prompt(invalid, persistent): + assert invalid is False and persistent is False + boot_prompts.append(True) + live_ready['value'] = True +g['prompt_for_password'] = boot_prompt +g['persist_password_hash'] = lambda _value: (_ for _ in ()).throw( + AssertionError('boot credential touched persistent state') +) +assert namespace['main']() == 0 +assert boot_prompts == [True] +assert not g['CREDENTIAL_FILE'].exists() +status = json.loads((g['RUNTIME'] / 'recovery-access-status.json').read_text()) +assert status['credential_scope'] == 'boot' +assert status['credential_persisted'] is False +assert status['state_outcome'] == 'repair_required' +assert valid_hash not in json.dumps(status) "#; let result = Command::new("python3") .arg("-c") From 30f1e24ac6d746bddab286d5053764f155445a00 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:58:06 +0700 Subject: [PATCH 328/386] alpha11: verify truthful recovery credential scope --- scripts/check-alpha8-ssh-hotfix.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/check-alpha8-ssh-hotfix.py b/scripts/check-alpha8-ssh-hotfix.py index 654826e0..7324c2be 100644 --- a/scripts/check-alpha8-ssh-hotfix.py +++ b/scripts/check-alpha8-ssh-hotfix.py @@ -9,6 +9,7 @@ HOOK = ROOT / "build/usb/hooks/010-rigos.chroot" DOCKERFILE = ROOT / "build/usb/Dockerfile" RECOVERY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-recovery-access.service" +RECOVERY_AUTHORITY = ROOT / "build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access" RECOVERY_GATE = ROOT / "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify" STATE_READY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-state-ready.service" HOSTKEY_UNIT = ROOT / "build/usb/includes.chroot/etc/systemd/system/rigos-ssh-hostkeys.service" @@ -130,12 +131,27 @@ def main() -> int: if required not in recovery_unit: raise RuntimeError(f"recovery access hotfix wiring is missing: {required}") + recovery_authority = RECOVERY_AUTHORITY.read_text(encoding="utf-8") + compile(recovery_authority, str(RECOVERY_AUTHORITY), "exec") + for required in ( + "def persistent_store_ready(status: dict) -> bool:", + '"credential_scope": "persistent" if persistent else "boot"', + "credential_persisted = persistent and CREDENTIAL_FILE.is_file()", + "if persistent:", + "This password is not persistent", + ): + if required not in recovery_authority: + raise RuntimeError(f"recovery credential authority contract is missing: {required}") + recovery_gate = RECOVERY_GATE.read_text(encoding="utf-8") compile(recovery_gate, str(RECOVERY_GATE), "exec") for required in ( 'status.get("boot_id") != boot_id', 'status.get("local_console_access") is not True', - 'status.get("credential_persisted") is not True', + 'scope = status.get("credential_scope")', + 'if scope == "persistent":', + 'elif scope == "boot":', + 'return deny("boot_credential_claims_persistence")', ): if required not in recovery_gate: raise RuntimeError(f"recovery access validator contract is missing: {required}") From 7120fb59a14f96664067fe03723454dd1e4cc7f5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:58:46 +0700 Subject: [PATCH 329/386] alpha11: advance diagnostic SSH image contract --- crates/rigos-config/tests/diagnostic_ssh.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/diagnostic_ssh.rs b/crates/rigos-config/tests/diagnostic_ssh.rs index 9251e5d2..b0c9fb0f 100644 --- a/crates/rigos-config/tests/diagnostic_ssh.rs +++ b/crates/rigos-config/tests/diagnostic_ssh.rs @@ -55,8 +55,8 @@ fn diagnostic_ssh_keeps_host_identity_mandatory_without_requiring_ready_state() ); } - assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.10")); - assert!(version.contains("RIGOS_BUILD_ORDINAL=10")); + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.11")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=11")); } #[test] From 69e15e17be29c13842ee0016bf91bff1a9a8a859 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:59:18 +0700 Subject: [PATCH 330/386] alpha11: advance exact-image gate contract --- crates/rigos-config/tests/firstboot_image_gate.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_image_gate.rs b/crates/rigos-config/tests/firstboot_image_gate.rs index 8b623ffb..a4c4b7d6 100644 --- a/crates/rigos-config/tests/firstboot_image_gate.rs +++ b/crates/rigos-config/tests/firstboot_image_gate.rs @@ -10,17 +10,18 @@ fn repo_file(path: &str) -> String { } #[test] -fn alpha10_build_runs_source_and_exact_image_firstboot_gates() { +fn alpha11_build_runs_source_and_exact_image_firstboot_gates() { let version = repo_file("build/usb/version.env"); let entrypoint = repo_file("scripts/build-usb-image-entrypoint.sh"); let verifier = repo_file("scripts/verify-firstboot-image.sh"); let hook = repo_file("build/usb/hooks/010-rigos.chroot"); - assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.10")); - assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.10")); - assert!(version.contains("RIGOS_BUILD_ORDINAL=10")); + assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.11")); + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.11")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=11")); assert!(entrypoint.contains("--test firstboot_tty")); + assert!(entrypoint.contains("--test state_resize_recovery")); assert!(entrypoint.contains("bash ./scripts/verify-firstboot-image.sh \"$image\"")); for required in [ From 9b58f25e648fb822884bb52da9161df723058ee4 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 01:59:49 +0700 Subject: [PATCH 331/386] alpha11: add state resize recovery regression tests --- .../tests/state_resize_recovery.rs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 crates/rigos-config/tests/state_resize_recovery.rs diff --git a/crates/rigos-config/tests/state_resize_recovery.rs b/crates/rigos-config/tests/state_resize_recovery.rs new file mode 100644 index 00000000..68fcf635 --- /dev/null +++ b/crates/rigos-config/tests/state_resize_recovery.rs @@ -0,0 +1,104 @@ +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn repo_path(path: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(path) +} + +#[test] +fn state_resize_timeout_has_a_bounded_verified_recovery_path() { + let orchestrator = fs::read_to_string(repo_path( + "build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate", + )) + .unwrap(); + let service = fs::read_to_string(repo_path( + "build/usb/includes.chroot/etc/systemd/system/rigos-state.service", + )) + .unwrap(); + + for required in [ + "FILESYSTEM_TIMEOUT_SECONDS = 300", + "def complete_resize_after_timeout() -> bool:", + "timeout=FILESYSTEM_TIMEOUT_SECONDS", + "post-timeout ext4 check failed", + "state filesystem resize failed", + "resize2fs: timeout", + ] { + assert!( + orchestrator.contains(required), + "state recovery contract is missing: {required}" + ); + } + assert!(service.contains("TimeoutStartSec=12min")); +} + +#[test] +fn resize_timeout_recovery_retries_core_and_reports_failed_repair_truthfully() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("rigos-state-resize-{unique}")); + fs::create_dir_all(&root).unwrap(); + + let fixture = r#" +import runpy +import sys +from pathlib import Path + +source = Path(sys.argv[1]) +root = Path(sys.argv[2]) +namespace = runpy.run_path(str(source), run_name='rigos_state_resize_test') +g = namespace['main'].__globals__ +g['STATUS'] = root / 'state-status.json' +g['ATTESTATION'] = root / 'boot-device.json' +g['BOOT_ID'] = root / 'boot-id' +g['BOOT_ID'].write_text('boot-test\n', encoding='ascii') + +# A core resize timeout is completed under the longer verified repair budget, +# then core is rerun to perform the normal mount and initialization path. +statuses = [ + {'outcome': 'limited_capacity', 'message': 'bounded command failed: resize2fs: timeout'}, + {'outcome': 'ready', 'message': None}, +] +core_calls = [] +repair_calls = [] +g['run_core'] = lambda: core_calls.append(True) or 0 +def read_json(path): + if path == g['STATUS']: + return statuses.pop(0) + return {} +g['read_json'] = read_json +g['complete_resize_after_timeout'] = lambda: repair_calls.append(True) or True +assert namespace['main']() == 0 +assert len(core_calls) == 2 +assert repair_calls == [True] +assert statuses == [] + +# A failed long repair is not reclassified as limited capacity. +recorded = [] +g['verified_state_device'] = lambda: (Path('/dev/test-state'), None) +results = iter([(True, None), (False, 'resize2fs: timeout after 300s')]) +g['run_repair_command'] = lambda _argv, _accepted: next(results) +g['mark_repair_required'] = lambda message: recorded.append(message) +assert namespace['complete_resize_after_timeout']() is False +assert recorded == ['state filesystem resize failed: resize2fs: timeout after 300s'] +"#; + + let result = Command::new("python3") + .arg("-c") + .arg(fixture) + .arg(repo_path( + "build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate", + )) + .arg(&root) + .status() + .expect("run state resize recovery fixture"); + + let _ = fs::remove_dir_all(root); + assert!(result.success(), "state resize recovery fixture failed"); +} From 5209a84a87bf1f3c2fac3b0204260eab69a1140c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:00:29 +0700 Subject: [PATCH 332/386] alpha11: gate state resize and recovery credential paths --- scripts/build-usb-image-entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 336a12e2..469c0b70 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -22,6 +22,9 @@ source "$version_env" python3 ./scripts/check-alpha8-ssh-hotfix.py python3 ./scripts/verify-systemd-ordering.py python3 -m py_compile \ + ./build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access \ + ./build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate \ + ./build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-randomx-msr \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-miner-gate \ ./build/usb/includes.chroot/usr/lib/rigos/rigos-ssh-hostkeys \ @@ -46,6 +49,7 @@ cargo test --locked -p rigos-config --test randomx_build_entrypoint -- --nocaptu cargo test --locked -p rigos-config --test randomx_msr_authority -- --nocapture cargo test --locked -p rigos-config --test firstboot_tty -- --nocapture cargo test --locked -p rigos-config --test diagnostic_ssh -- --nocapture +cargo test --locked -p rigos-config --test state_resize_recovery -- --nocapture ./scripts/build-usb-image.sh From 7872316ec7481a680c394aedd57380ea66293262 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:01:28 +0700 Subject: [PATCH 333/386] alpha11: add exact-image state recovery verifier --- scripts/verify-state-recovery-image.sh | 100 +++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 scripts/verify-state-recovery-image.sh diff --git a/scripts/verify-state-recovery-image.sh b/scripts/verify-state-recovery-image.sh new file mode 100644 index 00000000..3ee33998 --- /dev/null +++ b/scripts/verify-state-recovery-image.sh @@ -0,0 +1,100 @@ +#!/bin/bash +set -euo pipefail + +die() { + printf 'verify-state-recovery-image: %s\n' "$*" >&2 + exit 1 +} + +[[ $# -eq 1 ]] || die 'usage: verify-state-recovery-image.sh ' +image="$(readlink -f "$1")" +[[ -f "$image" ]] || die "image is missing: $image" +[[ "$(id -u)" -eq 0 ]] || die 'must run as root' + +partition_json="$(sfdisk --json "$image")" +start="$(jq -r '.partitiontable.partitions[1].start' <<<"$partition_json")" +size="$(jq -r '.partitiontable.partitions[1].size' <<<"$partition_json")" +[[ "$start" =~ ^[0-9]+$ && "$size" =~ ^[0-9]+$ ]] || die 'ROOT_A geometry is invalid' + +loop="$( + losetup --find --show --read-only \ + --offset $((start * 512)) \ + --sizelimit $((size * 512)) \ + "$image" +)" +temporary="$(mktemp -d)" +cleanup() { + set +e + mountpoint -q "$temporary/root-a" && umount "$temporary/root-a" + losetup -d "$loop" 2>/dev/null + rm -rf "$temporary" +} +trap cleanup EXIT + +mkdir -p "$temporary/root-a" "$temporary/squash" +mount -o ro "$loop" "$temporary/root-a" +squashfs="$temporary/root-a/live/filesystem.squashfs" +[[ -f "$squashfs" ]] || die 'ROOT_A squashfs is missing' + +unsquashfs -no-progress -d "$temporary/squash" "$squashfs" \ + usr/bin/python3 usr/bin/python3.11 \ + usr/local/sbin/rigos-state-orchestrate \ + usr/local/sbin/rigos-recovery-access \ + usr/lib/rigos/rigos-recovery-access-verify \ + etc/systemd/system/rigos-state.service \ + etc/rigos-release \ + >/dev/null + +root="$temporary/squash" +orchestrator="$root/usr/local/sbin/rigos-state-orchestrate" +recovery="$root/usr/local/sbin/rigos-recovery-access" +gate="$root/usr/lib/rigos/rigos-recovery-access-verify" +state_service="$root/etc/systemd/system/rigos-state.service" +release="$root/etc/rigos-release" + +[[ -x "$root/usr/bin/python3" ]] || die 'Python runtime is missing' +[[ -x "$orchestrator" ]] || die 'state orchestrator is missing or not executable' +[[ -x "$recovery" ]] || die 'recovery credential authority is missing or not executable' +[[ -x "$gate" ]] || die 'recovery credential gate is missing or not executable' +[[ -f "$state_service" ]] || die 'state service is missing' +[[ -f "$release" ]] || die 'release metadata is missing' + +python3 -m py_compile "$orchestrator" "$recovery" "$gate" +grep -Fqx 'VERSION_ID="0.0.4-alpha.11"' "$release" \ + || die 'embedded alpha.11 version is missing' +grep -Fqx 'TimeoutStartSec=12min' "$state_service" \ + || die 'state service recovery window is missing' + +for required in \ + 'FILESYSTEM_TIMEOUT_SECONDS = 300' \ + 'def complete_resize_after_timeout() -> bool:' \ + 'timeout=FILESYSTEM_TIMEOUT_SECONDS' \ + 'post-timeout ext4 check failed' \ + 'state filesystem resize failed' \ + 'resize2fs: timeout' +do + grep -Fq "$required" "$orchestrator" \ + || die "state resize recovery contract is missing: $required" +done + +for required in \ + 'def persistent_store_ready(status: dict) -> bool:' \ + '"credential_scope": "persistent" if persistent else "boot"' \ + 'credential_persisted = persistent and CREDENTIAL_FILE.is_file()' \ + 'This password is not persistent' +do + grep -Fq "$required" "$recovery" \ + || die "recovery credential contract is missing: $required" +done + +for required in \ + 'scope = status.get("credential_scope")' \ + 'if scope == "persistent":' \ + 'elif scope == "boot":' \ + 'boot_credential_claims_persistence' +do + grep -Fq "$required" "$gate" \ + || die "recovery credential gate contract is missing: $required" +done + +printf 'RIGOS state recovery and credential image verification passed: %s\n' "$image" From a983bca7d469f2afa9d8a622b1a0f1d6d3b2b5a3 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:02:00 +0700 Subject: [PATCH 334/386] alpha11: run exact-image state recovery gate --- scripts/build-usb-image-entrypoint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-usb-image-entrypoint.sh b/scripts/build-usb-image-entrypoint.sh index 469c0b70..94943f47 100644 --- a/scripts/build-usb-image-entrypoint.sh +++ b/scripts/build-usb-image-entrypoint.sh @@ -57,3 +57,4 @@ image="./dist/usb/${RIGOS_IMAGE_ID}-${RIGOS_IMAGE_VERSION}.img" bash ./scripts/verify-randomx-performance-image.sh "$image" bash ./scripts/verify-miner-observer-image.sh "$image" bash ./scripts/verify-firstboot-image.sh "$image" +bash ./scripts/verify-state-recovery-image.sh "$image" From 76b4a129ce00b2acd9b3113fff09aacf93d4f6d7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:02:23 +0700 Subject: [PATCH 335/386] alpha11: lock exact-image state recovery contract --- crates/rigos-config/tests/state_resize_recovery.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/rigos-config/tests/state_resize_recovery.rs b/crates/rigos-config/tests/state_resize_recovery.rs index 68fcf635..9f687b1b 100644 --- a/crates/rigos-config/tests/state_resize_recovery.rs +++ b/crates/rigos-config/tests/state_resize_recovery.rs @@ -19,6 +19,9 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { "build/usb/includes.chroot/etc/systemd/system/rigos-state.service", )) .unwrap(); + let entrypoint = fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")).unwrap(); + let image_verifier = + fs::read_to_string(repo_path("scripts/verify-state-recovery-image.sh")).unwrap(); for required in [ "FILESYSTEM_TIMEOUT_SECONDS = 300", @@ -32,8 +35,16 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { orchestrator.contains(required), "state recovery contract is missing: {required}" ); + assert!( + image_verifier.contains(required), + "exact-image verifier is missing: {required}" + ); } assert!(service.contains("TimeoutStartSec=12min")); + assert!(image_verifier.contains("losetup --find --show --read-only")); + assert!(image_verifier.contains("mount -o ro")); + assert!(!image_verifier.contains("mount -o rw")); + assert!(entrypoint.contains("bash ./scripts/verify-state-recovery-image.sh \"$image\"")); } #[test] From 194b1adfcfd0c531ccf02a2b01693ceda31b542a Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:03:23 +0700 Subject: [PATCH 336/386] alpha11: check recovery gate as interpreted Python source --- scripts/verify-state-recovery-image.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-state-recovery-image.sh b/scripts/verify-state-recovery-image.sh index 3ee33998..fab7186a 100644 --- a/scripts/verify-state-recovery-image.sh +++ b/scripts/verify-state-recovery-image.sh @@ -55,7 +55,7 @@ release="$root/etc/rigos-release" [[ -x "$root/usr/bin/python3" ]] || die 'Python runtime is missing' [[ -x "$orchestrator" ]] || die 'state orchestrator is missing or not executable' [[ -x "$recovery" ]] || die 'recovery credential authority is missing or not executable' -[[ -x "$gate" ]] || die 'recovery credential gate is missing or not executable' +[[ -f "$gate" ]] || die 'recovery credential gate is missing' [[ -f "$state_service" ]] || die 'state service is missing' [[ -f "$release" ]] || die 'release metadata is missing' From 57941921132cb57f507fc542fbe88e20079fcec8 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:04:57 +0700 Subject: [PATCH 337/386] alpha11: rustfmt recovery gate test --- crates/rigos-config/tests/alpha8_recovery_gate.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/rigos-config/tests/alpha8_recovery_gate.rs b/crates/rigos-config/tests/alpha8_recovery_gate.rs index 4f4ca0b7..45ff4b85 100644 --- a/crates/rigos-config/tests/alpha8_recovery_gate.rs +++ b/crates/rigos-config/tests/alpha8_recovery_gate.rs @@ -10,7 +10,9 @@ fn repo_path(path: &str) -> PathBuf { } fn run_gate(runtime: &PathBuf, boot: &PathBuf) -> std::process::ExitStatus { - let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify"); + let gate = repo_path( + "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify", + ); Command::new("python3") .arg(gate) .env("RIGOS_RUNTIME_PATH", runtime) From c8775aecc7897b65be764c77989fcaae7b598795 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:05:15 +0700 Subject: [PATCH 338/386] alpha11: rustfmt state resize recovery test --- crates/rigos-config/tests/state_resize_recovery.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/state_resize_recovery.rs b/crates/rigos-config/tests/state_resize_recovery.rs index 9f687b1b..7baf47cd 100644 --- a/crates/rigos-config/tests/state_resize_recovery.rs +++ b/crates/rigos-config/tests/state_resize_recovery.rs @@ -19,7 +19,8 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { "build/usb/includes.chroot/etc/systemd/system/rigos-state.service", )) .unwrap(); - let entrypoint = fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")).unwrap(); + let entrypoint = + fs::read_to_string(repo_path("scripts/build-usb-image-entrypoint.sh")).unwrap(); let image_verifier = fs::read_to_string(repo_path("scripts/verify-state-recovery-image.sh")).unwrap(); @@ -44,7 +45,9 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { assert!(image_verifier.contains("losetup --find --show --read-only")); assert!(image_verifier.contains("mount -o ro")); assert!(!image_verifier.contains("mount -o rw")); - assert!(entrypoint.contains("bash ./scripts/verify-state-recovery-image.sh \"$image\"")); + assert!( + entrypoint.contains("bash ./scripts/verify-state-recovery-image.sh \"$image\"") + ); } #[test] From 6a11d1c3d4e978e7959a0e2950c05cf1f440194b Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:05:59 +0700 Subject: [PATCH 339/386] alpha11: recover e2fsck and repeated resize timeouts truthfully --- .../usr/local/sbin/rigos-state-orchestrate | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate index 47c04a83..2963114b 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate @@ -210,28 +210,32 @@ def main() -> int: if status.get("outcome") == "ready": return 0 message = str(status.get("message") or "") + outcome = status.get("outcome") + recoverable = outcome in ("limited_capacity", "repair_required") if ( - status.get("outcome") == "limited_capacity" + outcome == "limited_capacity" and not retried_missing_device and "No such file or directory" in message ): retried_missing_device = True time.sleep(1) continue - if ( - status.get("outcome") in ("limited_capacity", "repair_required") - and not repaired_filesystem - and "e2fsck -f" in message - ): + if recoverable and ("e2fsck -f" in message or "e2fsck: timeout" in message): + if repaired_filesystem: + mark_repair_required( + "state core ext4 check remained incomplete after bounded repair" + ) + return 0 repaired_filesystem = True if forced_check(): continue return 0 - if ( - status.get("outcome") in ("limited_capacity", "repair_required") - and not completed_resize_timeout - and "resize2fs: timeout" in message - ): + if recoverable and "resize2fs: timeout" in message: + if completed_resize_timeout: + mark_repair_required( + "state core resize remained incomplete after bounded repair" + ) + return 0 completed_resize_timeout = True if complete_resize_after_timeout(): continue From b3737dedbd9721d1ebd9b6f24e6062c13bee208c Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:19:44 +0700 Subject: [PATCH 340/386] Format alpha11 recovery gate test --- crates/rigos-config/tests/alpha8_recovery_gate.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/alpha8_recovery_gate.rs b/crates/rigos-config/tests/alpha8_recovery_gate.rs index 45ff4b85..4f4ca0b7 100644 --- a/crates/rigos-config/tests/alpha8_recovery_gate.rs +++ b/crates/rigos-config/tests/alpha8_recovery_gate.rs @@ -10,9 +10,7 @@ fn repo_path(path: &str) -> PathBuf { } fn run_gate(runtime: &PathBuf, boot: &PathBuf) -> std::process::ExitStatus { - let gate = repo_path( - "build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify", - ); + let gate = repo_path("build/usb/includes.chroot/usr/lib/rigos/rigos-recovery-access-verify"); Command::new("python3") .arg(gate) .env("RIGOS_RUNTIME_PATH", runtime) From 5d9e5b2e91783a57a51d6802d7d269f9428e92e7 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:20:02 +0700 Subject: [PATCH 341/386] Format alpha11 state recovery test --- crates/rigos-config/tests/state_resize_recovery.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/state_resize_recovery.rs b/crates/rigos-config/tests/state_resize_recovery.rs index 7baf47cd..13473f74 100644 --- a/crates/rigos-config/tests/state_resize_recovery.rs +++ b/crates/rigos-config/tests/state_resize_recovery.rs @@ -45,9 +45,7 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { assert!(image_verifier.contains("losetup --find --show --read-only")); assert!(image_verifier.contains("mount -o ro")); assert!(!image_verifier.contains("mount -o rw")); - assert!( - entrypoint.contains("bash ./scripts/verify-state-recovery-image.sh \"$image\"") - ); + assert!(entrypoint.contains("bash ./scripts/verify-state-recovery-image.sh \"$image\"")); } #[test] From 15f1a2c0476d00724fc9054347de268d2a9d1162 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 02:39:19 +0700 Subject: [PATCH 342/386] Fix alpha11 recovery message verifier contract --- .../usb/includes.chroot/usr/local/sbin/rigos-recovery-access | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access index 21a9b007..291efe81 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-recovery-access @@ -185,8 +185,8 @@ def prompt_for_password(invalid_stored_hash: bool, persistent: bool) -> None: else: message = ( "Persistent state is unavailable. Set a temporary rig administrator password " - "for this boot so local and SSH diagnostics remain available. This password is " - "not persistent and may be requested again after reboot." + "for this boot so local and SSH diagnostics remain available. " + "This password is not persistent and may be requested again after reboot." ) subprocess.run( ["/usr/bin/whiptail", "--title", "RIGOS RECOVERY ACCESS", "--msgbox", message, "12", "76"], From df844ea506b06e3455017120594c42eac35e14d5 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:26:22 +0700 Subject: [PATCH 343/386] fix(state): repair preen exit 4 on verified USB state --- .../usr/local/sbin/rigos-state-orchestrate | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate index 2963114b..bdba251e 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate @@ -15,6 +15,7 @@ BOOT_ID = Path("/proc/sys/kernel/random/boot_id") PARTUUID_ROOT = Path("/dev/disk/by-partuuid") PARTUUID_RE = re.compile(r"^[0-9A-Fa-f-]{1,128}$") FILESYSTEM_TIMEOUT_SECONDS = 300 +E2FSCK_UNCORRECTED_EXIT = 4 MAX_COMMAND_OUTPUT = 64 * 1024 @@ -111,6 +112,15 @@ def verified_state_device() -> tuple[Path | None, str | None]: return device, None +def revalidate_state_device(expected: Path) -> tuple[Path | None, str | None]: + observed, error = verified_state_device() + if error is not None or observed is None: + return None, error or "verified state device is unavailable" + if observed != expected: + return None, "verified state device changed during repair" + return observed, None + + def mark_repair_required(message: str) -> None: status = read_json(STATUS) attestation = read_json(ATTESTATION) @@ -164,18 +174,41 @@ def run_repair_command(argv: list[str], accepted: tuple[int, ...]) -> tuple[bool return True, None +def repair_ext4(device: Path, failure_prefix: str) -> bool: + checked, check_failure = run_repair_command( + ["/usr/sbin/e2fsck", "-f", "-p", str(device)], (0, 1) + ) + if checked: + return True + uncorrected = f"e2fsck: exit {E2FSCK_UNCORRECTED_EXIT}" + if check_failure is None or not check_failure.startswith(uncorrected): + mark_repair_required(f"{failure_prefix}: {check_failure}") + return False + + revalidated, error = revalidate_state_device(device) + if error is not None or revalidated is None: + mark_repair_required(error or "verified state device is unavailable before repair") + return False + repaired, repair_failure = run_repair_command( + ["/usr/sbin/e2fsck", "-f", "-y", str(revalidated)], (0, 1) + ) + if not repaired: + mark_repair_required(f"automatic ext4 repair failed: {repair_failure}") + return False + + revalidated, error = revalidate_state_device(device) + if error is not None or revalidated is None: + mark_repair_required(error or "verified state device is unavailable after repair") + return False + return True + + def forced_check() -> bool: device, error = verified_state_device() if error is not None or device is None: mark_repair_required(error or "verified state device is unavailable") return False - ok, failure = run_repair_command( - ["/usr/sbin/e2fsck", "-f", "-p", str(device)], (0, 1) - ) - if ok: - return True - mark_repair_required(f"forced ext4 check failed: {failure}") - return False + return repair_ext4(device, "forced ext4 check failed") def complete_resize_after_timeout() -> bool: @@ -183,14 +216,14 @@ def complete_resize_after_timeout() -> bool: if error is not None or device is None: mark_repair_required(error or "verified state device is unavailable") return False - checked, check_failure = run_repair_command( - ["/usr/sbin/e2fsck", "-f", "-p", str(device)], (0, 1) - ) - if not checked: - mark_repair_required(f"post-timeout ext4 check failed: {check_failure}") + if not repair_ext4(device, "post-timeout ext4 check failed"): + return False + revalidated, error = revalidate_state_device(device) + if error is not None or revalidated is None: + mark_repair_required(error or "verified state device is unavailable before resize") return False resized, resize_failure = run_repair_command( - ["/usr/sbin/resize2fs", str(device)], (0,) + ["/usr/sbin/resize2fs", str(revalidated)], (0,) ) if not resized: mark_repair_required(f"state filesystem resize failed: {resize_failure}") From c4ff273bedd8afc740c252e7e1b00c5b74f79abf Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:26:36 +0700 Subject: [PATCH 344/386] fix(state): allow bounded full repair window --- .../usb/includes.chroot/etc/systemd/system/rigos-state.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service index aedfef6c..017dc20b 100644 --- a/build/usb/includes.chroot/etc/systemd/system/rigos-state.service +++ b/build/usb/includes.chroot/etc/systemd/system/rigos-state.service @@ -8,7 +8,7 @@ Before=local-fs.target rigos-state-ready.service Type=oneshot ExecStartPre=/usr/bin/systemd-tmpfiles --create /usr/lib/tmpfiles.d/rigos.conf ExecStart=/usr/local/sbin/rigos-state-orchestrate -TimeoutStartSec=12min +TimeoutStartSec=20min RemainAfterExit=yes [Install] From 76be5e802eb6b109bba57d8a2ffebd52db56fbd6 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:26:47 +0700 Subject: [PATCH 345/386] build: advance immutable artifact to alpha.12 --- build/usb/version.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/usb/version.env b/build/usb/version.env index aa1456cb..d70e22ff 100644 --- a/build/usb/version.env +++ b/build/usb/version.env @@ -1,8 +1,8 @@ -RIGOS_PRODUCT_VERSION=0.0.4-alpha.11 -RIGOS_IMAGE_VERSION=0.0.4-alpha.11 +RIGOS_PRODUCT_VERSION=0.0.4-alpha.12 +RIGOS_IMAGE_VERSION=0.0.4-alpha.12 RIGOS_IMAGE_ID=rigos-usb-amd64 RIGOS_IMAGE_CHANNEL=alpha -RIGOS_BUILD_ORDINAL=11 +RIGOS_BUILD_ORDINAL=12 RIGOS_XMRIG_VERSION=6.26.0 RIGOS_XMRIG_ARCHIVE_SHA256=fc6f8ae5f64e4f17481f7e3be29a1c56949f216a998414188003eae1db20c9e5 RIGOS_XMRIG_BINARY_SHA256=b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49 From b29970ed5464b2d10d42d65e25774d0ac04ca9ae Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:27:10 +0700 Subject: [PATCH 346/386] test(state): cover bounded e2fsck exit 4 escalation --- .../tests/state_resize_recovery.rs | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/rigos-config/tests/state_resize_recovery.rs b/crates/rigos-config/tests/state_resize_recovery.rs index 13473f74..f4dc49c4 100644 --- a/crates/rigos-config/tests/state_resize_recovery.rs +++ b/crates/rigos-config/tests/state_resize_recovery.rs @@ -26,11 +26,14 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { for required in [ "FILESYSTEM_TIMEOUT_SECONDS = 300", + "E2FSCK_UNCORRECTED_EXIT = 4", + "def repair_ext4(device: Path, failure_prefix: str) -> bool:", "def complete_resize_after_timeout() -> bool:", "timeout=FILESYSTEM_TIMEOUT_SECONDS", - "post-timeout ext4 check failed", + "automatic ext4 repair failed", "state filesystem resize failed", "resize2fs: timeout", + "[\"/usr/sbin/e2fsck\", \"-f\", \"-y\"", ] { assert!( orchestrator.contains(required), @@ -41,7 +44,7 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { "exact-image verifier is missing: {required}" ); } - assert!(service.contains("TimeoutStartSec=12min")); + assert!(service.contains("TimeoutStartSec=20min")); assert!(image_verifier.contains("losetup --find --show --read-only")); assert!(image_verifier.contains("mount -o ro")); assert!(!image_verifier.contains("mount -o rw")); @@ -49,7 +52,7 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { } #[test] -fn resize_timeout_recovery_retries_core_and_reports_failed_repair_truthfully() { +fn resize_timeout_recovery_retries_core_and_repairs_only_e2fsck_exit_four() { let unique = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() @@ -91,12 +94,41 @@ assert len(core_calls) == 2 assert repair_calls == [True] assert statuses == [] -# A failed long repair is not reclassified as limited capacity. +# Preen exit 4 is the only condition that escalates to bounded -y repair. recorded = [] +calls = [] g['verified_state_device'] = lambda: (Path('/dev/test-state'), None) -results = iter([(True, None), (False, 'resize2fs: timeout after 300s')]) -g['run_repair_command'] = lambda _argv, _accepted: next(results) +results = iter([ + (False, 'e2fsck: exit 4: unexpected inconsistency'), + (True, None), + (True, None), +]) +def run_repair(argv, accepted): + calls.append((argv, accepted)) + return next(results) +g['run_repair_command'] = run_repair g['mark_repair_required'] = lambda message: recorded.append(message) +assert namespace['complete_resize_after_timeout']() is True +assert [call[0][0:3] for call in calls] == [ + ['/usr/sbin/e2fsck', '-f', '-p'], + ['/usr/sbin/e2fsck', '-f', '-y'], + ['/usr/sbin/resize2fs', '/dev/test-state'], +] +assert recorded == [] + +# Operational e2fsck failures never escalate to -y. +recorded.clear() +calls.clear() +results = iter([(False, 'e2fsck: exit 8: operational error')]) +assert namespace['complete_resize_after_timeout']() is False +assert len(calls) == 1 +assert calls[0][0][0:3] == ['/usr/sbin/e2fsck', '-f', '-p'] +assert recorded == ['post-timeout ext4 check failed: e2fsck: exit 8: operational error'] + +# A failed resize remains repair_required and is never reclassified as capacity. +recorded.clear() +calls.clear() +results = iter([(True, None), (False, 'resize2fs: timeout after 300s')]) assert namespace['complete_resize_after_timeout']() is False assert recorded == ['state filesystem resize failed: resize2fs: timeout after 300s'] "#; From 05cebd42017b52334bc537c09a0a23e57bd2a0cc Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:27:28 +0700 Subject: [PATCH 347/386] test(image): require alpha.12 verified fsck escalation --- scripts/verify-state-recovery-image.sh | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/verify-state-recovery-image.sh b/scripts/verify-state-recovery-image.sh index fab7186a..7121ec0d 100644 --- a/scripts/verify-state-recovery-image.sh +++ b/scripts/verify-state-recovery-image.sh @@ -60,21 +60,33 @@ release="$root/etc/rigos-release" [[ -f "$release" ]] || die 'release metadata is missing' python3 -m py_compile "$orchestrator" "$recovery" "$gate" -grep -Fqx 'VERSION_ID="0.0.4-alpha.11"' "$release" \ - || die 'embedded alpha.11 version is missing' -grep -Fqx 'TimeoutStartSec=12min' "$state_service" \ - || die 'state service recovery window is missing' +grep -Fqx 'VERSION_ID="0.0.4-alpha.12"' "$release" \ + || die 'embedded alpha.12 version is missing' +grep -Fqx 'TimeoutStartSec=20min' "$state_service" \ + || die 'state service full repair window is missing' for required in \ 'FILESYSTEM_TIMEOUT_SECONDS = 300' \ + 'E2FSCK_UNCORRECTED_EXIT = 4' \ + 'def repair_ext4(device: Path, failure_prefix: str) -> bool:' \ 'def complete_resize_after_timeout() -> bool:' \ 'timeout=FILESYSTEM_TIMEOUT_SECONDS' \ - 'post-timeout ext4 check failed' \ + 'automatic ext4 repair failed' \ 'state filesystem resize failed' \ - 'resize2fs: timeout' + 'resize2fs: timeout' \ + '["/usr/sbin/e2fsck", "-f", "-y"' do grep -Fq "$required" "$orchestrator" \ - || die "state resize recovery contract is missing: $required" + || die "state repair contract is missing: $required" +done + +for required in \ + 'def revalidate_state_device(expected: Path)' \ + 'verified state device changed during repair' \ + 'if check_failure is None or not check_failure.startswith(uncorrected):' +do + grep -Fq "$required" "$orchestrator" \ + || die "state repair safety boundary is missing: $required" done for required in \ From fa0db7ff35bb44ce40db203ef87625075e694185 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:27:42 +0700 Subject: [PATCH 348/386] test(firstboot): advance exact image assertions to alpha.12 --- crates/rigos-config/tests/firstboot_image_gate.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rigos-config/tests/firstboot_image_gate.rs b/crates/rigos-config/tests/firstboot_image_gate.rs index a4c4b7d6..0ef5143b 100644 --- a/crates/rigos-config/tests/firstboot_image_gate.rs +++ b/crates/rigos-config/tests/firstboot_image_gate.rs @@ -10,15 +10,15 @@ fn repo_file(path: &str) -> String { } #[test] -fn alpha11_build_runs_source_and_exact_image_firstboot_gates() { +fn alpha12_build_runs_source_and_exact_image_firstboot_gates() { let version = repo_file("build/usb/version.env"); let entrypoint = repo_file("scripts/build-usb-image-entrypoint.sh"); let verifier = repo_file("scripts/verify-firstboot-image.sh"); let hook = repo_file("build/usb/hooks/010-rigos.chroot"); - assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.11")); - assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.11")); - assert!(version.contains("RIGOS_BUILD_ORDINAL=11")); + assert!(version.contains("RIGOS_PRODUCT_VERSION=0.0.4-alpha.12")); + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.12")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=12")); assert!(entrypoint.contains("--test firstboot_tty")); assert!(entrypoint.contains("--test state_resize_recovery")); From 0d28c3a122385a0c1131335d84dd4e2617a452ad Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:29:10 +0700 Subject: [PATCH 349/386] fix(state): route core e2fsck exit 4 into verified repair --- .../includes.chroot/usr/local/sbin/rigos-state-orchestrate | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate index bdba251e..3568003a 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate @@ -253,7 +253,12 @@ def main() -> int: retried_missing_device = True time.sleep(1) continue - if recoverable and ("e2fsck -f" in message or "e2fsck: timeout" in message): + e2fsck_failure = ( + "e2fsck -f" in message + or "e2fsck: timeout" in message + or f"e2fsck: exit {E2FSCK_UNCORRECTED_EXIT}" in message + ) + if recoverable and e2fsck_failure: if repaired_filesystem: mark_repair_required( "state core ext4 check remained incomplete after bounded repair" From 7fc073a208ce9dddf3904fce8fdf972ed3357424 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:29:42 +0700 Subject: [PATCH 350/386] test(state): prove core exit 4 reaches verified repair --- .../tests/state_resize_recovery.rs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/rigos-config/tests/state_resize_recovery.rs b/crates/rigos-config/tests/state_resize_recovery.rs index f4dc49c4..55cc1602 100644 --- a/crates/rigos-config/tests/state_resize_recovery.rs +++ b/crates/rigos-config/tests/state_resize_recovery.rs @@ -34,6 +34,7 @@ fn state_resize_timeout_has_a_bounded_verified_recovery_path() { "state filesystem resize failed", "resize2fs: timeout", "[\"/usr/sbin/e2fsck\", \"-f\", \"-y\"", + "f\"e2fsck: exit {E2FSCK_UNCORRECTED_EXIT}\" in message", ] { assert!( orchestrator.contains(required), @@ -81,17 +82,33 @@ statuses = [ {'outcome': 'ready', 'message': None}, ] core_calls = [] -repair_calls = [] +resize_calls = [] g['run_core'] = lambda: core_calls.append(True) or 0 def read_json(path): if path == g['STATUS']: return statuses.pop(0) return {} g['read_json'] = read_json -g['complete_resize_after_timeout'] = lambda: repair_calls.append(True) or True +g['complete_resize_after_timeout'] = lambda: resize_calls.append(True) or True assert namespace['main']() == 0 assert len(core_calls) == 2 -assert repair_calls == [True] +assert resize_calls == [True] +assert statuses == [] + +# The exact exit-4 message emitted by the core reaches the verified fsck path. +statuses = [ + { + 'outcome': 'limited_capacity', + 'message': 'bounded command failed: e2fsck: exit 4: unexpected inconsistency', + }, + {'outcome': 'ready', 'message': None}, +] +core_calls.clear() +forced_calls = [] +g['forced_check'] = lambda: forced_calls.append(True) or True +assert namespace['main']() == 0 +assert len(core_calls) == 2 +assert forced_calls == [True] assert statuses == [] # Preen exit 4 is the only condition that escalates to bounded -y repair. From 55dfd228c41177893bf9be4ae6f1d285ac4a11c9 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:30:13 +0700 Subject: [PATCH 351/386] test(image): require core exit 4 routing --- scripts/verify-state-recovery-image.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/verify-state-recovery-image.sh b/scripts/verify-state-recovery-image.sh index 7121ec0d..0e514a25 100644 --- a/scripts/verify-state-recovery-image.sh +++ b/scripts/verify-state-recovery-image.sh @@ -74,7 +74,8 @@ for required in \ 'automatic ext4 repair failed' \ 'state filesystem resize failed' \ 'resize2fs: timeout' \ - '["/usr/sbin/e2fsck", "-f", "-y"' + '["/usr/sbin/e2fsck", "-f", "-y"' \ + 'f"e2fsck: exit {E2FSCK_UNCORRECTED_EXIT}" in message' do grep -Fq "$required" "$orchestrator" \ || die "state repair contract is missing: $required" From 5af491daed09dff4b5f90244f4fee718edd5ade2 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 03:39:23 +0700 Subject: [PATCH 352/386] tests: update diagnostic SSH release assertions for alpha12 --- crates/rigos-config/tests/diagnostic_ssh.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rigos-config/tests/diagnostic_ssh.rs b/crates/rigos-config/tests/diagnostic_ssh.rs index b0c9fb0f..c3a4760a 100644 --- a/crates/rigos-config/tests/diagnostic_ssh.rs +++ b/crates/rigos-config/tests/diagnostic_ssh.rs @@ -55,8 +55,8 @@ fn diagnostic_ssh_keeps_host_identity_mandatory_without_requiring_ready_state() ); } - assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.11")); - assert!(version.contains("RIGOS_BUILD_ORDINAL=11")); + assert!(version.contains("RIGOS_IMAGE_VERSION=0.0.4-alpha.12")); + assert!(version.contains("RIGOS_BUILD_ORDINAL=12")); } #[test] From ab829bab412eda060054f4fa53c9268dda6f5918 Mon Sep 17 00:00:00 2001 From: DEADBYTE Date: Sat, 11 Jul 2026 04:31:36 +0700 Subject: [PATCH 353/386] fix(state): parse Rust e2fsck Option exit status --- .../usr/local/sbin/rigos-state-orchestrate | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate index 3568003a..16ff6b12 100644 --- a/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate +++ b/build/usb/includes.chroot/usr/local/sbin/rigos-state-orchestrate @@ -14,6 +14,9 @@ ATTESTATION = RUNTIME / "boot-device.json" BOOT_ID = Path("/proc/sys/kernel/random/boot_id") PARTUUID_ROOT = Path("/dev/disk/by-partuuid") PARTUUID_RE = re.compile(r"^[0-9A-Fa-f-]{1,128}$") +E2FSCK_EXIT_RE = re.compile( + r"\be2fsck: exit (?:(?P[0-9]+)|Some\((?P