xtask/src/isoinstaller.rs
//! `cargo xtask iso-installer` — build gunnar's **installer ISO** through the
//! dwarves Build Thing, from inside the gunnar checkout.
//!
//! ## Why this verb has to spawn dwarves instead of doing the work
//!
//! gunnar declares its build VARIANTS in `.nornir/build-variants.toml` like every
//! other repo in the constellation, and the rule is that a variant is built
//! through the Build Thing door, never a hand-rolled `cargo build` line — the
//! declaration is the only authority on the variant's features, and a raw cargo
//! line silently produces the WRONG artifact by inheriting `default` instead.
//!
//! **The model is variant -> features -> settings/sub-settings -> typ.** This
//! verb used to say `--suite ORIGINAL --atom gunnar-installer`, which is the
//! vocabulary that model replaced, and it hardcoded the ONE variant — so
//! `gunnar-installer-metered`, `-aws` and `-azure` were declared and
//! unreachable from xtask. It now passes `--variant` and takes the name.
//!
//! But gunnar's xtask carries **zero dependencies** on purpose (repository
//! tooling is invoked as a tool, never as a dependency;
//! asserted by `cargo xtask guard`), so it cannot link `nornir-build-thing` and
//! read that declaration itself. The door is therefore only reachable from
//! `dwarves`, which does link it. This verb is the missing signpost: it spawns
//! that exact door with the arguments already correct, so an operator does not
//! have to know the dwarves incantation to build a gunnar artifact.
//!
//! Spawning `cargo` is not the shell-out the laws forbid — cargo is the build
//! tool, and `xtask` already spawns it for `install-test`, `bench` and `bare`.
//!
//! ## The two ISOs, which is not a detail
//!
//! gunnar declares **two** media and they are not interchangeable:
//!
//! * `gunnar-appliance-iso` (kind `iso`) — a **live** ISO. It *is* the appliance:
//! `/data` is initramfs RAM, so nothing survives a power cut and the SSH host
//! key is re-minted every boot. Built by `cargo xtask iso`.
//! * `gunnar-installer` (kind `installer`) — an **installer** ISO. It *produces*
//! an appliance: korp-installer runs as PID 1, lays down GPT + ESP + payload +
//! a real FAT32 `/data` on P3, then boots off the installed disk. THIS verb.
//!
//! ## Requirements on the target machine (from `korp_installer::hwdetect`)
//!
//! * **UEFI firmware.** A legacy-BIOS target is refused by name.
//! * **A target disk of at least 512 MiB**; the variant declares `target_disk_gib = 64`.
//! * Optional RAID1 needs **two disks of the same model and exact size**.
use std::path::{Path, PathBuf};
use std::process::Command;
/// The variant built when the operator names none.
pub const DEFAULT_VARIANT: &str = "gunnar-installer";
/// Where a variant's medium lands, relative to the gunnar checkout. The
/// composer names the file after the variant, so this is derived and not a
/// constant — hardcoding one file name is what made this verb single-variant.
pub fn iso_out(variant: &str) -> String {
format!(".iso-out/{variant}.iso")
}
/// What the operator asked for.
#[derive(Debug)]
struct Ask {
variant: String,
/// `--opt k=v`, passed through in order.
opts: Vec<String>,
/// `--param k=v`, passed through in order: a SITE value for a parameter the
/// variant declares (`clock=peer://…`, `clock_key=…`). dwarves validates it
/// against the declaration, so nothing is checked twice here.
params: Vec<String>,
/// `--build <stage>`; `kernel` compiles the kernel from source in the
/// pinned container instead of carrying one off this box.
build: Option<String>,
/// `--unenrolled <why>` — the DELIBERATE, NAMED act of composing a medium
/// with no authenticity anchor. See [`RootKey::Unenrolled`].
unenrolled: Option<String>,
}
/// Parse the passthrough. Unknown flags are an error rather than ignored: a
/// silently dropped `--variant` builds the wrong ISO and says nothing.
fn parse(args: &[String]) -> Result<Ask, String> {
let (mut variant, mut opts, mut params, mut build) =
(DEFAULT_VARIANT.to_string(), Vec::new(), Vec::new(), None);
let mut unenrolled: Option<String> = None;
let mut i = 0;
while i < args.len() {
let need = |i: usize, what: &str| -> Result<String, String> {
args.get(i + 1)
.cloned()
.ok_or_else(|| format!("{what} needs a value"))
};
match args[i].as_str() {
"--variant" => {
variant = need(i, "--variant")?;
i += 2;
}
"--opt" => {
opts.push(need(i, "--opt")?);
i += 2;
}
"--param" => {
params.push(need(i, "--param")?);
i += 2;
}
"--build" => {
build = Some(need(i, "--build")?);
i += 2;
}
// The unsigned path is the EXCEPTION and it is spelled out loud. It
// takes a REASON rather than being a bare switch: the reason is the
// only thing that will still exist when somebody finds the medium in
// six months and asks why it has no anchor.
"--unenrolled" => {
unenrolled = Some(need(i, "--unenrolled")?);
i += 2;
}
other => {
return Err(format!(
"unknown argument {other:?}. This verb takes --variant <name>, \
--opt <k=v> (repeatable), --param <k=v> (repeatable), --build <stage> \
and --unenrolled <why>; see --help"
))
}
}
}
Ok(Ask {
variant,
opts,
params,
build,
unenrolled,
})
}
/// **The medium carries the SITE, never a box** (2026-09-21, T17). A
/// `hub_conf` param whose text declares `[Interface]` (the ceremony dialect's
/// only key there is `Address`) is refused: one medium installs the primary AND
/// its twin, so an address on it is the same tunnel address on both. The box's
/// address is handed over at bootstrap (`Bootstrap.AssignTunnelAddress`,
/// `gunnar control tunnel enrol`).
///
/// `@fixture` is not read here, because there is nothing to read: it is a WORD
/// the Build Thing resolves later, in its own process, through edda's
/// `korp_installer::wgfixture` (the one writer). That writer renders the site
/// only since edda `14d1472` (agent/wg-address-at-bootstrap). Until a
/// korp-installer carrying it is published and pinned, the fixture a given
/// dwarves resolves may still render the old `[Interface] Address` — which
/// the box reads as an older medium's, said out loud every boot.
pub fn hub_conf_param_refusal(
param: &str,
read: &dyn Fn(&Path) -> Result<String, String>,
) -> Option<String> {
let (name, value) = param.split_once('=')?;
if !(name == "hub_conf" || name.ends_with(".hub_conf")) || value == "@fixture" {
return None;
}
let text = match value.strip_prefix('@') {
Some(path) => match read(Path::new(path)) {
Ok(t) => t,
Err(e) => return Some(format!("REFUSED [hub-conf] --param {name}=@{path}: {e}")),
},
None => value.replace("\\n", "\n"),
};
let per_box = text.lines().map(str::trim).find(|l| {
l.eq_ignore_ascii_case("[interface]")
|| l.split_once('=')
.is_some_and(|(k, _)| k.trim().eq_ignore_ascii_case("address"))
})?;
Some(format!(
"REFUSED [hub-conf] --param {name} declares `{per_box}` — a per-BOX value on a medium that installs every box of \
the site (the primary and its twin get the SAME tunnel address). Drop the [Interface] section: the medium \
carries the hub's PublicKey, Endpoint, AllowedIPs and PersistentKeepalive only, and each box is handed its \
address at bootstrap by `gunnar control tunnel enrol` (Bootstrap.AssignTunnelAddress)"
))
}
/// Locate the sibling `dwarves` checkout — the constellation is a flat set of
/// checkouts beside each other, so it is `../dwarves` from the gunnar root.
/// `$DWARVES_ROOT` overrides for a non-standard layout.
pub fn dwarves_root(gunnar_root: &Path) -> Result<PathBuf, String> {
if let Ok(p) = std::env::var("DWARVES_ROOT") {
if !p.is_empty() {
let p = PathBuf::from(p);
return if p.join("Cargo.toml").is_file() {
Ok(p)
} else {
Err(format!("$DWARVES_ROOT={} has no Cargo.toml", p.display()))
};
}
}
let sibling = gunnar_root
.parent()
.map(|p| p.join("dwarves"))
.unwrap_or_default();
if sibling.join("Cargo.toml").is_file() {
Ok(sibling)
} else {
Err(format!(
"no dwarves checkout at {} — gunnar's build atoms are declared here but the \
Build Thing that reads them lives in dwarves. Clone it beside this repo, or \
point $DWARVES_ROOT at it",
sibling.display()
))
}
}
/// **Where the kernel in a shipped ISO came from.**
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Kernel {
/// Compiled from source in the pinned toolchain container (`--build kernel`).
/// The only path that needs no exception, because nothing off this box
/// enters the medium.
FromSource,
/// Named by the operator: `$KORP_KERNEL` or `$TUNNR_KERNEL`. A declared
/// artifact, wherever it was built.
Named { var: &'static str, path: PathBuf },
/// Carried off THIS box's `/boot`, under the named exception. Says so.
HostException(PathBuf),
}
impl Kernel {
/// The line the verb prints, so a receipt records WHICH of the three
/// happened rather than "a kernel".
pub fn describe(&self) -> String {
match self {
Kernel::FromSource => "from source, in the pinned container".to_string(),
Kernel::Named { var, path } => format!("{} (declared by ${var})", path.display()),
Kernel::HostException(p) => format!(
"{} — CARRIED OFF THIS BOX under the named exception $GUNNAR_HOST_KERNEL",
p.display()
),
}
}
}
/// The env-var name that grants the named exception. Not a boolean somebody can
/// set by accident: it has to be typed, and it appears in the verb's output.
pub const HOST_KERNEL_EXCEPTION: &str = "GUNNAR_HOST_KERNEL";
/// Everything the decision depends on, gathered by [`resolve_kernel`] and
/// handed here as values — so the decision itself is pure and has tests.
pub struct KernelFacts {
/// `--build <stage>`, verbatim.
pub build: Option<String>,
/// `$KORP_KERNEL`/`$TUNNR_KERNEL`, in that order: the var's name, its
/// value, and whether this user can read it.
pub named: Option<(&'static str, String, bool)>,
/// Readable `/boot/vmlinuz*` on this box, newest LAST.
pub host: Vec<PathBuf>,
/// `$GUNNAR_HOST_KERNEL` is set and non-empty.
pub exception: bool,
}
/// **Pick the kernel the installer ISO carries, and refuse rather than guess.**
///
/// ## The defect this replaced
///
/// This used to end in "newest readable `/boot/vmlinuz-*` wins". That is the
/// HOST's kernel — whatever this laptop happens to be running, whatever the
/// distribution last upgraded to overnight — silently sealed into a medium that
/// installs an appliance somewhere else. Two things were wrong with it:
///
/// 1. **The law.** Every build goes through the declared toolchain container;
/// a host path is a NAMED EXCEPTION, and an exception that is taken by
/// default is not an exception, it is the rule.
/// 2. **Nobody could tell.** The ISO's provenance was a line on stderr in a
/// build nobody re-read. An appliance that panics at boot because its
/// kernel lacks a config the recipe needs (`tunnr`'s PCI hot-plug is
/// exactly this) looks identical to one built correctly.
///
/// So: `--build kernel` compiles it in the container and needs no exception. An
/// explicit `$KORP_KERNEL`/`$TUNNR_KERNEL` is a DECLARED artifact and is taken.
/// Anything else is REFUSED BY NAME, and the refusal lists the three ways
/// forward — including the exception, which must be typed and which the verb
/// then prints on every line that mentions the kernel.
pub fn decide_kernel(f: &KernelFacts) -> Result<Kernel, String> {
// A kernel stage compiles its own, and the recipe's symbol requirements
// reach it only through that chained build.
if f.build.as_deref() == Some("kernel") {
return Ok(Kernel::FromSource);
}
if let Some((var, path, readable)) = &f.named {
return if *readable {
Ok(Kernel::Named {
var,
path: PathBuf::from(path),
})
} else {
Err(format!(
"REFUSED [kernel-unreadable] ${var}={path} is not readable by this user. \
/boot/vmlinuz is 0600 root:root on a stock Debian/Ubuntu box; copy the kernel \
somewhere this user can read, or build it with --build kernel"
))
};
}
if !f.exception {
return Err(format!(
"REFUSED [host-kernel-not-declared] no kernel was declared for this ISO.\n \
This verb will NOT quietly seal whatever kernel this box happens to be running \
into a medium that installs an appliance somewhere else — which is what it used to \
do, and the reason an ISO's provenance could not be read off its output.\n \
Three ways forward:\n \
--build kernel compile it from source in the pinned \
container. The only one that carries nothing off this box, and the one the law wants.\n \
KORP_KERNEL=/path/to/vmlinuz a kernel you built and can name.\n \
{HOST_KERNEL_EXCEPTION}=1 the NAMED EXCEPTION: take this box's newest \
readable /boot/vmlinuz*. The verb then prints, on every line, that the kernel was \
carried off this box.\n \
Readable on this box right now: {}",
if f.host.is_empty() {
"none — /boot/vmlinuz is root-only here and no /boot/vmlinuz-* is readable either".to_string()
} else {
f.host.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", ")
}
));
}
match f.host.last() {
Some(p) => Ok(Kernel::HostException(p.clone())),
None => Err(format!(
"REFUSED [no-readable-kernel] {HOST_KERNEL_EXCEPTION} was granted, but this box has \
no readable kernel: /boot/vmlinuz is root-only on a stock Debian/Ubuntu box and no \
/boot/vmlinuz-* is readable by this user either. Pass one explicitly with \
KORP_KERNEL=/path/to/vmlinuz, or build it with --build kernel"
)),
}
}
/// Gather the facts off this box and decide. The only impure half.
fn resolve_kernel(build: Option<&str>) -> Result<Kernel, String> {
let named = ["KORP_KERNEL", "TUNNR_KERNEL"].into_iter().find_map(|var| {
let v = std::env::var(var).ok().filter(|p| !p.is_empty())?;
let ok = readable(Path::new(&v));
Some((var, v, ok))
});
// Readable candidates, newest LAST. Sorted by name, which orders kernel
// versions correctly for the common `N.N.N-NN-flavour` shape. Plain
// `/boot/vmlinuz` (the symlink) sorts first and so is the weakest pick,
// which is right: it is the least specific name.
let mut host: Vec<PathBuf> = std::fs::read_dir("/boot")
.map(|rd| {
rd.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n == "vmlinuz" || n.starts_with("vmlinuz-"))
&& readable(p)
})
.collect()
})
.unwrap_or_default();
host.sort();
decide_kernel(&KernelFacts {
build: build.map(str::to_string),
named,
host,
exception: std::env::var(HOST_KERNEL_EXCEPTION).is_ok_and(|v| !v.is_empty() && v != "0"),
})
}
fn readable(p: &Path) -> bool {
std::fs::File::open(p).is_ok()
}
// ─────────────────────────────────────────────────────────────────────────────
// THE ed25519 ROOT OF TRUST IS A DECISION, NOT A DEFAULT
// ─────────────────────────────────────────────────────────────────────────────
//
// This is the kernel refusal above, applied to the second thing a medium is
// allowed to be missing.
//
// MEASURED: the appliance media in this estate were composed with
// `KORP_ROOT_SIGNING_KEY` UNSET. korp-installer's forge takes that arm, stages
// no `korp/root.pub` and no `korp/app.sig`, and BAKES `korp.require_signed=0`
// into the signed first stage — so every appliance installed from those media
// boots on blake3 INTEGRITY alone, with no ed25519 AUTHENTICITY anchor at all.
// blake3 proves the payload is whole. Nothing proves it is ours.
//
// The forge does say so: `⚠ NO ROOT SIGNING KEY … composing an UNENROLLED
// medium`. It is a warning, on stderr, in the middle of a compose that then
// SUCCEEDS and writes an ISO — and the failure mode of a warning in a long
// build log is the whole reason `decide_kernel` above stopped being one.
//
// **Why it matters MORE since 2026-09-21.** The owner ruled that there is one
// medium: the twin is installed from the primary's own ISO. So an unenrolled
// medium is no longer one box's problem — it is the primary AND the box that
// exists to replace the primary, off the same unanchored image.
//
// `=dev` is not the answer for this estate. `DEVELOPMENT_SEED` is 32 bytes of
// ASCII published in korp-installer's own source; anyone can produce that
// signature. It proves the enrollment PATH works, which is exactly what
// installtest wants and exactly what a medium that installs gunnar.rs must not
// be shipped with.
//
// So: the METERED lineage — the rows that install the estate that SELLS — needs
// a real seed file and refuses `dev`. Every other lineage may take `dev`, loudly.
// Unset is refused everywhere, by name, with the ways forward spelled.
/// The variable korp-installer's forge reads the operator's seed from. Named
/// here as a string rather than through `korp_installer::rootkey::SIGNING_KEY_ENV`
/// because `xtask` carries ONE dependency by design (`gunnar-recipe`, which
/// re-exports `nornir-build-thing`), and korp-installer is not in that graph.
/// `installtest` links the constant and asserts the two agree.
pub const SIGNING_KEY_ENV: &str = "KORP_ROOT_SIGNING_KEY";
/// The value that selects the PUBLISHED development seed.
pub const DEVELOPMENT_SELECTOR: &str = "dev";
/// **Where a medium's authenticity anchor comes from.**
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RootKey {
/// A seed file the operator keeps. The only shape a shipping medium takes.
Seed(PathBuf),
/// The published development seed — proves the path, anchors nothing.
Development,
/// **No anchor at all, DECLARED.** `--unenrolled <why>`.
///
/// This is the shape item 4 of the order asks for: after the root-key work,
/// composing an unanchored medium must be a deliberate, loudly-named act and
/// not what happens when a variable is unset. Before T2's refusal it was the
/// latter — `KORP_ROOT_SIGNING_KEY` unset composed an ISO with a warning in
/// the middle of a long build log. After T2's refusal it was impossible,
/// which is one notch too far: `installtest` and a community build have real
/// use for a medium composed without a key, and a rule with no door is a rule
/// people route around by editing the source.
///
/// So there is a door, it has a name, and it takes a REASON — the one thing
/// that will still exist when somebody finds the medium in six months. The
/// reason is printed, and it is refused on the lineage that installs the
/// estate that sells.
Unenrolled(String),
}
impl RootKey {
/// The line the verb prints, so a build log records WHICH of the two
/// happened rather than "a key".
pub fn describe(&self) -> String {
match self {
RootKey::Seed(p) => format!("{} (a real seed, via ${SIGNING_KEY_ENV})", p.display()),
RootKey::Development => format!(
"the PUBLISHED DEVELOPMENT seed (${SIGNING_KEY_ENV}={DEVELOPMENT_SELECTOR}) — \
anyone can produce this signature; it is NOT an authenticity anchor"
),
RootKey::Unenrolled(why) => format!(
"NONE — this medium is composed UNENROLLED on purpose (--unenrolled {why:?}). \
It stages no korp/root.pub and no korp/app.sig and BAKES \
`korp.require_signed=0`; every appliance it installs boots on blake3 \
INTEGRITY alone. The payload is proven whole and NOTHING proves it is yours"
),
}
}
}
/// Everything the decision depends on, as values, so the decision is pure.
pub struct RootKeyFacts {
/// The variant being composed — which decides whether `dev` is acceptable.
pub variant: String,
/// `$KORP_ROOT_SIGNING_KEY`, verbatim, if set and non-empty.
pub value: Option<String>,
/// Whether that value, read as a path, is readable by this user.
pub readable: bool,
/// `--unenrolled <why>`, if the operator typed it.
pub unenrolled: Option<String>,
}
/// **Does this variant install the estate that SELLS?** The metered lineage is
/// `gunnar-server-metered`'s media — `gunnar-installer-metered` and its cloud
/// twins. Matched on the substring rather than on a list, so a metered row
/// declared tomorrow is covered the day it is declared; that is the same reason
/// `is_twin_row` used to match on argv and not on a name.
#[must_use]
pub fn is_metered_medium(variant: &str) -> bool {
variant.contains("metered")
}
/// **Pick the medium's root of trust, and refuse rather than compose an
/// unanchored appliance.**
/// The shortest `--unenrolled` reason this verb will accept.
///
/// A reason of `x` is a switch wearing a reason's clothes. Short enough that no
/// honest sentence is refused, long enough that the build record carries a fact.
pub const MIN_UNENROLLED_REASON: usize = 12;
pub fn decide_root_key(f: &RootKeyFacts) -> Result<RootKey, String> {
let metered = is_metered_medium(&f.variant);
// ── THE NAMED EXCEPTION, decided BEFORE the key, because it is a different
// question. "Which key anchors this medium" and "this medium has no anchor
// and here is why" are not two answers on one axis; running them together is
// how an unset variable came to mean the second one.
if let Some(why) = f.unenrolled.as_deref().map(str::trim) {
if f.value
.as_deref()
.map(str::trim)
.is_some_and(|v| !v.is_empty())
{
return Err(format!(
"REFUSED [root-key-both-declared] --unenrolled was given AND ${SIGNING_KEY_ENV} \
is set to `{}`. One says this medium has no authenticity anchor and the other \
names the key that anchors it; they cannot both be what you meant, and \
whichever this verb picked silently would be the wrong one half the time. \
Unset the variable or drop the flag.",
f.value.as_deref().unwrap_or_default().trim()
));
}
if metered {
return Err(format!(
"REFUSED [root-key-unenrolled-on-a-metered-medium] --unenrolled composes `{}` — \
a medium of the METERED lineage, which is what installs the estate that \
SELLS.\n \
An unenrolled appliance boots on blake3 integrity alone: an attacker with ESP \
write access removes root.pub and app.sig, writes their own payload to P2 and \
its blake3 into app.blake3, and the machine boots reporting NOT-ENROLLED \
honestly on the way (the estate's own threat model, ATTACK 1, measured \
2026-08-16). There is no reason for that to be true of the boxes customers \
pay for.\n \
Mint a key — `dwarves root-key generate` — or compose a community variant.",
f.variant
));
}
if why.len() < MIN_UNENROLLED_REASON {
return Err(format!(
"REFUSED [root-key-unenrolled-without-a-reason] --unenrolled needs a REASON of \
at least {MIN_UNENROLLED_REASON} characters, not {}.\n \
The reason is the only part of this decision that still exists when somebody \
finds the medium in six months and asks why it has no anchor. A bare switch \
records that it was flipped; it never records that anybody meant it.\n \
e.g. `--unenrolled \"installtest arms the dev key itself\"`.",
why.len()
));
}
return Ok(RootKey::Unenrolled(why.to_string()));
}
let Some(value) = f.value.as_deref().map(str::trim).filter(|v| !v.is_empty()) else {
return Err(format!(
"REFUSED [root-key-not-declared] no ed25519 root signing key was declared for \
`{}`.\n \
With ${SIGNING_KEY_ENV} unset, korp-installer's forge stages NO korp/root.pub and \
NO korp/app.sig and BAKES `korp.require_signed=0` into the signed first stage. \
Every appliance installed from the medium then boots on blake3 INTEGRITY alone, \
with no AUTHENTICITY anchor: the payload is proven whole, and nothing proves it \
is yours. That is a property of the MEDIUM, read once at mastering, and since \
the one-medium ruling of 2026-09-21 the SAME medium installs the primary and the \
twin that exists to replace it.\n \
Two ways forward:\n \
{SIGNING_KEY_ENV}=/path/to/seed 32 raw bytes or 64 hex characters, kept \
wherever you keep keys and never in this repository. The only shape a shipping \
medium takes. You do not have one yet: `dwarves root-key generate` mints it, \
prints its public half and says what custody on this build box does and does \
not protect against (gunnar/.nornir/root-key-custody.md).\n \
{SIGNING_KEY_ENV}={DEVELOPMENT_SELECTOR} the PUBLISHED \
development seed. Proves the enrollment path end to end; anchors nothing, because \
anyone can produce that signature.\n \
--unenrolled \"<why>\" compose with NO anchor at all, on purpose \
and on the record. A reason is required and it is printed; refused on the \
metered lineage.{}",
f.variant,
if metered {
format!("\n `{DEVELOPMENT_SELECTOR}` is REFUSED for this variant — see below.")
} else {
String::new()
}
));
};
if value == DEVELOPMENT_SELECTOR {
return if metered {
Err(format!(
"REFUSED [root-key-development-on-a-metered-medium] \
${SIGNING_KEY_ENV}={DEVELOPMENT_SELECTOR} composes `{}` — a medium of the \
METERED lineage, which is what installs the estate that SELLS.\n \
The development seed is 32 bytes of ASCII published in korp-installer's own \
source (`korp DEVELOPMENT key NOT SECRET!`). Anyone who has read that file \
can sign a payload this medium's appliance will accept, so enrolling it is \
not an authenticity anchor — it is an anchor-shaped hole that REPORTS \
`signature-over-root` as enrolled. An absent anchor at least says \
NOT-ENROLLED.\n \
Use {SIGNING_KEY_ENV}=/path/to/seed, or compose a community variant \
(`--variant gunnar-installer-open`) if proving the path is what you wanted.",
f.variant
))
} else {
Ok(RootKey::Development)
};
}
let path = PathBuf::from(value);
if f.readable {
Ok(RootKey::Seed(path))
} else {
Err(format!(
"REFUSED [root-key-unreadable] ${SIGNING_KEY_ENV}={} is not readable by this user. \
It must be a file holding the 32-byte ed25519 seed, raw or as 64 hex characters. \
A seed this build cannot read is not a key it can fall back from: composing \
without one is a separate, DECLARED decision and is refused above by its own name.",
path.display()
))
}
}
/// Gather the facts off the environment and decide. The only impure half.
fn resolve_root_key(variant: &str, unenrolled: Option<&str>) -> Result<RootKey, String> {
let value = std::env::var(SIGNING_KEY_ENV)
.ok()
.filter(|v| !v.trim().is_empty());
let readable = value
.as_deref()
.map(str::trim)
.filter(|v| *v != DEVELOPMENT_SELECTOR)
.is_some_and(|v| readable(Path::new(v)));
decide_root_key(&RootKeyFacts {
variant: variant.to_string(),
value,
readable,
unenrolled: unenrolled.map(str::to_string),
})
}
/// Run the verb. `args` is the operator's passthrough (currently only `--help`).
/// The refusal tag `cargo xtask reimage` already uses for this exact fact
/// (`private-gunnar-ops/xtask/src/reimage.rs`, `checkout_preflight`). One
/// wording for one fact, in two verbs, so an operator who has met it once
/// recognises it.
pub const WORKTREE_TAG: &str = "gunnar-worktree";
/// **A git worktree cannot stamp the medium it composes.**
///
/// `reimage` learned this on 2026-09-15 and refuses it; `iso-installer` takes
/// the same input, hits the same wall, and until now SHIPPED — a working ISO
/// whose banner names a version and no commit. Two verbs disagreeing about one
/// fact is bad; the one that disagrees silently is the one that produces the
/// artifact people measure against, which is worse.
///
/// Why it happens, MEASURED 2026-09-16 composing `gunnar-installer-open` from
/// `/home/rickard/git/gunnar-wt-lifecycle`: a linked worktree's `.git` is a
/// FILE reading `gitdir: <main>/.git/worktrees/<name>`, an absolute path
/// OUTSIDE the tree the toolchain container mounts. Inside the container git
/// cannot resolve it, `git describe` fails in the build scripts, and
/// `GUNNAR_BUILD_REF` degrades to `unknown` — so the appliance's SSH banner
/// comes back as a bare `SSH-2.0-gunnar_1.4.0` with no commit in it.
///
/// It is NOT fixed by passing a describe into the container: the artifact still
/// would not contain a resolvable source, and a value handed in could be wrong
/// in a way nothing downstream could catch. A medium that cannot say which
/// commit it is cannot be the subject of a verdict.
pub fn worktree_refusal(checkout: &Path) -> Option<String> {
let dot_git = checkout.join(".git");
if !dot_git.is_file() {
return None;
}
let pointer = std::fs::read_to_string(&dot_git).unwrap_or_default();
Some(format!(
"{} is a git worktree ({}): the payload is built inside the toolchain container, which \
cannot see that git dir, so `git describe` fails there and the medium's banner carries no \
build reference — every verdict taken on it would name a version and no commit (MEASURED \
2026-09-16). Compose from a real clone: `git clone --branch <branch> {} <dir>` and run \
this verb there",
checkout.display(),
pointer.trim(),
checkout.display()
))
}
pub fn run(gunnar_root: &Path, args: &[String]) -> Result<bool, String> {
if args.iter().any(|a| a == "--help" || a == "-h") {
println!("{}", help(gunnar_root));
return Ok(true);
}
let ask = parse(args)?;
let read =
|p: &Path| std::fs::read_to_string(p).map_err(|e| format!("reading {}: {e}", p.display()));
if let Some(refusal) = ask
.params
.iter()
.find_map(|p| hub_conf_param_refusal(p, &read))
{
return Err(refusal);
}
// BEFORE anything is compiled: a worktree cannot stamp what it builds.
if let Some(why) = worktree_refusal(gunnar_root) {
return Err(format!("REFUSED [{WORKTREE_TAG}] {why}"));
}
let dwarves = dwarves_root(gunnar_root)?;
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
// BEFORE anything is compiled: the installer on the medium is the one the
// composer (dwarves) builds, by its own path-dep — held to the pin here, and
// named in the record afterwards (T15 TD8/TD11, `medium::preflight`).
let (built, source_verdict) = crate::medium::preflight(gunnar_root, &dwarves)?;
eprintln!(
"iso-installer: installer korp-installer {} from {} [{}] · {}",
built.version,
built
.dir
.as_ref()
.map(|d| d.display().to_string())
.unwrap_or_else(|| "the registry (dwarves' lock)".into()),
built.revision.as_deref().unwrap_or("revision unknown"),
if source_verdict == "match" {
"MATCHES the pin"
} else {
"UNPINNED — not the pin, by KORP_INSTALLER_UNPINNED"
}
);
// A `--build kernel` variant compiles its OWN kernel from source in the
// pinned container, and the recipe's symbol requirements reach it only
// through that chained build. Handing it a kernel off this box would be the
// host-library path the laws call a named exception, so it is not taken
// silently: KORP_KERNEL is set ONLY when no kernel stage was asked for.
let kernel = resolve_kernel(ask.build.as_deref())?;
// And the second thing a medium is allowed to be missing. korp-installer's
// forge only WARNS about an absent key and then writes the ISO anyway; a
// warning in a long build log is what `decide_kernel` above stopped being.
let root_key = resolve_root_key(&ask.variant, ask.unenrolled.as_deref())?;
// A front medium carries the SIGNED `gunnar` CLI its site serves under
// `/download/` (E4, 2026-10-11; `cli_stage`), built and signed from THIS
// checkout before the payload that embeds it.
if crate::cli_stage::carries_cli(&ask.variant) {
crate::cli_stage::stage(gunnar_root, &dwarves, &cargo)?;
}
eprintln!(
"iso-installer: variant `{}` through the dwarves Build Thing",
ask.variant
);
eprintln!("iso-installer: gunnar {}", gunnar_root.display());
eprintln!("iso-installer: dwarves {}", dwarves.display());
eprintln!("iso-installer: kernel {}", kernel.describe());
eprintln!("iso-installer: rootkey {}", root_key.describe());
if let RootKey::Unenrolled(why) = &root_key {
// A banner, not a line in the flow. The failure mode of a warning in a
// long build log is the whole reason `decide_kernel` stopped being one,
// and this is the fact that warning was about.
eprintln!(
"iso-installer: ────────────────────────────────────────────────────────────\n\
iso-installer: COMPOSING AN UNENROLLED MEDIUM — NO AUTHENTICITY ANCHOR\n\
iso-installer: reason: {why}\n\
iso-installer: Every appliance installed from this ISO boots on blake3\n\
iso-installer: integrity alone and reports signature-over-root=NOT-ENROLLED.\n\
iso-installer: ────────────────────────────────────────────────────────────"
);
}
let mut cmd = Command::new(&cargo);
cmd.current_dir(&dwarves);
// `cargo run` sets CARGO_MANIFEST_DIR for its child, and a grandchild
// inherits it. `ring`'s build script reads that variable, and cargo
// compares it against the SPAWNING process's environment rather than the
// per-crate one — so whichever manifest happened to launch this compose
// decides whether ring, rustls, reqwest, gix, ureq, nornir, dwarves and
// gunnar-server are rebuilt from scratch.
//
// MEASURED 2026-09-23: this variable alone cost a rebuild roughly seven
// minutes of its 44, in two places that looked like different problems —
// 171 s that another lane recorded as "total silence, cause unrecoverable"
// (it was this rebuild, emitting only warnings, so nothing touched the
// filesystem) and a 101 s tooling rebuild one rung later.
//
// Removing it is the honest fix rather than pinning a value: the payload
// graph compiles INSIDE the toolchain container, whose script exports four
// variables and not this one, so ABSENT is what that graph already records.
// Pinning a path here would agree with the host graph and keep flipping the
// other, which is a fix that looks like one.
cmd.env_remove("CARGO_MANIFEST_DIR");
// An unenrolled compose must reach korp-installer's forge with the variable
// ABSENT, whatever this process inherited. `decide_root_key` already refuses
// the two together, so this cannot silently discard a declared key — it is
// the belt on a decision that has already been made out loud.
if matches!(root_key, RootKey::Unenrolled(_)) {
cmd.env_remove(SIGNING_KEY_ENV);
}
// `--build kernel` compiles its own; handing it one off this box would be
// the host path the laws call a named exception, so KORP_KERNEL is set only
// when a kernel was actually picked.
match &kernel {
Kernel::FromSource => {}
Kernel::Named { path, .. } => {
cmd.env("KORP_KERNEL", path);
}
Kernel::HostException(path) => {
cmd.env("KORP_KERNEL", path);
}
}
cmd.args([
"run",
"-p",
"dwarves",
"--features",
"appliance-iso",
"--bin",
"dwarves",
"--",
"buildthing",
])
.arg(gunnar_root)
.args(["--variant", &ask.variant]);
for o in &ask.opts {
cmd.args(["--opt", o]);
}
for p in &ask.params {
cmd.args(["--param", p]);
}
if let Some(b) = &ask.build {
cmd.args(["--build", b]);
}
let status = cmd.status().map_err(|e| format!("spawn {cargo}: {e}"))?;
if !status.success() {
return Err(format!("the Build Thing door exited {status}"));
}
// Do not claim success on an exit code alone — check the OUTPUT.
// This verb has been bitten by exactly that: a compose that refused its
// kernel pin still left an exit code an eye could read as success.
let iso = gunnar_root.join(iso_out(&ask.variant));
match std::fs::metadata(&iso) {
Ok(m) if m.len() > 0 => {
eprintln!("iso-installer: {} ({} bytes)", iso.display(), m.len());
// What this medium CAN DO, bound to it by sha256 — read by
// deploy/upcloud to decide one upload or two. See `medium`.
let path = crate::medium::write(
gunnar_root,
&iso,
matches!(kernel, Kernel::FromSource),
&kernel.describe(),
&built,
source_verdict,
)?;
eprintln!("iso-installer: {}", path.display());
Ok(true)
}
Ok(_) => Err(format!("{} exists but is EMPTY", iso.display())),
Err(e) => Err(format!(
"the build reported success but {} is absent ({e}) — an exit code is not an artifact",
iso.display()
)),
}
}
/// The operator-facing instructions, also printed by `--help`.
fn help(root: &Path) -> String {
format!(
"cargo xtask iso-installer — build gunnar's INSTALLER ISO
WHAT IT MAKES
An installer ISO: boot it on the target and it lays gunnar onto the disk
(GPT + ESP + payload + a real FAT32 /data on P3), then boots off that disk.
Its sibling `cargo xtask iso` makes the LIVE ISO instead, which IS the
appliance and keeps /data in RAM — nothing survives a reboot there.
VARIANTS
--variant <name> default `gunnar-installer` (keyed). Also declared:
gunnar-installer-open, gunnar-installer-metered,
gunnar-installer-metered-aws, -azure.
--opt <k=v> repeatable, e.g. --opt dhcp=on --opt wireguard=off
--param <k=v> repeatable, a site value, e.g. --param clock=peer://10.13.8.99:50123
--build <stage> e.g. --build kernel, to compile the kernel from source in
the pinned container rather than carrying one off this box.
OUTPUT
{iso} (named after the variant)
REQUIREMENTS ON THE TARGET MACHINE
* UEFI firmware. A legacy-BIOS machine is refused by name.
* A target disk of at least 512 MiB; the variant declares target_disk_gib = 64.
* Optional RAID1 mirroring needs two disks of the SAME model and exact size.
REQUIREMENTS ON THE BUILD BOX
* A `dwarves` checkout beside this one (or $DWARVES_ROOT). gunnar's atoms are
declared here, but the Build Thing that reads them lives in dwarves, because
gunnar's xtask is dependency-free by law and cannot link it.
* A DECLARED kernel. This verb will not quietly seal whatever kernel this box
is running into a medium that installs an appliance somewhere else, so with
none declared it REFUSES by name. Three ways forward:
--build kernel compile it from source in the pinned container
KORP_KERNEL=/path/vmlinuz a kernel you built and can name
GUNNAR_HOST_KERNEL=1 the NAMED EXCEPTION — take this box's newest
readable /boot/vmlinuz*, and say so on every
line that mentions the kernel.
* A DECLARED ed25519 ROOT OF TRUST. With KORP_ROOT_SIGNING_KEY unset the forge
stages no root.pub/app.sig and BAKES korp.require_signed=0: the appliance
boots on blake3 INTEGRITY alone and nothing proves the payload is yours.
Since one medium installs both the primary and its twin, that is refused:
KORP_ROOT_SIGNING_KEY=/path/seed 32 raw bytes or 64 hex characters, kept
where you keep keys. The shipping shape.
KORP_ROOT_SIGNING_KEY=dev the PUBLISHED development seed. Proves
the path, anchors nothing — and is
REFUSED for a `metered` variant, which
is what installs the estate that sells.
--unenrolled '<why>' compose with NO anchor, on purpose and
on the record. The reason is required
and printed; REFUSED on `metered`, and
refused together with a declared key.
You have no key yet? `dwarves root-key generate` mints one, prints its public
half and its fingerprint, and never prints the private half. Where that key
should LIVE is the owner's call — .nornir/root-key-custody.md lays out the
four options with what each does and does not protect against.
WHAT IT RUNS (the door, if you would rather type it yourself)
cd ../dwarves && KORP_KERNEL=<kernel> \\
cargo run -p dwarves --features appliance-iso --bin dwarves -- \\
buildthing {root} --variant <name>
",
iso = root.join(iso_out(DEFAULT_VARIANT)).display(),
root = root.display(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn facts() -> KernelFacts {
KernelFacts {
build: None,
named: None,
host: Vec::new(),
exception: false,
}
}
fn boot(names: &[&str]) -> Vec<PathBuf> {
let mut v: Vec<PathBuf> = names
.iter()
.map(|n| PathBuf::from("/boot").join(n))
.collect();
v.sort();
v
}
// ── the defect ───────────────────────────────────────────────────────────
/// **THE DEFECT, pinned.** With nothing declared, this verb used to pick
/// the newest readable `/boot/vmlinuz-*` — the HOST's kernel, whatever this
/// laptop happened to be running — and seal it into a medium that installs
/// an appliance somewhere else. It now refuses BY NAME, and the refusal
/// lists the three ways forward.
#[test]
fn an_undeclared_kernel_is_refused_by_name_and_never_taken_off_this_box() {
let f = KernelFacts {
host: boot(&["vmlinuz", "vmlinuz-7.0.0-31-generic"]),
..facts()
};
let e = decide_kernel(&f).unwrap_err();
assert!(e.starts_with("REFUSED [host-kernel-not-declared]"), "{e}");
// The three ways forward are all named, or the refusal is a dead end.
assert!(e.contains("--build kernel"), "{e}");
assert!(e.contains("KORP_KERNEL="), "{e}");
assert!(e.contains(HOST_KERNEL_EXCEPTION), "{e}");
// And it SAYS what it could have taken, so the operator knows the
// exception would work before typing it.
assert!(e.contains("vmlinuz-7.0.0-31-generic"), "{e}");
}
/// The refusal is the same when `/boot` is empty — it just names that too,
/// rather than pointing at an exception that could not help.
#[test]
fn an_undeclared_kernel_on_a_box_with_no_readable_one_says_so() {
let e = decide_kernel(&facts()).unwrap_err();
assert!(e.starts_with("REFUSED [host-kernel-not-declared]"), "{e}");
assert!(e.contains("root-only here"), "{e}");
}
// ── the three ways forward ───────────────────────────────────────────────
/// `--build kernel` compiles it in the pinned container. Nothing off this
/// box enters the medium, so no exception is needed and none is granted.
#[test]
fn a_kernel_stage_compiles_its_own_and_needs_no_exception() {
let f = KernelFacts {
build: Some("kernel".into()),
host: boot(&["vmlinuz-7.0.0-31-generic"]),
..facts()
};
assert_eq!(decide_kernel(&f).unwrap(), Kernel::FromSource);
// Even with a named kernel present, the stage wins: handing a chained
// build a kernel off this box is exactly the host path it exists to avoid.
let f = KernelFacts {
named: Some(("KORP_KERNEL", "/tmp/vmlinuz".into(), true)),
..f
};
assert_eq!(decide_kernel(&f).unwrap(), Kernel::FromSource);
}
/// A stage that is NOT `kernel` is passed through to dwarves and does not
/// excuse an undeclared kernel.
#[test]
fn another_build_stage_does_not_excuse_an_undeclared_kernel() {
let f = KernelFacts {
build: Some("payload".into()),
host: boot(&["vmlinuz-7.0.0-31-generic"]),
..facts()
};
assert!(decide_kernel(&f)
.unwrap_err()
.starts_with("REFUSED [host-kernel-not-declared]"));
}
/// An explicit `$KORP_KERNEL` is a declared artifact and is taken, and the
/// variable that declared it is carried into the receipt.
#[test]
fn a_named_kernel_is_taken_and_the_variable_that_named_it_is_recorded() {
let f = KernelFacts {
named: Some(("KORP_KERNEL", "/build/vmlinuz".into(), true)),
..facts()
};
let k = decide_kernel(&f).unwrap();
assert_eq!(
k,
Kernel::Named {
var: "KORP_KERNEL",
path: PathBuf::from("/build/vmlinuz")
}
);
assert!(k.describe().contains("$KORP_KERNEL"), "{}", k.describe());
// And it wins over the host, exception or not — a declared kernel is
// never quietly replaced by this box's.
let f = KernelFacts {
host: boot(&["vmlinuz-7.0.0-31-generic"]),
exception: true,
..f
};
assert_eq!(
decide_kernel(&f).unwrap(),
Kernel::Named {
var: "KORP_KERNEL",
path: PathBuf::from("/build/vmlinuz")
}
);
}
/// A named kernel this user cannot read is refused by name rather than
/// falling back to the host — the fallback is how the wrong kernel shipped.
#[test]
fn an_unreadable_named_kernel_is_refused_and_never_falls_back() {
let f = KernelFacts {
named: Some(("KORP_KERNEL", "/boot/vmlinuz".into(), false)),
host: boot(&["vmlinuz-7.0.0-31-generic"]),
exception: true,
..facts()
};
let e = decide_kernel(&f).unwrap_err();
assert!(e.starts_with("REFUSED [kernel-unreadable]"), "{e}");
assert!(e.contains("/boot/vmlinuz"), "{e}");
}
/// **The named exception, when it is typed.** The newest candidate wins,
/// and every line that mentions the kernel says it was carried off this box.
#[test]
fn the_named_exception_takes_the_newest_and_says_so_out_loud() {
let f = KernelFacts {
host: boot(&[
"vmlinuz",
"vmlinuz-6.8.0-40-generic",
"vmlinuz-7.0.0-31-generic",
]),
exception: true,
..facts()
};
let k = decide_kernel(&f).unwrap();
assert_eq!(
k,
Kernel::HostException(PathBuf::from("/boot/vmlinuz-7.0.0-31-generic"))
);
let said = k.describe();
assert!(said.contains("CARRIED OFF THIS BOX"), "{said}");
assert!(said.contains(HOST_KERNEL_EXCEPTION), "{said}");
}
/// The exception granted on a box with nothing readable is still a refusal,
/// by its own name — not a panic and not an empty `KORP_KERNEL`.
#[test]
fn the_exception_on_a_box_with_no_kernel_is_refused_by_name() {
let f = KernelFacts {
exception: true,
..facts()
};
let e = decide_kernel(&f).unwrap_err();
assert!(e.starts_with("REFUSED [no-readable-kernel]"), "{e}");
}
/// **One medium, one site, no box** (T17): a hub_conf carrying the box's
/// `[Interface] Address` is refused before anything is built — literal or
/// `@path` — and the site-only one and the proof fixture pass.
#[test]
fn a_hub_conf_param_that_carries_a_box_address_is_refused() {
let site = "[Peer]\nPublicKey = 6yUuA2NvUdRuoHwFmS9OD1V8pFpXmDgVQjJ+YEptGm4=\nAllowedIPs = 10.99.0.0/24\nEndpoint = 10.0.2.2:51820\n";
let with_box = format!("[Interface]\nAddress = 10.99.0.7/24\n\n{site}");
let files = |p: &Path| -> Result<String, String> {
match p.to_str() {
Some("/site.conf") => Ok(site.to_string()),
Some("/box.conf") => Ok(with_box.clone()),
_ => Err("no such file".into()),
}
};
assert_eq!(hub_conf_param_refusal("hub_conf=@/site.conf", &files), None);
assert_eq!(hub_conf_param_refusal("hub_conf=@fixture", &files), None);
assert_eq!(
hub_conf_param_refusal("clock=peer://10.13.8.99:50123", &files),
None
);
for p in [
"hub_conf=@/box.conf",
"wireguard.hub_conf=@/box.conf",
"hub_conf=[Interface]\\nAddress = 10.99.0.7/24",
] {
let e =
hub_conf_param_refusal(p, &files).unwrap_or_else(|| panic!("{p} must be refused"));
assert!(
e.starts_with("REFUSED [hub-conf]") && e.contains("tunnel enrol"),
"{e}"
);
}
assert!(
hub_conf_param_refusal("hub_conf=@/missing", &files).is_some(),
"an unreadable hub_conf is refused, not skipped"
);
}
// ── the passthrough ──────────────────────────────────────────────────────
/// A silently dropped `--variant` builds the WRONG ISO and says nothing, so
/// an unknown flag is an error and every known one is kept in order.
#[test]
fn the_passthrough_is_parsed_and_a_stranger_is_refused_by_name() {
let a = parse(&[]).unwrap();
assert_eq!(a.variant, DEFAULT_VARIANT);
assert!(a.opts.is_empty() && a.params.is_empty() && a.build.is_none());
let args: Vec<String> = [
"--variant",
"gunnar-installer-metered",
"--opt",
"dhcp=on",
"--opt",
"wireguard=off",
"--param",
"clock=peer://10.13.8.99:50123",
"--build",
"kernel",
]
.iter()
.map(|s| s.to_string())
.collect();
let a = parse(&args).unwrap();
assert_eq!(a.variant, "gunnar-installer-metered");
assert_eq!(
a.opts,
vec!["dhcp=on", "wireguard=off"],
"the --opt order is the operator's"
);
assert_eq!(a.params, vec!["clock=peer://10.13.8.99:50123"]);
assert_eq!(a.build.as_deref(), Some("kernel"));
let e = parse(&["--suite".to_string(), "ORIGINAL".to_string()]).unwrap_err();
assert!(
e.contains("--suite"),
"the old vocabulary must be refused by name: {e}"
);
assert!(
e.contains("--variant"),
"and the refusal must name the word that replaced it: {e}"
);
// A flag with no value is refused rather than reading past the end.
for flag in ["--variant", "--opt", "--param", "--build"] {
let e = parse(&[flag.to_string()]).unwrap_err();
assert!(
e.contains(flag) && e.contains("needs a value"),
"{flag}: {e}"
);
}
}
/// ★ **The unsigned path is the EXCEPTION, and it is named.**
///
/// **REDDEN BY**: in [`decide_root_key`], delete the whole
/// `if let Some(why) = f.unenrolled` block.
///
/// T2 closed the hole where an UNSET variable composed an unanchored medium.
/// That left no door at all, and a rule with no door is a rule people route
/// around by editing the source — `installtest` arms the dev key on purpose
/// and a community build has real use for a keyless medium. So the door
/// exists, it has a name, and it takes a REASON: the reason is the only part
/// of the decision that still exists when somebody finds the medium in six
/// months. A bare switch records that it was flipped and never that anybody
/// meant it.
#[test]
fn composing_unenrolled_is_a_named_act_with_a_reason_on_the_record() {
let facts = |variant: &str, value: Option<&str>, unenrolled: Option<&str>| RootKeyFacts {
variant: variant.to_string(),
value: value.map(str::to_string),
readable: false,
unenrolled: unenrolled.map(str::to_string),
};
let why = "installtest arms the dev key itself";
// ── The door opens, on a lineage that may use it, and the reason is kept.
let k = decide_root_key(&facts("gunnar-installer-open", None, Some(why))).unwrap();
assert_eq!(k, RootKey::Unenrolled(why.to_string()));
// …and what the build log records is the COST, not the flag.
let said = k.describe();
assert!(said.contains("UNENROLLED on purpose"), "{said}");
assert!(said.contains("korp.require_signed=0"), "{said}");
assert!(
said.contains(why),
"the reason must reach the build log: {said}"
);
// ── A bare switch is refused: the reason has to be a reason.
for empty in ["", " ", "x", "because"] {
let e = decide_root_key(&facts("gunnar-installer-open", None, Some(empty)))
.expect_err("a switch wearing a reason's clothes was accepted");
assert!(
e.starts_with("REFUSED [root-key-unenrolled-without-a-reason]"),
"{empty:?}: {e}"
);
}
// ── REFUSED on the lineage that installs the estate that SELLS. The
// same line T2 drew for `dev`, for the same measured reason: there is no
// cause for a customer's box to boot on integrity alone.
for metered in ["gunnar-installer-metered", "gunnar-installer-metered-azure"] {
let e = decide_root_key(&facts(metered, None, Some(why)))
.expect_err("an unanchored METERED medium was composed");
assert!(
e.starts_with("REFUSED [root-key-unenrolled-on-a-metered-medium]"),
"{metered}: {e}"
);
assert!(
e.contains("dwarves root-key generate"),
"the way out must be named: {e}"
);
}
// ── A key AND the flag is refused rather than one silently winning.
// Whichever this verb picked quietly would be the wrong one half the time.
let e = decide_root_key(&facts(
"gunnar-installer-open",
Some("/keys/estate.seed"),
Some(why),
))
.expect_err("a declared key and --unenrolled were reconciled silently");
assert!(e.starts_with("REFUSED [root-key-both-declared]"), "{e}");
assert!(
e.contains("/keys/estate.seed"),
"the refusal must name what it found: {e}"
);
}
/// **REDDEN BY**: in [`parse`], delete the `"--unenrolled"` arm.
///
/// The flag has to survive the passthrough, and a flag with no value must be
/// refused rather than reading past the end of the argv.
#[test]
fn the_unenrolled_flag_survives_the_passthrough() {
let a = parse(&[]).unwrap();
assert!(a.unenrolled.is_none(), "the DEFAULT is the signed path");
let args: Vec<String> = [
"--variant",
"gunnar-installer-open",
"--unenrolled",
"proving the enrollment path",
]
.iter()
.map(|s| s.to_string())
.collect();
let a = parse(&args).unwrap();
assert_eq!(a.unenrolled.as_deref(), Some("proving the enrollment path"));
let e = parse(&["--unenrolled".to_string()]).unwrap_err();
assert!(
e.contains("--unenrolled") && e.contains("needs a value"),
"{e}"
);
}
/// **REDDEN BY**: in the unset arm of [`decide_root_key`], delete the
/// `dwarves root-key generate` sentence.
///
/// The refusal used to end at "use a seed file", and the estate had no way to
/// make one — every path out of the refusal led to a file that did not exist
/// and no verb that made it. That is the gap lane T10 was opened for, so the
/// refusal must now name the verb that closes it.
#[test]
fn the_refusal_names_the_verb_that_mints_the_key_it_asks_for() {
let e = decide_root_key(&RootKeyFacts {
variant: "gunnar-installer".to_string(),
value: None,
readable: false,
unenrolled: None,
})
.expect_err("unset must still refuse");
assert!(e.starts_with("REFUSED [root-key-not-declared]"), "{e}");
assert!(
e.contains("dwarves root-key generate"),
"a refusal that asks for a key the estate cannot mint is a dead end: {e}"
);
assert!(
e.contains("--unenrolled"),
"the named exception must be reachable from here: {e}"
);
}
/// The medium is named after the variant. Hardcoding one file name is what
/// made this verb single-variant, and a build that "succeeds" while writing
/// a file nobody looks at is the failure mode the output check exists for.
#[test]
fn the_medium_is_named_after_the_variant() {
assert_eq!(iso_out("gunnar-installer"), ".iso-out/gunnar-installer.iso");
assert_eq!(
iso_out("gunnar-installer-metered"),
".iso-out/gunnar-installer-metered.iso"
);
assert_ne!(
iso_out("gunnar-installer"),
iso_out("gunnar-installer-open")
);
}
/// The paid appliance needs the METERED variant; `-open` is the community
/// build and answers `Entitlement.Set` with Unimplemented, so a paying
/// signup gets no quota. The default is the keyed one, never `-open`.
#[test]
fn the_root_of_trust_is_declared_or_the_compose_is_refused() {
let facts = |variant: &str, value: Option<&str>, readable: bool| RootKeyFacts {
variant: variant.to_string(),
value: value.map(str::to_string),
readable,
unenrolled: None,
};
// ── THE DEFECT, pinned. The estate's media were composed like this.
let e = decide_root_key(&facts("gunnar-installer-metered", None, false)).unwrap_err();
assert!(e.starts_with("REFUSED [root-key-not-declared]"), "{e}");
// The refusal must say what the silence COST, not merely that a variable
// is unset — that is the whole difference from the forge's warning.
assert!(e.contains("korp.require_signed=0"), "{e}");
assert!(e.contains("blake3"), "{e}");
// And both ways forward, or the refusal is a dead end.
assert!(e.contains("KORP_ROOT_SIGNING_KEY=/path/to/seed"), "{e}");
assert!(e.contains("KORP_ROOT_SIGNING_KEY=dev"), "{e}");
// ── `dev` is REFUSED on the lineage that installs the estate that sells.
for metered in ["gunnar-installer-metered", "gunnar-installer-metered-aws"] {
let e = decide_root_key(&facts(metered, Some("dev"), false)).unwrap_err();
assert!(
e.starts_with("REFUSED [root-key-development-on-a-metered-medium]"),
"{metered}: {e}"
);
// It must say WHY a published key is worse than none: it reports as
// enrolled. An absent anchor at least says NOT-ENROLLED.
assert!(e.contains("NOT-ENROLLED"), "{e}");
}
// ── and taken, loudly, on a community one.
let k = decide_root_key(&facts("gunnar-installer-open", Some("dev"), false)).unwrap();
assert_eq!(k, RootKey::Development);
assert!(
k.describe().contains("NOT an authenticity anchor"),
"{}",
k.describe()
);
// ── a real seed is taken wherever it is readable, metered or not.
for v in [
"gunnar-installer",
"gunnar-installer-metered",
"gunnar-installer-open",
] {
let k = decide_root_key(&facts(v, Some("/keys/root.seed"), true)).unwrap();
assert_eq!(k, RootKey::Seed(PathBuf::from("/keys/root.seed")));
assert!(k.describe().contains("a real seed"), "{}", k.describe());
}
// ── an unreadable seed is refused by name and NEVER falls back to
// composing unenrolled: the fallback is how the estate's media happened.
let e = decide_root_key(&facts("gunnar-installer", Some("/keys/gone"), false)).unwrap_err();
assert!(e.starts_with("REFUSED [root-key-unreadable]"), "{e}");
assert!(e.contains("/keys/gone"), "{e}");
// ── an empty or whitespace value is UNSET, not a path named "".
for blank in ["", " "] {
let e = decide_root_key(&facts("gunnar-installer", Some(blank), false)).unwrap_err();
assert!(
e.starts_with("REFUSED [root-key-not-declared]"),
"{blank:?}: {e}"
);
}
// ── the lineage test matches by the word, so a metered row declared
// tomorrow is covered the day it is declared.
assert!(is_metered_medium("gunnar-installer-metered-azure"));
assert!(!is_metered_medium("gunnar-installer-open"));
assert!(!is_metered_medium(DEFAULT_VARIANT));
// The spelling must be korp-installer's own, or this gate is guarding a
// variable the forge never reads.
assert_eq!(SIGNING_KEY_ENV, "KORP_ROOT_SIGNING_KEY");
assert_eq!(DEVELOPMENT_SELECTOR, "dev");
}
#[test]
fn the_default_variant_is_not_the_community_build() {
assert_eq!(DEFAULT_VARIANT, "gunnar-installer");
assert!(!DEFAULT_VARIANT.ends_with("-open"));
}
/// A worktree is refused BY NAME, a real clone is not, and the refusal says
/// the two things an operator needs: which pointer it found, and the clone
/// that gets them out of it.
#[test]
fn a_worktree_cannot_compose_a_medium_and_the_refusal_says_why() {
let base = std::env::temp_dir().join(format!("iso-worktree-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let (wt, clone) = (base.join("wt"), base.join("clone"));
std::fs::create_dir_all(&wt).unwrap();
std::fs::create_dir_all(clone.join(".git")).unwrap();
std::fs::write(wt.join(".git"), "gitdir: /elsewhere/.git/worktrees/wt\n").unwrap();
let why = super::worktree_refusal(&wt).expect("a worktree is refused");
// The pointer it actually found, so the operator sees the evidence.
assert!(
why.contains("gitdir: /elsewhere/.git/worktrees/wt"),
"{why}"
);
// The way out, spelled as a command.
assert!(why.contains("git clone --branch"), "{why}");
// The reason, not just the rule: no commit in the banner.
assert!(
why.contains("git describe") && why.contains("no build reference"),
"{why}"
);
// A real clone composes.
assert!(
super::worktree_refusal(&clone).is_none(),
"a real clone must not be refused"
);
// And a directory with no `.git` at all is not this verb's business.
assert!(super::worktree_refusal(&base).is_none());
// The tag is the one `reimage` already uses: one wording for one fact.
assert_eq!(super::WORKTREE_TAG, "gunnar-worktree");
let _ = std::fs::remove_dir_all(&base);
}
}