Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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);
    }
}