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

nordisk/gunnar

crates/gunnar-recipe/src/recipe.rs

//! **The recipe itself** — [`GunnarBuildThing`], gunnar's implementation of
//! `nornir_build_thing::plugin::BuildThing`.
//!
//! Everything the toml's `[build]` header carried lives here: the feature space
//! gunnar `offers`, the `allow_unbuilt` and `allow_unbuilt_bins` excuses, the
//! toolchain image, the container and the ISO. The
//! `[[build.variant]]` list lives in [`crate::variants`].

use std::collections::{BTreeMap, BTreeSet};

use nornir_build_thing::cargo_container::CargoToolchain;
use nornir_build_thing::container::{ContainerBuild, ImageFormat, ImageLayer};
use nornir_build_thing::contract::Coordinate;
use nornir_build_thing::data::{Data, DataFs, DataIo};
use nornir_build_thing::installer::{InstallerBuild, MediumBoot, PayloadKind};
use nornir_build_thing::iso::{IsoBuild, IsoFormat};
use nornir_build_thing::kernel_requires::{clock_requires, hotplug_requires, KconfigReq};
use nornir_build_thing::plugin::{BuildThing, Manifest, ABI_VERSION};
use nornir_build_thing::target::{Bus, Console, Firmware, Nic, Target};
use nornir_build_thing::variant::{AllowEntry, BuildVariantSet};

use crate::variants::VARIANTS;

/// The published crate name. A coordinate's first field, and the string dwarves
/// types when there is no artifact to read a receipt out of.
pub const KRATE: &str = "gunnar-build-thing";

/// **This recipe's content hash** — sha256 over its own `src/**/*.rs`, computed
/// while it compiled. See `build.rs` for what is hashed and why `Cargo.toml` is
/// not.
///
/// `krate@version` says which recipe was *intended*. This says which bytes ran.
/// A version can be rebuilt without moving; the hash cannot.
pub const CONTENT_HASH: &str = env!("GUNNAR_BUILD_THING_CONTENT_HASH");

/// gunnar's build thing.
///
/// Zero-sized: it holds no state because a recipe has none. Everything it
/// answers is `const` data in this crate, which is what makes it safe to call
/// from a UI thread and what makes two callers on two machines get the same
/// answer.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GunnarBuildThing;

impl GunnarBuildThing {
    /// **Where this recipe lives** — the coordinate dwarves points at, and the
    /// one a receipt records.
    ///
    /// `hash` is [`CONTENT_HASH`], populated, and that is the whole point:
    /// `nornir_build_thing::receipt::audit_receipt` REFUSES a receipt whose
    /// `build_thing.hash` is missing, because the version alone says which recipe
    /// was intended rather than which one ran.
    ///
    /// This used to return `None`, on the argument that "a coordinate minted by
    /// the recipe itself cannot know the content hash of the bytes that will
    /// eventually run". That argument confuses two hashes. The receipt records
    /// the hash of the RECIPE, not of the artifact, and a recipe knows its own
    /// bytes at its own compile time — `build.rs` reads them. The artifact's
    /// hash is what the znippy attestation carries, and that one genuinely
    /// cannot exist until after the build.
    ///
    /// A coordinate with no hash is still a legal thing to hold: it is what
    /// somebody TYPES before any artifact exists, and `Coordinate::parse`
    /// accepts the two-part form for exactly that. It is not what a recipe
    /// mints about itself.
    #[must_use]
    pub fn coordinate() -> Coordinate {
        Coordinate {
            krate: KRATE.to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
            hash: Some(CONTENT_HASH.to_string()),
        }
    }
}

/// The feature space gunnar offers. Any variant feature outside this set is
/// rejected by `BuildVariantSet::validate`.
fn offers() -> BTreeSet<String> {
    // `engine-znippy` joined 2026-08-10: the storage engine stopped being an
    // undeclared crate (`crates/gunnar-znippy`) and became a feature of the
    // already-declared `gunnar-server` package (`locked_on_target.md`, the
    // `gunnar-znippy` collapse). A
    // feature outside this set is rejected for a variant, so the appliance could
    // not name its own engine until it was here.
    //
    // `engine-gix` is deliberately absent — not a shape, an A/B arm. See
    // [`allow_unbuilt`].
    //
    // `mimalloc` joined 2026-08-11 for the same reason `engine-znippy` did: the
    // appliance variant sets `no_default_features`, so a feature it must carry has
    // to be nameable here or it silently does not reach the artifact. It is a
    // shape and not an arm — every shipping gunnar that serves is built on
    // `gunnar_server::ServerAllocator`, and a build without it is 2.2x the
    // clone CPU, which is a different product rather than a different
    // measurement.
    //
    // `dhcp-client` and `wg-client` joined 2026-08-22, for the third instance
    // of the same reason: they are what `[[build.installer.options]]` decides,
    // and an option's feature must be nameable on the appliance row or the
    // toggle decides nothing. They are shapes, not arms — the two capabilities
    // an appliance customer says yes or no to when the medium is mastered.
    // `replication` joined 2026-08-30, and for the FOURTH instance of the same
    // reason: the appliance row sets `no_default_features`, and
    // replication is not in the crate's `default` list at all, so it can only
    // reach the artifact by being named. It is a SHAPE and not an arm — an
    // appliance that cannot keep a standby is a different product, which is
    // exactly the argument the durability drill makes.
    [
        "server",
        "client",
        "control",
        "systemd",
        "engine-znippy",
        "mimalloc",
        "dhcp-client",
        "wg-client",
        "replication",
        // `metered` joined 2026-09-04: the service provider's appliance
        // — a licensed cargo feature. The same shape argument as `replication`
        // — not in the crate's `default`, so it reaches an artifact only by
        // being named, and only the two `gunnar-server-metered*` rows name it.
        "metered",
        // `grow-runtime` joined 2026-09-15: the runtime data-set growth (korp-installer
        // 0.1.4 on the registry) — every server variant names it, so the appliance
        // grows with the box up. Declared HERE, because the toml is emitted from
        // this code and d17802c had edited only the toml.
        "grow-runtime",
        // `live-app` joined 2026-10-12: the app replaced live (korp-installer `live`, behind the
        // installer's log door) — every server variant that pairs (`wg-client`) names it.
        "live-app",
        // `console` joined 2026-09-25: the front medium's payload (`gunnar-front`)
        // names it — the web face (gunnar-ui-server + the built console) on the
        // front's own :443. `gunnar-front-metered` names `metered` instead, which
        // implies it.
        "console",
        // `bin` (2026-09-20, `gunnar-clock-musl`'s roll-up) LEFT 2026-10-21, lane
        // L10: the gunnar-clock crate moved to edda estate-clock and its row is gone.
        // `admit-share` joined 2026-09-28: the usage share on the admission
        // gate (`edda_admit::UsageShare`, owner-approved design). Built by
        // both metered server rows (and so by every metered installer chain);
        // the open rows stay without it.
        "admit-share",
        // `dev-narrate` joined 2026-09-30 (DEV-NARRATE, owner + coordinator):
        // the installer option `dev-narrate` (default OFF) decides it, so it is
        // `open` on the four payloads of the estate's media and named by their
        // four `…-narrate` rows — the build point an ON dev medium arrives at.
        "dev-narrate",
        // `public-site` joined 2026-10-25 (TASKS-12, the owner's SLIM RULE):
        // gunnar.rs's public face (gunnar-ui's `gunnar-public-site`, mounted by
        // the console), named by `gunnar-front-metered` and its narrate twin only.
        "public-site",
        // `forges` joined 2026-10-25 (AIRGAP, owner rule): forge import (GitHub, a
        // GitLab) left the console's default build because it dials those forges. Every
        // front row names it, and every front medium switches it on
        // (`korp.env.GUNNAR_UI_FORGES=on`), so no live or sold front changes.
        "forges",
        // `clock-fetch` joined 2026-10-25 (AIRGAP): estate-clock's HTTPS Date sources left
        // the default builds; every appliance row (server and front) names it, so a live
        // box keeps the sources its medium names.
        "clock-fetch",
        // `resolver-probe` joined 2026-10-25 (AIRGAP B3): the boot's DNS question for
        // github.com left the default builds; every appliance row names it.
        "resolver-probe",
        // `fleet-pull` joined 2026-10-25 (AIRGAP B5): the pull channel to the vendor's
        // monetize, named by the six gunnar-server rows (estate and metered) only.
        "fleet-pull",
        // `mirror-http` and `sso-egress` joined 2026-10-26 (A32-18): the mirror pull's https
        // half and the company-SSO egress (both ureq) left gunnar-server's default build;
        // the six gunnar-server rows name both.
        "mirror-http",
        "sso-egress",
        // `acme` joined 2026-10-25 (AIRGAP B1): the front's ACME client left the default
        // build; every front row names it and every front medium switches it on.
        "acme",
        // `egress-door` joined 2026-10-25 (AIRGAP B2): the front's outbound CONNECT door for
        // its boxes; every front row names it and every front medium switches it on.
        "egress-door",
        // `mail` joined 2026-10-25 (AIRGAP): the console's invite mail left its default
        // build; every front row names it (the relay's own configuration is the switch).
        "mail",
        // `sso` joined 2026-10-27 (A34-15): company sign-in left the console's default build
        // (gunnar-ui-server `sso`); every front row names it. gunnar-server's own `sso` (the
        // protocols) comes with `sso-egress`, which every server row names.
        "sso",
        // `http` joined 2026-10-28 (OX-VARIANTS): smart HTTP, the `gunnar` package's default
        // beside `engine-znippy`, named by the two crates.io rows `gunnar-open` and
        // `gunnar-open-client` (`cargo install gunnar`, with and without `client`).
        "http",
    ]
    .iter()
    .map(|s| (*s).to_string())
    .collect()
}

/// Features that exist in the workspace and that **no variant names** — with the
/// reason each one is not a gap.
///
/// An excuse with no reason is not an excuse; `nornir_build_thing::buildgate`
/// refuses an empty one. These are copied word for word from the toml so the
/// migration loses no argument.
fn allow_unbuilt() -> BTreeMap<String, String> {
    [
        (
            "fs",
            "gunnar-lfs's on-disk object store, enabled transitively by the server half; a cell cannot name another crate's feature.",
        ),
        (
            "engine-gix",
            "the second arm of the storage A/B, taken only by a dev/bench build that compiles both engines and selects at runtime; never a delivery shape, and never in the appliance.",
        ),
        (
            "conformance",
            "a test-only harness feature (each crate dev-depends on itself for it); shipping it would be the defect.",
        ),
        // ── The named extras and the service shape (lane GUN-PRODUCTS, 2026-10-30) ──
        (
            "online",
            "a named extra of the products (gunnar, gunnar-cli): opt-in at `cargo install <product> --features online`, the outbound surfaces no delivered row ships; `cargo xtask ci`'s `extras` step compiles every product with `full`, which includes it.",
        ),
        (
            "federation",
            "a named extra of the products (gunnar, gunnar-cli): the other forges, opt-in at `cargo install <product> --features federation`; compiled by `cargo xtask ci`'s `extras` step (`full`).",
        ),
        (
            "full",
            "a named extra of the products: every open extra at once, opt-in at `cargo install <product> --features full`; compiled by `cargo xtask ci`'s `extras` step, delivered by no estate row.",
        ),
        (
            "systemd",
            "gunnar-cli's systemd service shape (`cargo install gunnar-cli --features systemd`, the retired `gunnar-service` bin): compiled by `cargo xtask ci`'s `systemd-check` and `systemd-tests` steps and by installtest's subject build; no estate row ships it, the appliance has no init system.",
        ),
        // ── gunnar-crypt's and gunnar-core's (the encrypted repositories, 2026-10-06) ──
        // Library features of crates the delivered binaries reach through their OWN
        // dependency lines (a cell cannot name another crate's feature):
        (
            "cipher",
            "gunnar-crypt's envelope cipher (AES-SIV, HKDF, HMAC): enabled through `keygen`/`wrap` by gunnar-cli's `client` and gunnar-policy, and alone by the browser console in gunnar-ui; a cell cannot name another crate's feature.",
        ),
        (
            "keygen",
            "gunnar-crypt's epoch-key minting: enabled through `wrap` by gunnar-cli's `client` and gunnar-policy's dev build; a cell cannot name another crate's feature.",
        ),
        (
            "user-key",
            "gunnar-crypt's person's encryption key (CRYPT-YUBI): enabled through `wrap` by gunnar-cli's `client`, and by the browser console in gunnar-ui with `pq` (no SSH); a cell cannot name another crate's feature.",
        ),
        (
            "pq",
            "gunnar-crypt's quantum-safe key and ring (CRYPT-PQ-DEFAULT: the default level): enabled through `wrap` by gunnar-cli's `client`, and alone beside `user-key` by the browser console in gunnar-ui (no SSH); a cell cannot name another crate's feature.",
        ),
        (
            "wrap",
            "gunnar-crypt's age/HPKE key wrap: gunnar-crypt's own default, and enabled by gunnar-cli's `client` (`gunnar-crypt` with wrap + diff); a cell cannot name another crate's feature.",
        ),
        (
            "diff",
            "gunnar-crypt's plaintext diff of an encrypted file: gunnar-crypt's own default and gunnar-cli's `client`; a cell cannot name another crate's feature.",
        ),
        (
            "pq-keys",
            "gunnar-crypt's age1pq1 key decoding (bech32): enabled by gunnar-core's dependency line for its key store, and through `wrap`; a cell cannot name another crate's feature.",
        ),
        // gunnar-grpc's company SSO (2026-10-22) needs no excuse since A34-15: `sso` is a
        // feature the front rows name (gunnar-front's console sign-in), and gunnar-server's
        // `sso` comes with `sso-egress`, which every server row names.
        (
            "test-hooks",
            "gunnar-core's test-only hooks, enabled by gunnar-server's DEV dependency on gunnar-core; shipping it would be the defect.",
        ),
        // ── The 2026-09-04 pass, and it is the FIRST time anything read this
        // ── table.
        //
        // `tests/feature_axis.rs` (new that day) is gunnar's first caller of
        // `nornir_build_thing::feature_axis::FeatureAxis::manifest_gate` — the
        // shared verdict on "every feature this workspace declares is built by
        // a variant or has a written reason". Before it, this list had no
        // reader anywhere in the repository, and the gate had exactly one
        // caller in the whole constellation (znippy-zoomies). Its first run
        // found SIX rows wrong out of six-plus-four, which is what a table
        // nobody reads looks like.
        //
        // `delta` and `simd` were DELETED rather than reworded: both named
        // crates that no longer live here. `gunnar-pack` and `gunnar-delta`
        // moved to the sibling `edda` checkout, so no `Cargo.toml` in this repo
        // declares either feature, and the gate's "EXCUSE FOR A FEATURE THAT IS
        // NOT THERE" arm is exactly for that. Their dependency-side spelling
        // (`gunnar-pack = { features = ["delta"] }`) is a different thing and
        // was never what this table meant.
        //
        // The four below were the gate's "OFFERED BUT NEVER BUILT" list. None
        // is deletable — they gate 37, 5, 8 and 17 `cfg` sites respectively —
        // so each got the reason nothing names it. Two of them SHIP and are
        // invisible to the `built` column for one structural reason worth
        // stating once: a cell names features of its atom's own package, so a
        // feature reached as `<other-crate>/<feature>` from a feature that IS
        // named can never appear there. That is the same shape as `fs` above.
        (
            "porcelain",
            "gunnar-client's own default (gix + a real worktree), and it SHIPS — reached transitively as `gunnar-client/porcelain` from `gunnar-cli`'s `client` feature, which is itself offered and built. A cell names features of the atom's own package, so the transitive spelling is invisible to the `built` column.",
        ),
        (
            "webhooks",
            "outbound event delivery, and it does NOT ship in 5.4 (UW-22): `gunnar-cli`'s `server` feature names `gunnar-server/webhooks`, but no variant builds `gunnar-cli` with `server`, and no appliance row (`gunnar-server*`) names it, so no shipped box delivers events. `Webhooks.*` is called only by `gunnar-cli control`, and the console has no screen for it. Shipping it is a product decision: add it to the appliance rows and give the console a screen.",
        ),
        (
            "stages",
            "gunnar-wire's eighteen instrumentation stages (`dep:gunnar-stage`), enabled only by `bench/`'s own default — a DETACHED workspace, so no delivery variant can name it. Measurement scaffolding, never a shipped shape.",
        ),
        (
            "testmatrix",
            "gunnar-server's emit channel for nornir-testmatrix rows (`functional_status`); on only under `cargo test`, and with it off the call is an `#[inline]` no-op with the crate absent from the graph entirely. Shipping a test reporter in the appliance would be the defect.",
        ),
        // ── 2026-09-29: `gunnar-common/stderr-capture` (buyflow-observable) ──
        (
            "stderr-capture",
            "gunnar-common's fd-2 pipe whose lines are said through tracing (monetize-server's eprintln! reaches the boot log), and it SHIPS — named as `gunnar-common/stderr-capture` by `gunnar-front`'s manifest, so every front row carries it. Transitive, like `porcelain`.",
        ),
        // ── 2026-10-26: `gunnar-common/jobs` (A32-14) ─────────────────────────
        (
            "jobs",
            "gunnar-common's named jera jobs (a box's duty and the job queue on jera ChainQueue lanes), and it SHIPS — named as `gunnar-common/jobs` by `gunnar-server`'s and `gunnar-front`'s manifests, so every server and front row carries it. Transitive, like `stderr-capture`.",
        ),
        // `peer`, `fetch`, `adjust` (2026-09-14, gunnar-clock) LEFT 2026-10-21, lane L10: the
        // crate is edda estate-clock now, and Gunnar's own manifests declare none of them.
        // ── 2026-10-10: `gunnar-repair` (5.4 resilience) ─────────────────────
        //
        // The one repair crate's default build is pure (the wasm guest links it);
        // its four features are the I/O layers, and every one SHIPS: `proto` through
        // `gunnar-server`'s `replication` and `gunnar-cli`'s `control`, `fleet` (the shared
        // pump, M12) through `gunnar-server`'s `fleet-pull`: transitive, like `porcelain`.
        (
            "fleet",
            "gunnar-repair's estate side of monetize's pull channel: monetize-estate-side's SHARED pump with gunnar's executor as its PumpBox (A28 M12), and it SHIPS — named as `gunnar-repair/fleet` by `gunnar-server`'s `fleet-pull`, which every appliance row carries (the community build has `replication` without it and links no monetize crate). Transitive, like `porcelain`.",
        ),
        (
            "grpc",
            "gunnar-repair's tonic helpers (poison codec, the Bootstrap.Restore call), and it SHIPS — reached through `gunnar-repair/proto` from `gunnar-server`'s `replication` and `gunnar-cli`'s `control`. Transitive, like `porcelain`.",
        ),
        (
            "proto",
            "gunnar-repair's adapters over gunnar's own generated clients, and it SHIPS — named as `gunnar-repair/proto` by `gunnar-server` (`replication`) and `gunnar-cli` (`control`). Transitive, like `porcelain`.",
        ),
        (
            "pump",
            "gunnar-repair's async pump and verb orchestration, and it SHIPS — reached through `gunnar-repair/grpc` from `gunnar-server`'s `replication` and `gunnar-cli`'s `control`. Transitive, like `porcelain`.",
        ),
        (
            "container-robot",
            "gunnar-server's container robot TARGET (`tests/appliance_container_robot.rs`, A28 L14): a test tool, never a delivery shape. It runs as `--features container-robot -- --ignored`; the feature turns on edda's `robot` (`wg-appliance-core/robot`, published in 0.1.7), and no row builds it.",
        ),
        (
            "mcp",
            "gunnar-front's MCP connector (TASKS-13, owner 2026-10-25): OFF by default and in every release row until the owner turns it on, so no delivery row may build it (`tests/mcp_off_by_default.rs` holds the default tree free of gunnar-mcp); its own test target `mcp_bridge` requires it.",
        ),
        // ── `bin` WAS HERE, and it is GONE (2026-09-20) ──────────────────────
        //
        // Its text said: "the FRONT host's `gunnar-clock` binary … It ships to
        // the front, which is not an atom of this product: no variant row fits
        // it until a front-host atom exists. Built today as `cargo build -p
        // gunnar-clock --features bin --target x86_64-unknown-linux-musl` — a
        // host build, named here as the exception it is."
        //
        // `Atom::GunnarClock` and the `gunnar-clock-musl` variant exist now, so
        // the excuse is spent: `bin` is BUILT, by a row, in the pinned
        // container. It is deleted rather than reworded because
        // `FeatureAxis::stale` refuses an excuse for a feature a variant builds
        // — an allowlist that keeps entries after the debt is paid stops being
        // a list of debts.
    ]
    .iter()
    .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
    .collect()
}

/// **Declared `[[bin]]` targets no variant builds** — `[build.allow_unbuilt_bins]`,
/// the bin-side twin of [`allow_unbuilt`], keyed by bin name and valued with the
/// reason. Four entries, copied byte for byte from the toml.
///
/// This became LOADER VOCABULARY in nornir-build-thing 0.1.9 (2026-08-26):
/// before that the table worked through a second door (`buildgate::read_allowlist`
/// re-read the raw file) and `BuildVariantSet` had no field it could land in, so
/// the parity assertion in `tests/toml_parity.rs` could not hold it. From 0.1.9
/// it is a field, so the mirror declares it and the assertion does.
fn allow_unbuilt_bins() -> BTreeMap<String, AllowEntry> {
    [
        (
            "xtask",
            "repository tooling, never shipped; built by every `cargo xtask` run, not by a delivery.",
        ),
        (
            "gunnar-build-thing",
            "repository tooling, never shipped: the cell runner that materializes CELLS' declared argv (one implementation — owns no build knowledge); tool axis like xtask, not the delivery axis.",
        ),
        (
            "gunnar-lfs-serve",
            "an ssh-exec interop fixture for tests/interop.rs; the shipped path is gunnar-cli's GitService::LfsTransfer arm, which every atom builds.",
        ),
        (
            "gunnar-installtest",
            "the detached install-test harness (own [workspace], deps on draupnir + tunnr-vm); run by `cargo xtask install-test`, never shipped.",
        ),
        // ── `gunnar-clock` WAS HERE, and it is GONE (2026-09-20) ─────────────
        //
        // Its text said: "the FRONT host's clock daemon (`gunnar-clock
        // --features bin`), shipped to the front by hand — see the `bin` row of
        // `allow_unbuilt` for why no variant builds it yet."
        //
        // `gunnar-clock-musl` builds it. `buildgate` refuses an
        // `[build.allow_unbuilt_bins]` entry that excuses a bin which IS built,
        // by name — so this deletion is not a courtesy, it is what makes the
        // repository compile its own honesty check.
    ]
    .iter()
    .map(|(k, v)| ((*k).to_string(), AllowEntry::Reason((*v).to_string())))
    .collect()
}

/// **The pinned build image every cargo variant compiles in** — `[build.toolchain]`,
/// mirrored value for value (the toml beside each key says how each was MEASURED
/// on the image that exists).
///
/// This became LOADER VOCABULARY in nornir-build-thing 0.1.9 (2026-08-26): the
/// registry 0.1.7/0.1.8 loaders had no `cargo_container` module and no
/// `toolchain` field, so the table parsed and was DROPPED silently — a host build
/// reported as a container build. From 0.1.9 it is `BuildVariantSet::toolchain`,
/// so the mirror declares it and `tests/toml_parity.rs` holds the two equal.
fn toolchain() -> Option<CargoToolchain> {
    Some(CargoToolchain {
        image: "nordisk-cargo-toolchain".to_string(),
        // **Re-pinned 2026-09-05 (I8).** The previous digest,
        // `f29e517c1ba34b920c…`, was written on 2026-08-30 and no
        // image with it has ever existed on the build host: the only local build of
        // `nordisk-cargo-toolchain:live` is from 2026-08-27 09:39 and has
        // THIS digest, and there is no registry behind the name to pull the
        // other from (`short-name did not resolve`, `403 denied`). So the pin
        // named an image nobody could produce, and every ISO row — metered or
        // not — was refused by it. The pin's whole point (`.nornir/
        // build-variants-reasoning.md`: an image cannot be replaced under two
        // builds that report the same provenance) is restored by naming the
        // image that IS here: rustc 1.97.1 inside it, matching `rust_version`
        // below. When the toolchain is rebuilt, this line moves WITH it, in the
        // same commit, and never before.
        //
        // **Moved again 2026-09-05 ~10:30.** A `podman image prune` removed
        // `07b4e3a8…` (unused-but-tagged — guarded now) and rebuilt the
        // toolchain through the forge door: the kernel image came back
        // byte-identical, the cargo one at THIS digest (`podman images
        // --digests`, 34 minutes old at the time of the bump). Same rustc,
        // same three targets. Every medium of 1.3.0 was composed on
        // `07b4e3a8…`; anything composed after this line is on `0b8ba17c…`,
        // and a receipt says which.
        //
        // **Moved for the last time by hand, 2026-09-05 13:3x — the image is
        // REPRODUCIBLE now.** edda `ac3852e`: the cargo Containerfile makes the
        // kernel file's four moves (base pinned by digest, a snapshot.debian.org
        // instant, `ENV SOURCE_DATE_EPOCH=1783900800`, logs scrubbed in-layer)
        // and the runner mints through `build_image_reproducible`. MEASURED by
        // bevis: two fresh empty stores, `--no-cache --pull=always`, ONE digest
        // both times, 11 418 files, 0 diffs, 0 mtime drift. So this line is no
        // longer a minting event: every box that mints the recipe gets this
        // MANIFEST digest (`72abf41e…` is the image ID — both 64 hex, different
        // things; kernel.rs:917 ruled it on 08-23 and bevis re-read it off the
        // minted image at 13:4x), `0b8ba17c…` names an image the recipe no longer produces, and
        // the next move happens only when the RECIPE changes — in the same
        // commit, per the rule above.
        // **Moved 2026-09-15 with the RECIPE, per the rule above.** nornir-build-thing
        // 0.1.17's cargo-toolchain.Containerfile adds `x86_64-unknown-uefi`, because
        // korp-installer 0.1.6 builds korp-boot (EFI/BOOT/BOOTX64.EFI) and both musl
        // PID-1s in THIS image instead of host cargo. Minted reproducibly on laptop as
        // `nordisk-cargo-toolchain:live-uefi`; this is its manifest digest.
        image_sha256: "86d575e874677da3eb9aff3ba6e38ca600781ee9b7948a9df43ef89a6213c762"
            .to_string(),
        rust_version: "1.97.1".to_string(),
        // `wasm32-unknown-unknown` joined 2026-09-05. The recipe crate's
        // own wasm build — the plugin the forge loads — was refused on every
        // run with "the toolchain image does not carry std for it (it
        // declares: gnu, musl)": a refusal by DECLARATION, while the pinned
        // image (`07b4e3a8…`, above) carries all three — `rustup target list
        // --installed` inside it: wasm32-unknown-unknown, x86_64-unknown-
        // linux-gnu, x86_64-unknown-linux-musl. The declaration now says what
        // the image has, which is the only thing it is for.
        targets: vec![
            "x86_64-unknown-linux-gnu".to_string(),
            "x86_64-unknown-linux-musl".to_string(),
            "wasm32-unknown-unknown".to_string(),
            "x86_64-unknown-uefi".to_string(),
        ],
        timeout_secs: 3600,
        mem_limit_mb: 8192,
        cpus: 8,
        network: false,
        // Not spelled in the toml; the loader's default, and the value the
        // loaded set carries.
        remap_paths: true,
        // **The sibling checkouts the toolchain container must have mounted beside
        // gunnar** for a build inside it to resolve.
        //
        // It was emptied on 2026-09-09 with the note that the fourteen crates which
        // used to force it (eleven in `../edda`, two in `../znippy`, one in
        // `../monetize`) had all become crates.io versions, so the container could
        // mount nothing but gunnar — the same graph a `git clone` gets — and with a
        // standing instruction: *"if a sibling path dependency is ever added back,
        // this list must grow with it in the SAME commit."*
        //
        // It was, and the list did not. MEASURED 2026-09-10, `cargo xtask
        // iso-installer --variant gunnar-installer-open`, which never reached cargo:
        //
        //     [build.toolchain]: path dependencies escape this repo into sibling
        //     repo(s) the declaration does not list … Add `siblings = ["edda",
        //     "znippy-zoomies"]`. Escaping: edda (3 dep(s), e.g.
        //     deploy/upcloud/Cargo.toml → nornir-build-thing); znippy-zoomies
        //     (1 dep(s), e.g. bench-stock/Cargo.toml → znippy-zoomies)
        //
        // The four are `deploy/upcloud/Cargo.toml`'s `nornir-hash`,
        // `nornir-build-thing` and `nornir-git`, and `bench-stock/Cargo.toml`'s
        // `znippy-zoomies`. The declaration follows the manifests rather than the
        // other way round: this is what the tree ACTUALLY reaches for, and a
        // container that is not told fails inside itself, where the error says
        // least.
        //
        // Shrink it back the day those four become registry versions again — and
        // measure it, do not assume it: the same verb prints the escape list.
        //
        // `gunnar-ui` and `monetize` since 2026-09-25: the FRONT medium's payload
        // (`crates/gunnar-front`) links gunnar-ui-server and monetize-server by
        // path and embeds the built console (`gunnar-ui/crates/gunnar-ui-web/dist`,
        // `dist-metered`); monetize is linked BARE (its plugins arrive in the feed) —
        // the owner's ruling that the UI and monetize ride vm1's own medium.
        //
        // `Skidbladnir` since 2026-09-26: nordisk-pairing 0.5.2 moved there from
        // edda (`crates/installer/nordisk-pairing`), and gunnar-front,
        // gunnar-server and plugin-door-wire reach it by path. MEASURED on loki
        // 2026-09-26: the metered compose was refused with "Add `siblings =
        // ["Skidbladnir"]`. Escaping: Skidbladnir (4 dep(s))".
        //
        // `znippy` and `draupnir` since 2026-10-03, TEMPORARY: the release-3 merge
        // onto the dependency bump patches storage-git-gix and nornir-build-thing
        // to edda's gix-0.88 crates (root `[patch.crates-io]`), and those reach
        // `../znippy/git-storage-trait` and `../draupnir` by path. They leave with
        // that patch table, when edda publishes the crates.
        siblings: vec![
            "edda".to_string(),
            "znippy-zoomies".to_string(),
            "gunnar-ui".to_string(),
            "monetize".to_string(),
            "Skidbladnir".to_string(),
            "znippy".to_string(),
            "draupnir".to_string(),
            // `tunnr` since 2026-10-12 (live update): gunnar-server links korp-installer's
            // `live` by path (`../tunnr/crates/korp-installer`, version kept). MEASURED on
            // oden: "Add `siblings = ["tunnr"]`. Escaping: tunnr (1 dep(s))".
            "tunnr".to_string(),
        ],
    })
}

/// The appliance image the `container` column builds: gunnar on `scratch`, with
/// no shell and no package manager in it.
fn containers() -> Vec<ContainerBuild> {
    vec![ContainerBuild {
        name: "gunnar-scratch".to_string(),
        base: "scratch".to_string(),
        // `scratch` is no base of ours: no recipe builds it (edda main, 2026-10-16).
        base_recipe: None,
        layers: vec![ImageLayer {
            // The image is assembled from the `gunnar-server` variant's artifact
            // rather than from a path, which is what keeps the image and the exe
            // the same binary instead of two things that agree by convention.
            from_variant: Some("gunnar-server".to_string()),
            dest: "/gunnar".to_string(),
            executable: true,
            ..ImageLayer::default()
        }],
        entrypoint: vec!["/gunnar".to_string()],
        cmd: vec!["serve".to_string()],
        workdir: Some("/data".to_string()),
        // `GUNNAR_APPLIANCE = "1"` — the ceremony gate, declared here rather
        // than typed by an operator, the same ruling holger's
        // `holger-mvp-appliance` variant makes. The toml carries the long form.
        // Without it the `se.nordisk.role = gunnar-appliance` label below was
        // aspirational: the image minted no identity and printed no canonical
        // line.
        env: [("GUNNAR_DATA_ROOT", "/data"), ("GUNNAR_APPLIANCE", "1")]
            .iter()
            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
            .collect(),
        expose: vec![2222, 50051],
        labels: [("se.nordisk.role", "gunnar-appliance")]
            .iter()
            .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
            .collect(),
        tags: vec!["gunnar-scratch:1.1.3".to_string()],
        format: ImageFormat::Oci,
        // The distributable docker-archive export. NAMEABLE since the 0.1.7 pin
        // bump (2026-08-17): the 0.1.5 loader had no such field and dropped the
        // toml's key benignly, and the old text here recorded the decision while
        // waiting for the bump. The parity guard now compares it for real.
        archive: ".container-out/gunnar-image.tar".to_string(),
        // Image-build mode, not file mode: this variant is layered from a variant's
        // artifact, so it declares no Containerfile and no context directory.
        containerfile: String::new(),
        context: String::new(),
        // 0.1.24's build-time digest pin (`#[serde(skip)]`: never declared,
        // never emitted). Unpinned: no consumer pins the appliance image.
        expected_sha256: String::new(),
        // 0.1.10's generalised consumption edges. The appliance image consumes
        // no other build thing: it packs `gunnar-server`, which is a cargo
        // variant of THIS repo and therefore a `from_variant`, not an edge.
        // Named on purpose — see the `..ContainerBuild::default()` note below.
        edges: BTreeMap::new(),
        // The toolchain surface's closure (0.1.11, 2026-08-30). DERIVED,
        // never hand-declared: it is what a consumer's steps require of an
        // IMAGE, landed by the planner before the build. This image is a
        // delivery artifact, not a build environment — nothing consumes it — so
        // it is `None`, and naming it is this mirror keeping its rule that every
        // field added upstream gets a decision here rather than defaulting in.
        requires: None,
        // No `..ContainerBuild::default()`: every field is named. That is worth
        // keeping — a field added upstream then fails to compile here and gets a
        // decision, rather than defaulting silently into gunnar's appliance
        // image.
    }]
}

/// **The kernel the LIVE medium boots** — the mirror of
/// `[build.iso.kernel_donor]` in `.nornir/build-variants.toml`: `kernel ↔
/// tunnr/tunnr-kernel : kernel`, the SAME donor the installer declares.
///
/// A free function and not a field of [`isos`]' `IsoBuild`, for the reason the
/// installer's donor was a raw-file guard under the 0.1.7 pin: this crate pinned
/// `nornir-build-thing` 0.1.9 from the registry, whose `IsoBuild` had no
/// `kernel_donor` (the iso edge is 0.1.10, edda `61e0d53`, published
/// 2026-08-27). It stays a free function because the `[[build.iso]]` mirror is
/// not the only reader — `kernel_donor_of` and `tests/cli_matrix.rs` ask it by
/// variant name, and one value declared once is the point. The
/// value is declared ONCE here; `tests/toml_parity.rs` holds the toml's table
/// to it, and since the 0.1.10 pin it ALSO rides `IsoBuild.kernel_donor`, so the
/// whole-struct parity holds it like any other field.
///
/// Said exactly: the FORGE (dwarves, linking edda main) consumes the edge, and
/// since the bump so does the loader gunnar's own binaries link.
/// `tests/iso_edge_linked.rs` asks that loader and was RED on the 0.1.9 pin on
/// purpose. When the pin
/// advanced (0.1.10, 2026-08-27) the value now ALSO rides `IsoBuild.kernel_donor`
/// in [`isos`], and `tests/iso_edge_linked.rs` turned green by itself.
pub fn iso_kernel_donor() -> nornir_build_thing::installer::KernelDonor {
    nornir_build_thing::installer::KernelDonor {
        repo: "tunnr".to_string(),
        kernel: "tunnr-kernel".to_string(),
    }
}

/// **Which declared variant consumes which kernel** — the kernel edge by variant
/// name, from the Rust mirror (the installer's field and the live medium's
/// [`iso_kernel_donor`]), or `None` for a variant that boots no kernel (the
/// exe/container rows). The one place a gate asks "does this variant carry a
/// kernel edge?" — `tests/cli_matrix.rs`' fragment law reads it.
pub fn kernel_donor_of(variant: &str) -> Option<nornir_build_thing::installer::KernelDonor> {
    if let Some(i) = installers().into_iter().find(|i| i.name == variant) {
        return i.kernel_donor;
    }
    if isos().iter().any(|i| i.name == variant) {
        return Some(iso_kernel_donor());
    }
    None
}

/// The live appliance ISO the `iso-live` column builds.
/// **What `dhcp` needs of the donor kernel** — the DHCPv4 client is four
/// datagrams on a socket, and this is the socket, named row by row.
///
/// They were measured against `tunnr-kernel` on 2026-08-30 and then moved
/// HERE, into the option row they always belonged to. They rode a
/// temporary `[[build.installer.option_requires]]` table for exactly as long as
/// this repo's own reader was the published `nornir-build-thing 0.1.10`, whose
/// `OptionSpec` is `#[serde(deny_unknown_fields)]` and refused the whole recipe
/// on the key `kernel_requires`. The pin is `nornir-build-thing 0.1.14` from
/// crates.io now (root `Cargo.toml`) — it was a path dep on the sibling edda
/// checkout while 0.1.11 was unpublished, and 2026-09-09 turned that path into a
/// version — so the declaration lives where the feature is and gunnar's own
/// loader can see it.
///
/// Three ABSENCES are measurements, not omissions:
/// * `CONFIG_PACKET` — the client is a UDP socket, never a raw one;
/// * `CONFIG_UNIX` — nothing here opens an AF_UNIX socket (PID 1's is AF_NETLINK);
/// * `CONFIG_VIRTIO_NET` — which NIC the machine has is `target.nic`'s fact, not
///   DHCP's; turning DHCP off does not remove the NIC.
fn dhcp_kernel_requires() -> Vec<KconfigReq> {
    vec![
        KconfigReq::y(
            "CONFIG_NET",
            "the DHCPv4 handshake is four datagrams on a socket; a kernel with no network stack has no socket to give (appliance_pid1::dhcp)",
        ),
        KconfigReq::y(
            "CONFIG_INET",
            "DISCOVER/REQUEST are UDP: the client binds 0.0.0.0:68 and sends to 255.255.255.255:67 (dhcp::client_socket), and the OFFER/ACK come back broadcast because the interface has no address yet",
        ),
        KconfigReq::y(
            "CONFIG_NETDEVICES",
            "SO_BINDTODEVICE names the interface the limited broadcast leaves by — the one thing that makes 255.255.255.255 routable with no address and no route",
        ),
        KconfigReq::y(
            "CONFIG_NET_CORE",
            "the netdev core that carries that broadcast out of eth0 before there is a FIB entry to look up",
        ),
    ]
}

/// **What `wireguard` needs of the donor kernel** — the USERSPACE data plane's
/// one device, and the two symbols it hangs off.
///
/// ONE writer for both media (the live ISO's row and the installer's twin): the
/// same boringtun opens the same `/dev/net/tun` on either, so a second list
/// would be a second answer. `CONFIG_WIREGUARD` is vetoed everywhere — the
/// requirement is TUN, and with this option off nothing in the medium opens
/// that device and the kernel needs none of it (MEASURED 2026-08-30: bzImage
/// 2 388 992 → 2 364 416 B with `--opt wireguard=off`, and TUN comes back BY
/// CLOSURE rather than by a fragment line when it is on).
fn wireguard_kernel_requires() -> Vec<KconfigReq> {
    vec![
        KconfigReq::y(
            "CONFIG_TUN",
            "boringtun is a USERSPACE data plane: wg_appliance_core opens /dev/net/tun and drives the tunnel from the app, which is why CONFIG_WIREGUARD is vetoed everywhere — with this option off nothing in the medium opens that device and the kernel needs no TUN at all",
        ),
        KconfigReq::y(
            "CONFIG_NETDEVICES",
            "TUN is a netdevice — the driver lives under the NETDEVICES menu (drivers/net/Kconfig)",
        ),
        KconfigReq::y(
            "CONFIG_INET",
            "CONFIG_TUN `depends on INET` (drivers/net/Kconfig), and the tunnel's outer packets are UDP over IPv4",
        ),
    ]
}

// **The paravirt wall clock and PCI hot-plug** every medium asks of the donor kernel
// are edda's `nornir_build_thing::kernel_requires::{clock_requires, hotplug_requires}`
// since lane G10 (2026-10-21): moved there AS IS from this file (clock 50c3e69, hot-plug
// 35290f29 beside the runtime probe b247bf64), one list for every product. holger's
// installer media declared no hot-plug, and a runtime grow on such a kernel answers 511.
// The long argument for each is on the shared function.

fn isos() -> Vec<IsoBuild> {
    vec![IsoBuild {
        name: "gunnar-appliance-iso".to_string(),
        // The kernel edge, on the struct that carries it since the 0.1.10 pin
        // (2026-08-27). See `iso_kernel_donor` for why the value is ALSO a free
        // function: the mirror had to say it under the 0.1.9 loader, which had
        // no field to say it on.
        kernel_donor: Some(iso_kernel_donor()),
        // The generalised consumption edges beside `kernel_donor`. gunnar
        // declares none: its one edge IS the kernel donor above. Named, because
        // this mirror names every field on purpose — a field added upstream
        // must get a decision here, not default in silently.
        edges: BTreeMap::new(),
        from_variant: Some("gunnar-server".to_string()),
        staging: "assets/appliance-root".to_string(),
        volume_id: "GUNNAR_APPLIANCE".to_string(),
        format: IsoFormat::Uefi,
        boot_params: [
            // Order is load-bearing — `.nornir/build-variants.toml` says why:
            // tty0 first so a KVM graphics console gets the printk stream,
            // ttyS0 LAST so it stays `/dev/console`, the serial line the
            // installtest boot proof reads korp-start and gunnar off.
            "console=tty0",
            "console=ttyS0,115200",
            "panic=10",
            "GUNNAR_DATA_ROOT=/data",
            // DHCP since the PID-1 client landed; the toml carries the full
            // why. QEMU user-net grants the same 10.0.2.15 the static line
            // used to bake in, so the forwarded-port recipes are unchanged.
            "GUNNAR_NET=eth0:dhcp",
            "korp.entry=gunnar",
            "korp.znippy=/app.znippy",
            "--",
            "serve",
            // ── THIS MEDIUM TAKES THE TOFU DOOR ──────────────────────────
            //
            // It said `--no-grpc --admin-key @/data/admin.pub` until
            // 2026-08-27, and what that shipped was a brick: `--no-grpc`
            // closed Bootstrap.Enroll and the seed file was never staged,
            // because the `admin_key` parameter beside it was
            // `required = false`. MEASURED by booting the asset published on
            // release v1.1.3 — `reading the public key from /data/admin.pub:
            // No such file or directory`. The live medium's state is RAM and
            // dies with the boot, so it takes TOFU and the installer keeps
            // the sealed key; `crate::reachability` and the toml both carry
            // the long form.
            //
            // ⚠ `--grpc-bind` is NOT decoration. Its default is
            // `127.0.0.1:50051` and gunnar has no shell and no port
            // forwarding, so dropping `--no-grpc` alone ships a control plane
            // nothing off the box can reach — the same defect wearing the
            // opposite argv. Delete this pair and
            // `tests/appliance_reachable.rs` goes red over this very
            // declaration, by name.
            "--grpc-bind",
            "0.0.0.0:50051",
            // ── THE FLOOR OF A /data THAT IS RAM ─────────────────────────
            //
            // `--disk-floor`'s default is "5 % or 2 GiB, whichever is larger"
            // (gunnar-ssh `headroom.rs`, measured 2026-09-04) — right for the
            // installed disk, where 2 GiB is the survivable reserve on a 25
            // GiB volume. This medium's `/data` is a tmpfs the size of the
            // guest's RAM, and MEASURED 2026-09-05 by the installtest robot on
            // a 2 GiB guest: `/data has 947.4 MiB free, under the 2.0 GiB
            // floor … no new repository and no push` — a live appliance that
            // refuses EVERY push, on a medium whose whole point is to serve
            // one boot's worth of git out of RAM. The absolute half of the
            // floor cannot be larger than the store; 5 % of the tmpfs plus a
            // 64 MiB absolute keeps the operating reserve (the control-store
            // rewrite, an append-side compaction) without demanding a disk
            // the medium does not have. The installers say nothing here and
            // keep the default.
            "--disk-floor",
            "5%,64MiB",
        ]
        .iter()
        .map(|s| (*s).to_string())
        .collect(),
        // gunnar's mark on the LIVE medium too, not only on the installer's
        // (measured 2026-08-18 on the laptop: the live boot wore korp's raven on the
        // graphics console). Same file the installer variant bakes; the parity
        // guard holds the two variants to one mark.
        boot_logo: Some("assets/gunnar.webp".to_string()),
        // NO PARAMETERS, and the empty vec is the decision rather than an
        // omission. An `admin_key` row stood here with `required: false` and
        // `staged_file: "admin.pub"`, and it was wrong twice over: the argv
        // referenced it UNCONDITIONALLY (so "not required" was unbuildable in
        // practice), and on THIS typ `staged_file` is relative to the initramfs
        // root, so the file landed at `/admin.pub` while the argv read
        // `/data/admin.pub` — one directory, measured, `EXIT=0`, and a medium
        // that could never serve. The toml block carries all three defects in
        // full. A per-site appliance sealed to a named key is the INSTALLER's
        // job; this one is a live demonstration medium and takes TOFU.
        params: Vec::new(),
        // ── the LIVE medium's own diet (2026-08-27) ──────────────────────────
        //
        // The twin of the installer's `wireguard` row below, on the medium that IS
        // gunnar rather than the one that installs it. Same name, same feature,
        // same forbidden families; what differs is which cell it decides, which is
        // the point of an option living on a cell.
        //
        // NO `params_when_on`: this cell declares no `hub_conf` (its `/data` is RAM
        // and dies with the boot), and a conditional parameter naming a row that
        // does not exist is refused at validation.
        //
        // ⚠ It resolves to `gunnar-server-thin` (`variants.rs`), and it has to: an
        // OFF build that resolves a feature set no variant declares gets no
        // receipt and cannot say which build point it is — gunnar's own gate said
        // so out loud on the first live composition of it.
        options: vec![nornir_build_thing::options::OptionSpec {
            name: "wireguard".to_string(),
            label: "WireGuard-klient".to_string(),
            default: true,
            feature: "wg-client".to_string(),
            params_when_on: Vec::new(),
            params_when_off: Vec::new(),
            param_kinds: std::collections::BTreeMap::new(),
            forbid_symbols: vec!["wg_appliance_core".to_string(), "boringtun".to_string()],
            bakes: None,
            // The SAME rows as the installer's twin of this option, out of the
            // one writer below: the live medium runs the same boringtun over the
            // same /dev/net/tun, and two hand-written lists would be two answers
            // to one question.
            kernel_requires: wireguard_kernel_requires(),
            requires: Vec::new(),
        }],
        // The one row-level requirement this medium carries: the paravirt wall
        // clock. The LIVE medium runs the same gunnar as the installed disk and
        // reads the same boot clock, so a donor without kvmclock boots it on the
        // emulated CMOS — two hours fast on UpCloud, measured 2026-09-20. PCI
        // hot-plug is NOT here: growing the data set is the installed disk's
        // verb, and this medium's `/data` is RAM.
        requires: clock_requires(),
        ..IsoBuild::default()
    }]
}

/// The installer ISO — the shape that puts gunnar on a **disk**.
///
/// The mirror of `.nornir/build-variants.toml`'s `[[build.installer]]`, which
/// carries the long argument. In one paragraph: `gunnar-appliance-iso` boots
/// gunnar FROM the ISO onto a `/data` that is RAM, so it can never hold an
/// identity; this variant boots `korp-installer` as PID 1, which formats a real
/// FAT32 filesystem on P3 (`edda/crates/korp-installer/src/esp.rs`,
/// `format_data_partition`) and leaves an installed appliance whose data root
/// OUTLIVES the reboot. That is the precondition the identity ceremony needed
/// and the reason gunnar seeded its WireGuard key instead of minting one.
///
/// The live variant above is untouched and PARKED — beside, never instead of.
///
/// TWO rows since 2026-08-28: the SEALED installer (door A, `--no-grpc
/// --admin-key @/data/admin.pub`, `admin_key` required) and its KEYLESS twin
/// `gunnar-installer-open` (door B, `--grpc-bind 0.0.0.0:50051`, no key on the
/// medium). The twin is DERIVED from the sealed row by [`open_twin`] rather
/// than written out a second time, so the one product decision that differs
/// — the bootstrap door — is the only thing that CAN differ between them here;
/// the toml spells both in full and `tests/toml_parity.rs` holds the two
/// spellings equal field by field.
fn installers() -> Vec<InstallerBuild> {
    let sealed = sealed_installer();
    let open = open_twin(&sealed);
    let gce = scsi_twin(&open);
    let aws = nitro_twin(&open);
    let azure = hyperv_twin(&open);
    // The pairing seed rides the UpCloud pair (open + metered) — derived from
    // the open row AFTER the cloud twins, so it is declared on exactly the two
    // media the estate pairs, and the metered row inherits it from its open twin.
    let open = with_pairing_seed(open);
    // DEV-NARRATE rides the same pair (and both fronts, below): the media an
    // estate is installed from. The metered row inherits it from its open twin.
    let open = with_dev_narrate(open);
    let metered = metered_twin(&open, METERED_INSTALLER, METERED_VOLUME_ID, MONETIZE_PUBKEY);
    let metered_aws = metered_twin(
        &aws,
        METERED_AWS_INSTALLER,
        METERED_AWS_VOLUME_ID,
        MONETIZE_PUBKEY,
    );
    let metered_azure = metered_twin(
        &azure,
        METERED_AZURE_INSTALLER,
        METERED_AZURE_VOLUME_ID,
        MONETIZE_PUBKEY,
    );
    // vm1's own medium, derived from the open row WITH its pairing seed (every
    // medium of an estate reaches the installer the same way).
    let front = front_installer(&open);
    let front_metered = front_metered_installer(&front);
    vec![
        sealed,
        open,
        gce,
        aws,
        azure,
        metered,
        metered_aws,
        metered_azure,
        front,
        front_metered,
    ]
}

/// The name of the SEALED installer — the original, the one whose `admin_key`
/// is `required = true`. Named rather than left a literal so the roster in
/// `tests/appliance_reachable.rs` can be asserted by name; the other three
/// already had consts and this one did not, which is how a four-variant roster
/// came to be checked as the number 3.
pub const SEALED_INSTALLER: &str = "gunnar-installer";

/// The name of the virtio-scsi medium and of its `[[build.installer]]` row.
pub const GCE_INSTALLER: &str = "gunnar-installer-gce";
/// Its volume id — DIFFERENT from `GUNNAR_INSTALL` on purpose: the medium is a
/// different medium (a different kernel is on it), and `installtest`'s
/// `declared_installer` resolves a medium to its cell by the volume id first.
/// Two media that answer to one id are resolved by FILENAME, which is a
/// convention rather than a fact about the bytes.
pub const GCE_VOLUME_ID: &str = "GUNNAR_INSTALL_GCE";

/// **The virtio-scsi twin: the medium for a cloud whose boot disk is SCSI.**
///
/// Derived from the OPEN row for the same reason [`open_twin`] is derived from
/// the sealed one — a third hand-written row would be a third place for the
/// medium to drift. Exactly THREE things differ, and each is a consequence of
/// the machine:
///
/// 1. **`target.buses ∋ virtio-scsi`.** GCE gen1/gen2 Persistent Disk really is
///    virtio-scsi (`nornir_build_thing::target::presets::GCE_VIRTIO`,
///    `boot_disk = VirtioScsi`), so the target disk appears as `/dev/sda` and
///    the kernel needs `SCSI BLK_DEV_SD SCSI_VIRTIO`. With the shared row's
///    `buses = [ide-cdrom, virtio-blk]` the disk is not in `/sys/block` at all
///    and the appliance refuses CORRECTLY on its own console — MEASURED
///    2026-08-30: `INSTALLER: FAILED hwdetect: no usable target disk — every
///    fixed disk carries the installer medium`.
///
///    `ide-cdrom` is DROPPED, because GCE attaches no cdrom (its `MediumBring`
///    is `raw disk.raw in a tar.gz`), and carrying `ATA ATA_PIIX BLK_DEV_SR`
///    for a device the machine does not have is exactly the bloat this row
///    exists to avoid.
/// 2. **A different kernel donor** — `tunnr-kernel-scsi`. Not a choice: the
///    shared donor's `nuked` preset says `# CONFIG_SCSI is not set` BY NAME,
///    and `KernelBuild::apply_requirements` refuses a requirement over a preset
///    veto instead of merging it. A widened shared row would not build a
///    smaller kernel — it would not build AT ALL.
/// 3. **A different volume id**, so a medium can still say which cell it is.
///
/// **Why not widen the one row.** Every symbol above would then be on the
/// UpCloud appliance's kernel too, and that kernel's whole point is 2.4 MB. The
/// bus is a property of ONE cloud's disk; a shared row would make every
/// customer pay for it.
fn scsi_twin(open: &InstallerBuild) -> InstallerBuild {
    let mut gce = open.clone();
    gce.name = GCE_INSTALLER.to_string();
    gce.volume_id = GCE_VOLUME_ID.to_string();
    gce.target = Target {
        firmware: Firmware::Both,
        consoles: vec![Console::Vga, Console::Serial],
        buses: vec![Bus::VirtioBlk, Bus::VirtioScsi],
        nic: Nic::Virtio,
        ..Target::default()
    };
    gce.kernel_donor = Some(nornir_build_thing::installer::KernelDonor {
        repo: "tunnr".to_string(),
        kernel: "tunnr-kernel-scsi".to_string(),
    });
    gce
}

/// The name of the NVMe/ENA medium and of its `[[build.installer]]` row.
pub const AWS_INSTALLER: &str = "gunnar-installer-aws";
/// Its volume id — DIFFERENT again, for [`GCE_VOLUME_ID`]'s exact reason: a
/// Nitro medium carries a different kernel (the NVMe+ENA donor), so it is a
/// different medium, and `installtest`'s `declared_installer` resolves a medium
/// to its cell by the volume id first.
pub const AWS_VOLUME_ID: &str = "GUNNAR_INSTALL_AWS";

/// **The NVMe/ENA twin: the medium for AWS Nitro.**
///
/// Derived from the OPEN row for the same reason [`scsi_twin`] is — a fourth
/// hand-written row would be a fourth place for the medium to drift. Exactly
/// THREE things differ from the open row, and each is a consequence of the
/// machine, the SAME three axes the GCE twin moves:
///
/// 1. **`target.buses = [nvme]`, `target.nic = ena`.** AWS Nitro presents its
///    boot disk as an NVMe controller (`/dev/nvme0n1`) and its NIC as the
///    Elastic Network Adapter — never virtio, never IDE, no cdrom. So the disk
///    the installer finds is NVMe and the closure asks for `CONFIG_NVME_CORE
///    CONFIG_BLK_DEV_NVME CONFIG_ENA_ETHERNET`, all built-in. With the shared
///    row's `[ide-cdrom, virtio-blk]` the Nitro target disk is not in
///    `/sys/block` at all and the appliance would refuse on its own console —
///    the GCE `no usable target disk` failure, one cloud over. `ide-cdrom` is
///    dropped because Nitro attaches no cdrom (its `MediumBring` is an imported
///    disk image), and no virtio bus is carried because Nitro has no virtio
///    device.
/// 2. **A different kernel donor** — `tunnr-kernel-nvme`. Like the SCSI donor
///    it starts from `slim`, not `nuked`: `slim` vetoes neither NVMe nor ENA by
///    name, so `KernelBuild::apply_requirements` MERGES the two requirements
///    instead of refusing them.
/// 3. **A different volume id**, so a medium can still say which cell it is.
///
/// `medium_boot` stays `Hybrid` (inherited): Nitro boots from an IMPORTED disk
/// image (VM Import or import-snapshot + register-image), and a pure El Torito
/// ISO with a zeroed LBA 0 would park exactly as it did on OVH — the hybrid
/// boot sector is what makes an imported-disk cloud bootable at all.
///
/// **Why not widen the one row.** Every symbol above would then be on the
/// UpCloud appliance's kernel and on GCE's, for devices neither machine has.
/// The bus and the NIC are one cloud's facts; a shared row would make every
/// customer pay for them.
fn nitro_twin(open: &InstallerBuild) -> InstallerBuild {
    let mut aws = open.clone();
    aws.name = AWS_INSTALLER.to_string();
    aws.volume_id = AWS_VOLUME_ID.to_string();
    // Declared by PRESET since 2026-09-05 (`aws-nitro` derives the same
    // both · vga+serial · nvme · ena this row spelled by hand), for the reason
    // the Azure twin gives: the preset is what tells the composer which
    // container Nitro imports (raw / disk image), and it writes it beside the
    // ISO. The OPEN row stays spelled out on purpose: `upcloud` is SeaBIOS-only
    // by measurement, while the open medium is `both` and boots under OVMF on
    // every KVM cloud and on this box's bench rig.
    aws.target = Target {
        preset: Some("aws-nitro".to_string()),
        ..Target::default()
    };
    aws.kernel_donor = Some(nornir_build_thing::installer::KernelDonor {
        repo: "tunnr".to_string(),
        kernel: "tunnr-kernel-nvme".to_string(),
    });
    aws
}

/// The name of the Hyper-V medium and of its `[[build.installer]]` row.
pub const AZURE_INSTALLER: &str = "gunnar-installer-azure";
/// Its volume id — DIFFERENT again, for [`GCE_VOLUME_ID`]'s exact reason: a
/// Hyper-V medium carries a different kernel (the VMBus donor), so it is a
/// different medium, and `installtest`'s `declared_installer` resolves a
/// medium to its cell by the volume id first.
pub const AZURE_VOLUME_ID: &str = "GUNNAR_INSTALL_AZURE";

/// **The Hyper-V twin: the medium for Azure (Generation 2 VMs).**
///
/// Derived from the OPEN row for the same reason [`nitro_twin`] is — a fifth
/// hand-written row would be a fifth place for the medium to drift. Exactly
/// THREE things differ from the open row, the SAME three axes the GCE and
/// Nitro twins move, each a consequence of the machine
/// (`nornir_build_thing::target::presets::AZURE_GEN2`):
///
/// 1. **`target.buses = [hv-storvsc]`, `target.nic = hv-netvsc`.** Azure
///    presents every disk as Hyper-V synthetic SCSI (`hv_storvsc`) and its NIC
///    as the synthetic `hv_netvsc`, both over VMBus — never virtio, never NVMe,
///    no cdrom. So the disk the installer finds is `/dev/sda` on storvsc and
///    the closure asks for `CONFIG_HYPERVISOR_GUEST CONFIG_HYPERV
///    CONFIG_HYPERV_STORAGE CONFIG_HYPERV_NET CONFIG_SCSI CONFIG_BLK_DEV_SD`,
///    all built-in. With the shared row's `[ide-cdrom, virtio-blk]` the target
///    disk is not in `/sys/block` at all — the GCE `no usable target disk`
///    failure, one cloud over. `ide-cdrom` is dropped because Azure attaches
///    no cdrom (its `MediumBring` is an imported VHD), and no virtio bus is
///    carried because a Gen2 VM has no virtio device.
/// 2. **A different kernel donor** — `tunnr-kernel-hyperv`. Like the NVMe donor
///    it starts from `slim`, which vetoes none of the VMBus symbols by name, so
///    `KernelBuild::apply_requirements` MERGES the requirements.
/// 3. **A different volume id**, so a medium can still say which cell it is.
///
/// `firmware` is `Uefi` alone, not `Both`: a Gen2 VM has no CSM, and a Gen1
/// VM boots its OS disk off emulated IDE — a bus this `target` group spells
/// only as a cdrom — so Gen1 is not a shape this medium claims. That is also
/// why `medium_boot` is `Gpt`, not the inherited `Hybrid`: Azure boots from an
/// IMPORTED disk image (a fixed-size VHD in a storage account → managed image
/// or Compute Gallery), so the ISO must be bootable as a DISK — and `hybrid`
/// does that with a legacy boot sector the composer refuses to lay without a
/// BIOS stage (MEASURED 2026-09-05: *"`medium_boot = "hybrid"` needs a real
/// legacy-BIOS stage, but `target.firmware = uefi` lays none"*). `gpt` lays the
/// GPT ESP partition instead, which is what UEFI reads off a disk. The ISO this
/// cell composes is that image's source; `qemu-img convert -O vpc -o
/// subformat=fixed` turns it into the VHD the upload wants.
///
/// **Why not widen the one row.** Every symbol above would then be on the
/// UpCloud appliance's kernel and on GCE's and Nitro's, for a hypervisor none
/// of those machines run under.
fn hyperv_twin(open: &InstallerBuild) -> InstallerBuild {
    let mut azure = open.clone();
    azure.name = AZURE_INSTALLER.to_string();
    azure.volume_id = AZURE_VOLUME_ID.to_string();
    // Declared by PRESET, not spelled out: `azure-gen2` derives exactly the
    // shape this row carried by hand (uefi · vga+serial · hv-storvsc ·
    // hv-netvsc — nornir-build-thing's own test asserts the seven fields
    // agree), and the preset is what lets the composer write the cloud's
    // import container beside the ISO (edda `62ada54`, 2026-09-05: a cell
    // that names an imported-disk machine yields its `.vhd` by declaration;
    // seven identical fields cannot say which container a cloud takes — four
    // virtio rows split two-and-two between an attached ISO and an imported
    // qcow2, so the shape alone is not the fact).
    azure.target = Target {
        preset: Some("azure-gen2".to_string()),
        ..Target::default()
    };
    azure.kernel_donor = Some(nornir_build_thing::installer::KernelDonor {
        repo: "tunnr".to_string(),
        kernel: "tunnr-kernel-hyperv".to_string(),
    });
    azure.medium_boot = MediumBoot::Gpt;
    azure
}

/// The name of the METERED medium and of its `[[build.installer]]` row.
pub const METERED_INSTALLER: &str = "gunnar-installer-metered";
/// Its volume id — DIFFERENT again, for [`GCE_VOLUME_ID`]'s exact reason. This
/// medium carries a DIFFERENT BINARY (`gunnar-server-metered`, whose symbol
/// table holds the entitlement code the community row has none of), so it is a
/// different medium, and `installtest`'s `declared_installer` resolves a medium
/// to its cell by the volume id first.
pub const METERED_VOLUME_ID: &str = "GUNNAR_INSTALL_METERED";

/// **monetize-server's Ed25519 verifying key**, the 32 public bytes every
/// `Entitlement.Set` on a metered appliance is checked against.
///
/// Baked into the medium rather than staged onto `/data`, because staging is
/// not reachable: `korp-installer` writes staged files only under
/// `korp.reinstall=wipe`, and the ESP is read-only once the box is running. So
/// on the default keep-upgrade the kernel command line is the ONLY channel into
/// a booting gunnar, which is why `--monetize-pubkey` had to learn the literal
/// spelling (`gunnar_common::monetize_key`) before this row could exist at all.
///
/// A VERIFYING key is public by construction — it is what monetize hands out so
/// that facts it signed can be checked. Putting it in a medium and on a cmdline
/// discloses nothing. The SIGNING key never leaves monetize-server, and nothing
/// in gunnar can accept one.
pub const MONETIZE_PUBKEY: &str =
    "1c3663248d60900e2996389fba4efa1d7179d6dbd08627d0426f16f94e44c4e6";

/// **Where a lapsed tenant renews.** Baked beside the key for the same reason
/// the key is baked at all: on a keep-upgrade the kernel command line is the
/// ONLY channel into a booting gunnar, and `--billing-url` is REQUIRED whenever
/// `--monetize-pubkey` is given.
///
/// **MEASURED, 2026-09-08, and it cost an outage.** `gunnar-server` used to
/// fall back to a hardcoded `https://gunnar.rs/billing` when told a key and no
/// URL. That default was removed — correctly: the OPEN product must not ship
/// one operator's commercial URL, and a metered appliance with no renew link
/// refuses its tenants into a dead end. But this row was not updated with it,
/// so the next medium carried a key and no URL, and gunnar — which is PID 1 —
/// refused to start:
///
/// ```text
/// gunnar: --monetize-pubkey was given, but --billing-url was not, and this
/// build has no default for it: it would have to invent the page your lapsed
/// tenants are sent to.
/// Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000100
/// ```
///
/// The refusal is right and stays. What was missing is this line. The pairing
/// is asserted by `metered_env_pairs_the_key_with_a_renew_url`, so a medium can
/// never again be composed with one half of it.
///
/// **A placeholder on a running estate** (UW-6): the medium is one for every
/// estate, so the installer's door feeds each site its own page
/// (`refusal_link` = `https://<site>/billing/{tenant}`, gunnar-server
/// `feed::FEED_BILLING_URL`), which replaces this one live.
pub const METERED_BILLING_URL: &str = "https://www.gunnar.rs/billing/{tenant}";

/// The environment variable `gunnar-server` reads the monetize verifying key
/// from (`gunnar_common::cli`, `env = "GUNNAR_MONETIZE_PUBKEY"`).
pub const MONETIZE_PUBKEY_ENV: &str = "GUNNAR_MONETIZE_PUBKEY";
/// The environment variable it reads the renew URL from (`env =
/// "GUNNAR_BILLING_URL"`, `requires = "monetize_pubkey"`).
pub const BILLING_URL_ENV: &str = "GUNNAR_BILLING_URL";

/// The `--param` name a composing door supplies the front's live key under.
pub const MONETIZE_PUBKEY_PARAM: &str = "monetize_pubkey";

/// The environment variable `gunnar-server-metered` reads `--monetize-vital` from.
pub const MONETIZE_VITAL_ENV: &str = "GUNNAR_MONETIZE_VITAL";
/// The `--param` a composing door sets it through (`true`|`false`).
pub const MONETIZE_VITAL_PARAM: &str = "monetize_vital";
/// Its default on every metered medium: owner ruling 2026-09-22, monetize is vital.
pub const MONETIZE_VITAL_DEFAULT: &str = "true";

/// The environment variable `gunnar-server` (with `admit-share`) reads
/// `--admit-limit` from.
pub const ADMIT_LIMIT_ENV: &str = "GUNNAR_ADMIT_LIMIT";
/// The `--param` a composing door sets it through (`100GB`, `none`, bytes).
pub const ADMIT_LIMIT_PARAM: &str = "admit_limit";
/// **Its default on every metered medium: `none` — PassThrough.** A medium
/// composed without the parameter admits exactly as a gate without the usage
/// share; a budget is always an estate's explicit decision
/// (`private-gunnar-ops` `estate.toml` `[admit]`).
pub const ADMIT_LIMIT_DEFAULT: &str = "none";
/// The environment variable `--admit-window-days` is read from.
pub const ADMIT_WINDOW_ENV: &str = "GUNNAR_ADMIT_WINDOW_DAYS";
/// The `--param` it is set through.
pub const ADMIT_WINDOW_PARAM: &str = "admit_window_days";

/// **The metered medium's monetize declaration, as a function of the key —
/// the ONE writer of both halves.**
///
/// ⚠ **Why an `env` pair and not two more words on `korp.args`, which is where
/// this lived until 2026-09-20.** The key WAS a constant spelled into the argv,
/// and a constant is the defect: `monetize-server` mints a fresh Ed25519
/// signing key on every freshly laid front, so the baked key went stale the
/// moment a new estate existed and the only way out was a human editing a
/// tracked declaration before every bring-up. `private-gunnar-ops`' own
/// `monetize-pubkey-mismatch` preflight refused the bring-up over exactly that,
/// correctly.
///
/// Making it an INPUT means a value the composing door supplies has to REPLACE
/// the declared default, and on `nornir-build-thing 0.1.17` there is exactly
/// one field with that property. Measured, in the order it was tried:
///
/// * a `[[build.installer.params]]` row landing on the cmdline **appends**
///   (`InstallerBuild::apply_cli_selection_with` → `boot_params.extend`), and
///   `korp.args` must stay EXACTLY ONE token
///   (`installer::declared_cmd_renders_as_one_korp_args_token`). Two tokens is
///   not an override, it is a medium with two argvs.
/// * appending a duplicate does not reach the installed disk either:
///   `korp_installer::install::carry_site_tokens` carries the **first** token
///   per key (`!carried.iter().any(|c| c.starts_with("{k}="))`), so the
///   leftover default would win on the box even though the installer's own
///   `env_pairs` is last-wins. The two readings disagree; that is a defect in
///   edda and it is reported, but nothing here may depend on it.
/// * `[[build.installer.options]].bakes` DOES template a value into a token and
///   DOES replace by key — but a bake hangs off an `OptionSpec`, and
///   `OptionSpec::validate` refuses one that names no cargo `feature`. There is
///   no honest feature here, and the long note on [`metered_twin`] already
///   ruled out spelling `metered` as a toggle.
/// * `InstallerBuild::env`, however, is rendered by `effective_boot_params`
///   with a PER-KEY idempotence rule — `korp.env.<K>=` is pushed only when no
///   token with that key is present, and landed parameters are already in
///   `boot_params` by then. So a supplied `--param monetize_pubkey=<hex>`
///   lands `korp.env.GUNNAR_MONETIZE_PUBKEY=<hex>` and the declared default is
///   **not emitted at all**: one token, the operator's, on every channel that
///   reads it. That is the override, and it needs nothing new anywhere.
///
/// **Every reason the key is on the kernel command line survives this.**
/// `korp.env.*` IS the command line: `korp-installer` writes staged files only
/// under `korp.reinstall=wipe` and the ESP is read-only once the box runs, so
/// on the default keep-upgrade the cmdline is still the ONLY channel into a
/// booting gunnar. What changed is which token on it carries the value.
///
/// **And the renew URL is still MANDATORY, now by construction rather than by
/// vigilance.** This function is the only thing that writes either half and it
/// writes both, so [`METERED_BILLING_URL`] rides beside ANY key — the supplied
/// one included, because a parameter can only ever REPLACE the key's token
/// (`apply_params_with` refuses an empty value) and can never remove the URL's.
/// The 2026-09-08 outage — a medium carrying `--monetize-pubkey` and no
/// `--billing-url`, gunnar refusing, gunnar being PID 1, `Kernel panic - not
/// syncing: Attempted to kill init!` — is unreachable from here.
///
/// Why the argv is the poorer channel for the same pair, measured against
/// clap: a value on the argv WINS over the same argument's `env`, so an argv
/// that spells the key cannot be overridden by anything the medium is composed
/// with. The pair moving together to `env` is what makes the override possible
/// at all.
#[must_use]
pub fn metered_env(monetize_pubkey: &str) -> std::collections::BTreeMap<String, String> {
    [
        (MONETIZE_PUBKEY_ENV.to_string(), monetize_pubkey.to_string()),
        (BILLING_URL_ENV.to_string(), METERED_BILLING_URL.to_string()),
        (
            MONETIZE_VITAL_ENV.to_string(),
            MONETIZE_VITAL_DEFAULT.to_string(),
        ),
        // The usage share's budget: `none` unless the estate says otherwise,
        // replaced per key by `--param admit_limit=…` (see `admit_limit_param`).
        (ADMIT_LIMIT_ENV.to_string(), ADMIT_LIMIT_DEFAULT.to_string()),
    ]
    .into_iter()
    .collect()
}

/// **The declared setting the front's live key arrives through.**
///
/// A `[[build.installer.params]]` row, which is how this model spells a
/// per-site value: the CLI's `--param monetize_pubkey=<hex>`, the ⚒ pane's
/// input row and the forge's field are all this one declaration, and a door
/// that supplies a name no row declares is refused BY NAME
/// (`params::apply_params_with`). So `private-gunnar-ops` forwards a value it
/// was handed and spells no key of its own.
///
/// `required: false` on purpose: [`MONETIZE_PUBKEY`] stays the DEFAULT for a
/// build that names none (a self-hoster, a KVM proof, the bench), and a named
/// key wins over it. `Text`, which is 0.1.7 vocabulary, and the value is a
/// single token by construction — 64 hex characters.
///
/// Not a secret and `names_a_secret` agrees: a VERIFYING key is what monetize
/// hands out so that facts it signed can be checked, and the SIGNING key never
/// leaves monetize-server.
#[must_use]
pub fn monetize_pubkey_param() -> nornir_build_thing::params::ParamSpec {
    nornir_build_thing::params::ParamSpec {
        lands: nornir_build_thing::params::Landing::Data,
        name: MONETIZE_PUBKEY_PARAM.to_string(),
        label: "monetize-server's Ed25519 verifying key, 64 hex characters (the front's \
                MONETIZE_SIGNING_PUBKEY)"
            .to_string(),
        kind: nornir_build_thing::params::ParamKind::Text,
        required: false,
        boot_param: format!("korp.env.{MONETIZE_PUBKEY_ENV}"),
        staged_file: String::new(),
    }
}

/// **`monetize_vital`, the owner's ruling of 2026-09-22, as a medium parameter.** `true` (the
/// declared default, [`metered_env`]): a box whose stored entitlement facts do not verify at boot
/// raises `entitlement.key_mismatch` and reports `maintenance` until monetize pushes one that
/// does; `false` only logs. The box starts and serves either way. Replaces the default's token by
/// the same per-key rule as [`monetize_pubkey_param`].
#[must_use]
pub fn monetize_vital_param() -> nornir_build_thing::params::ParamSpec {
    nornir_build_thing::params::ParamSpec {
        lands: nornir_build_thing::params::Landing::Data,
        name: MONETIZE_VITAL_PARAM.to_string(),
        label: "is monetize vital: true raises entitlement.key_mismatch and maintenance when stored facts do not verify at boot, false only logs (default true)".to_string(),
        kind: nornir_build_thing::params::ParamKind::Text,
        required: false,
        boot_param: format!("korp.env.{MONETIZE_VITAL_ENV}"),
        staged_file: String::new(),
    }
}

/// **`admit_limit`: the usage share's budget as a medium parameter** (owner
/// 2026-09-28). `100GB`, `5 GiB`, a byte count, or `none` (PassThrough — the
/// declared default, [`ADMIT_LIMIT_DEFAULT`]). Lands
/// `korp.env.GUNNAR_ADMIT_LIMIT`, replacing the default's token by the same
/// per-key rule as [`monetize_pubkey_param`]. Metered media only: the usage
/// share is in the metered binary alone.
#[must_use]
pub fn admit_limit_param() -> nornir_build_thing::params::ParamSpec {
    nornir_build_thing::params::ParamSpec {
        lands: nornir_build_thing::params::Landing::Data,
        name: ADMIT_LIMIT_PARAM.to_string(),
        label: "the usage share's transfer budget per window for a namespace: a size (100GB, 5GiB, bytes) or none = PassThrough (default none)".to_string(),
        kind: nornir_build_thing::params::ParamKind::Text,
        required: false,
        boot_param: format!("korp.env.{ADMIT_LIMIT_ENV}"),
        staged_file: String::new(),
    }
}

/// **`admit_window_days`: the usage share's rolling window**, in UTC days
/// (the server's default, 30, when absent). Lands
/// `korp.env.GUNNAR_ADMIT_WINDOW_DAYS`; no declared default token, so the
/// server's own default is the only one.
#[must_use]
pub fn admit_window_param() -> nornir_build_thing::params::ParamSpec {
    nornir_build_thing::params::ParamSpec {
        lands: nornir_build_thing::params::Landing::Data,
        name: ADMIT_WINDOW_PARAM.to_string(),
        label: "the usage share's rolling window in UTC days (default 30)".to_string(),
        kind: nornir_build_thing::params::ParamKind::Text,
        required: false,
        boot_param: format!("korp.env.{ADMIT_WINDOW_ENV}"),
        staged_file: String::new(),
    }
}

/// **The METERED twin: the same machine and the same door, billing the plan.**
///
/// Derived from the OPEN row for the reason [`scsi_twin`] and [`nitro_twin`]
/// are — a fifth hand-written row would be a fifth place for the medium to
/// drift. Exactly THREE things differ, and each is a consequence of the
/// BINARY rather than of the machine:
///
/// 1. **`variant = "gunnar-server-metered"`.** The community `gunnar-server`
///    contains no entitlement code at all, so a medium built from it serves
///    every namespace Free no matter what it is told. This was the whole gap:
///    `gunnar-server-metered` had existed since the entitlement work and NO
///    installer row named it, so no ISO could carry it.
/// 2. **The monetize pair on the cmdline**, [`metered_env`] — the verifying
///    key and the renew URL as `korp.env.*` tokens, plus the
///    [`monetize_pubkey_param`] row a composing door replaces the key through.
///    Spelled inline and never as a path: a path would name a file nothing was
///    allowed to create (see [`MONETIZE_PUBKEY`]), which is why every earlier
///    attempt at a metered appliance needed `korp.reinstall=wipe`.
/// 3. **A different volume id**, so a medium can still say which cell it is.
///
/// Everything else is inherited on purpose. Same kernel donor, same target,
/// same options, same parameters: metering is a property of the BINARY, not of
/// the machine, so a metered box is an UpCloud box that bills.
///
/// **Including `target.preset`, which the UpCloud row leaves `None`.** That is
/// the row the composer names when it asks for a machine, and the reason it
/// cannot answer `upcloud` yet is measured on the row it inherits from — see
/// the long `target:` comment in [`sealed_installer`] and
/// `tests/machine_named.rs`, which fails the day the block lifts. Inheriting
/// the silence rather than naming a machine here keeps the rule this function
/// exists for: only `name`, `variant`, `volume_id` and the key on the argv are
/// the twin's, and the MACHINE is always the row's it twins.
///
/// **Three twins since 2026-09-05**, one per cloud row, each derived from the
/// keyless row for THAT machine: [`METERED_INSTALLER`] of the open row
/// (UpCloud), [`METERED_AWS_INSTALLER`] of [`nitro_twin`]'s row and
/// [`METERED_AZURE_INSTALLER`] of [`hyperv_twin`]'s. The function takes the
/// row it twins rather than always the open one, so the AWS twin inherits
/// `aws-nitro` + `tunnr-kernel-nvme` and the Azure twin `azure-gen2` +
/// `tunnr-kernel-hyperv` + `medium_boot = gpt` — the machine's facts — from
/// the row that already spells them, and the composer writes each cloud's
/// import container (`.raw` / `.vhd`) beside the ISO from the inherited
/// preset exactly as it does for the open row. Only `name` and `volume_id`
/// are the caller's; `tests/toml_parity.rs` holds each twin to its open row
/// field by field, so nothing else CAN differ.
///
/// **Why not an option on the open row.** `metered` is a cargo feature, so it
/// selects a different variant and therefore a different artifact; an
/// `[[build.installer.options]]` toggle chooses features WITHIN one variant's
/// build and cannot swap the variant the medium is composed from.
fn metered_twin(
    open: &InstallerBuild,
    name: &str,
    volume_id: &str,
    monetize_pubkey: &str,
) -> InstallerBuild {
    let mut metered = open.clone();
    metered.name = name.to_string();
    metered.variant = METERED_VARIANT.to_string();
    metered.volume_id = volume_id.to_string();
    // The argv is the open row's, byte for byte. What makes this medium the
    // metered one is the BINARY it packs and the monetize pair below — and the
    // pair lives in `env` rather than on the argv so a composing door can
    // replace the key. See `metered_env`.
    metered.env = metered_env(monetize_pubkey);
    metered.params.push(monetize_pubkey_param());
    metered.params.push(monetize_vital_param());
    metered.params.push(admit_limit_param());
    metered.params.push(admit_window_param());
    metered
}

/// The `--param` name the estate's pairing seed arrives under (the pairing
/// contract, 2026-09-24: fixed, do not rename).
pub const PAIRING_SEED_PARAM: &str = "pairing_seed";
/// Where it lands, relative to `/data` on the installed box: the medium's
/// data-stage file `korp/data-stage/pairing/seed.toml`, copied by the first
/// boot to `/data/pairing/seed.toml` (`nordisk_pairing::seed::Seed` TOML).
pub const PAIRING_SEED_STAGED_FILE: &str = "pairing/seed.toml";

/// **The pairing seed — one per medium, nothing per box** (edda
/// `nordisk-pairing::seed`). `--param pairing_seed=@<seed.toml on the composing
/// host>` stages the file for `/data/pairing/seed.toml`; the PID-1 pairing loop
/// reads it there.
///
/// A STAGED FILE and never a `boot_param`: the seed holds the installer CA and
/// the bootstrap client certificate AND ITS KEY, multi-line PEM — the cmdline
/// is whitespace-split and readable to anyone who sees the boot. On the medium
/// the key is only as secret as the medium (owner 2026-09-24: bootstrap-only,
/// it may only join a slot that has not joined and expires with the installer
/// CA); on the installed box korp-installer lands it `0600` and mounts the ESP
/// copy owner-only (edda `agent/pairing-seed-stage`).
///
/// `required: false`: a medium composed without it composes exactly as before
/// and boots unpaired. The name says nothing secret, so `names_a_secret` lets
/// it through — the value's secrecy is the medium's, by design.
#[must_use]
pub fn pairing_seed_param() -> nornir_build_thing::params::ParamSpec {
    nornir_build_thing::params::ParamSpec {
        lands: nornir_build_thing::params::Landing::Data,
        name: PAIRING_SEED_PARAM.to_string(),
        label: "Pairing seed (nordisk-pairing seed.toml: estate, front, installer CA, bootstrap cert+key)"
            .to_string(),
        kind: nornir_build_thing::params::ParamKind::Text,
        required: false,
        boot_param: String::new(),
        staged_file: PAIRING_SEED_STAGED_FILE.to_string(),
    }
}

/// `row` plus the [`pairing_seed_param`], appended.
fn with_pairing_seed(mut row: InstallerBuild) -> InstallerBuild {
    row.params.push(pairing_seed_param());
    row
}

/// The `--opt` name of the DEV-NARRATE option.
pub const DEV_NARRATE_OPTION: &str = "dev-narrate";
/// The cargo feature it decides, on gunnar-server and gunnar-front alike.
pub const DEV_NARRATE_FEATURE: &str = "dev-narrate";
/// The symbol family that must be ABSENT from every OFF (release) payload:
/// Skidbladnir's `nordisk-narrate` crate, sender and all.
pub const DEV_NARRATE_FORBIDDEN: &str = "nordisk_narrate";

/// **DEV-NARRATE** (owner + coordinator 2026-09-30): the box's boot log, sealed
/// to the installer's key (the seed's `installer_pubkey`), over udp/51822 from
/// the moment the NIC has an address — before the installer's WireGuard link.
///
/// **Default OFF**, the one option here that is an opt-IN: a medium says
/// nothing ⇒ the payload carries no sender at all, and the audit reads
/// [`DEV_NARRATE_FORBIDDEN`] back out of the packed binary's symbols to prove
/// it. ON is the kit's dev medium only (`estate install --dev-narrate`, the
/// mock and a test instance; refused for the original estate), and resolves to
/// the payload's own `…-narrate` row. No kernel requirement beyond the
/// network the medium already needs (UDP/IPv4).
#[must_use]
pub fn dev_narrate_option() -> nornir_build_thing::options::OptionSpec {
    nornir_build_thing::options::OptionSpec {
        name: DEV_NARRATE_OPTION.to_string(),
        label: "DEV-NARRATE (sealed early-boot log to the installer, udp/51822) — dev media only"
            .to_string(),
        default: false,
        feature: DEV_NARRATE_FEATURE.to_string(),
        params_when_on: Vec::new(),
        params_when_off: Vec::new(),
        param_kinds: std::collections::BTreeMap::new(),
        forbid_symbols: vec![DEV_NARRATE_FORBIDDEN.to_string()],
        bakes: None,
        kernel_requires: Vec::new(),
        requires: Vec::new(),
    }
}

/// `row` plus the [`dev_narrate_option`], appended.
fn with_dev_narrate(mut row: InstallerBuild) -> InstallerBuild {
    row.options.push(dev_narrate_option());
    row
}

// ─────────────────────────────────────────────────────────────────────────────
//  THE FRONT MEDIUM — vm1 boots its OWN ISO
// ─────────────────────────────────────────────────────────────────────────────
//
// Owner, 2026-09-25, verbatim: "twin/main gunnar share ISO, but NOT the
// ui/monetize!! BUILD an iso for them using build thing!!!". So an estate has
// TWO media: main and twin boot the gunnar medium (`gunnar-installer-open` /
// `-metered`, payload `gunnar-server`, byte for byte the same for both), and
// vm1 boots THIS one, whose payload is `gunnar-front`. Lane A had put the
// front's role into gunnar-server on the gunnar medium (54fa7bd); it lives in
// `gunnar-front` now and gunnar-server refuses a feed of role `front`.

/// The name of the FRONT medium and of its `[[build.installer]]` row.
pub const FRONT_INSTALLER: &str = "gunnar-front-installer-open";
/// Its volume id — its own, for [`GCE_VOLUME_ID`]'s reason: a different payload
/// is a different medium, and a medium is resolved to its row by volume id.
pub const FRONT_VOLUME_ID: &str = "GUNNAR_FRONT_INSTALL";
/// The cargo variant the front medium lays down.
pub const FRONT_PAYLOAD_VARIANT: &str = "gunnar-front";
/// What korp-start execs on the installed front.
pub const FRONT_ENTRY: &str = "korp.entry=gunnar-front";
/// **The front's forge-import switch** (AIRGAP, owner rule 2026-10-25): gunnar-ui-server
/// dials no forge unless `GUNNAR_UI_FORGES=on`, and every front medium (open and metered)
/// sets it, so a live or sold front keeps forge import as before. A front built without
/// `forges` ignores it.
pub const FRONT_FORGES_SWITCH: &str = "korp.env.GUNNAR_UI_FORGES=on";
/// **The front's ACME switch** (AIRGAP B1, owner 2026-10-25): gunnar-front asks no ACME
/// directory unless it is built with `acme` and `GUNNAR_FRONT_ACME=on`; every front medium
/// sets it, so a live or sold front still gets its certificate.
pub const FRONT_ACME_SWITCH: &str = "korp.env.GUNNAR_FRONT_ACME=on";
/// **The front's egress-door switch** (AIRGAP B2): the boxes behind a front with no public
/// IPv4 leave through it; every front medium sets it.
pub const FRONT_EGRESS_SWITCH: &str = "korp.env.GUNNAR_FRONT_EGRESS=on";

/// **The front medium, derived from the open row** — the same donor kernel,
/// target, boot logo, disk, payload shape and pairing seed, so vm1 reaches the
/// installer exactly as main and twin do. What differs, and why:
///
/// * `variant` = [`FRONT_PAYLOAD_VARIANT`], `korp.entry=gunnar-front`: the
///   payload is the front's program, not the git server;
/// * no `korp.args=` (gunnar-front takes none) and no `GUNNAR_APPLIANCE=1`
///   (the appliance gate is the git server's identity ceremony);
/// * the params: `hub_conf` (the pre-0.3 hub declaration; the front IS the
///   hub now), `clock_key` (the front is the root of time, it pins no peer)
///   and `static_ip`/`gateway` (conditional on the dhcp option, which this row
///   does not carry) are dropped. `clock` stays and means the front's OWN
///   sources; `pairing_seed`, `log_udp`, `reinstall`, `install_then` stay;
/// * no options: DHCP on both NICs and the WireGuard link are not choices on
///   the front (the installer's link is WireGuard always, edda 0.4.0). The
///   options' kernel requirements (NET/INET/NETDEVICES/TUN…) move onto the
///   row, so the kernel closure is the gunnar medium's.
fn front_installer(open: &InstallerBuild) -> InstallerBuild {
    use nornir_build_thing::requires::{EdgeRequirement, Requirement};
    let mut front = open.clone();
    front.name = FRONT_INSTALLER.to_string();
    front.variant = FRONT_PAYLOAD_VARIANT.to_string();
    front.volume_id = FRONT_VOLUME_ID.to_string();
    front.boot_params = open
        .boot_params
        .iter()
        .filter(|p| !p.starts_with("korp.args=") && p.as_str() != "korp.env.GUNNAR_APPLIANCE=1")
        .map(|p| {
            if p.starts_with("korp.entry=") {
                FRONT_ENTRY.to_string()
            } else {
                p.clone()
            }
        })
        .collect();
    let entry = front
        .boot_params
        .iter()
        .position(|p| p == FRONT_ENTRY)
        .unwrap_or(front.boot_params.len());
    front
        .boot_params
        .insert(entry, FRONT_FORGES_SWITCH.to_string());
    front
        .boot_params
        .insert(entry + 1, FRONT_ACME_SWITCH.to_string());
    front
        .boot_params
        .insert(entry + 2, FRONT_EGRESS_SWITCH.to_string());
    front
        .params
        .retain(|p| !["hub_conf", "clock_key", "static_ip", "gateway"].contains(&p.name.as_str()));
    for p in &mut front.params {
        if p.name == "clock" {
            p.label = "The front's own clock sources (https://… or ntp://…, comma-separated): the estate's root of time".to_string();
        }
    }
    front.fixtures.clear();
    let mut requires = open.requires.clone();
    for k in open.options.iter().flat_map(|o| o.kernel_requires.iter()) {
        let known = requires
            .iter()
            .any(|r| matches!(&r.what, Requirement::Kconfig(have) if have.symbol == k.symbol));
        if !known {
            requires.push(EdgeRequirement {
                edge: "kernel".to_string(),
                what: Requirement::Kconfig(k.clone()),
            });
        }
    }
    front.options.clear();
    // The one option the front DOES carry: DEV-NARRATE (default off), so vm1's
    // early boot reaches the installer on a dev medium like main's and twin's.
    front.options.push(dev_narrate_option());
    front.requires = requires;
    front
}

/// The name of the METERED front medium and of its `[[build.installer]]` row: vm1 of a
/// SELLING site (`gunnar-twin-one`: gunnar.rs, test2.gunnar.rs). The same medium as
/// [`FRONT_INSTALLER`] with the payload built `metered`: the console's selling half
/// (gunnar-ui-server `metered`), which sells THROUGH the ONE box with the estate bearer
/// the installer feeds. It embeds no monetize (ONE monetize, owner 2026-10-10 13:35Z):
/// no monetize-server, no shop, in any build.
///
/// **The rule** (owner 2026-10-11): OPEN = no money code at all (the self-hoster's build;
/// the open build does not CONTAIN it); METERED = sells, via a ONE box only. A site's
/// front and boxes are open together or metered together.
pub const FRONT_METERED_INSTALLER: &str = "gunnar-front-installer-metered";
/// Its volume id, its own: a different binary is a different medium.
pub const FRONT_METERED_VOLUME_ID: &str = "GUNNAR_FRONT_INSTALL_METERED";
/// The cargo variant the paid front medium lays down.
pub const FRONT_METERED_PAYLOAD_VARIANT: &str = "gunnar-front-metered";

/// **The paid front medium** — [`front_installer`] with the metered payload.
/// Nothing else differs: no monetize key parameter (the front MINTS monetize's
/// key and offers it; the boxes take it from the offer), the same entry, seed,
/// kernel and machine.
fn front_metered_installer(front: &InstallerBuild) -> InstallerBuild {
    let mut m = front.clone();
    m.name = FRONT_METERED_INSTALLER.to_string();
    m.variant = FRONT_METERED_PAYLOAD_VARIANT.to_string();
    m.volume_id = FRONT_METERED_VOLUME_ID.to_string();
    // RA1-14: the CLI hole's reach is NOT the medium's: every sold estate's front is laid from
    // this medium too. gunnar.rs's front is fed `front_cli_hole = public` by the ONE box (its
    // site declaration's `cli_hole_public`); every other front keeps `own-network`.
    m
}

/// The cargo variant the metered medium composes — the build point that
/// actually links `monetize-embed` and the entitlement gate.
pub const METERED_VARIANT: &str = "gunnar-server-metered";

/// The name of the metered AWS Nitro medium and of its `[[build.installer]]`
/// row — [`metered_twin`] of [`AWS_INSTALLER`].
pub const METERED_AWS_INSTALLER: &str = "gunnar-installer-metered-aws";
/// Its volume id, its own for [`GCE_VOLUME_ID`]'s reason: a different binary
/// on a different kernel is a different medium, and `installtest` resolves a
/// medium to its cell by the volume id first.
pub const METERED_AWS_VOLUME_ID: &str = "GUNNAR_INSTALL_METERED_AWS";
/// The name of the metered Azure Gen2 medium and of its `[[build.installer]]`
/// row — [`metered_twin`] of [`AZURE_INSTALLER`].
pub const METERED_AZURE_INSTALLER: &str = "gunnar-installer-metered-azure";
/// Its volume id — see [`METERED_AWS_VOLUME_ID`].
pub const METERED_AZURE_VOLUME_ID: &str = "GUNNAR_INSTALL_METERED_AZURE";

/// The `korp.args=` token of the sealed installer — door A.
pub const SEALED_KORP_ARGS: &str = "korp.args=serve,--no-grpc,--admin-key,@/data/admin.pub";
/// **The private interface the control plane binds on.**
///
/// `eth1` and not an address, because there is no address to write down: the
/// utility NIC's address is DHCP-learned (UpCloud gave THIS appliance
/// `203.0.113.152`; the next box gets another, and so does this one after a
/// re-lease). A medium is mastered once and installed many times, so the only
/// value it can honestly carry is the one that is the same on every box — the
/// interface. `gunnar_common::bindspec::BindSpec` resolves it at boot.
///
/// `eth1` is the second NIC in PCI order, which on UpCloud is the utility
/// interface (public first, utility second — measured on the live appliance:
/// index 1 public `198.51.100.55`, index 2 utility `203.0.113.152`).
pub const PRIVATE_INTERFACE: &str = "eth1";

/// The `korp.args=` token of the keyless installer — door B.
///
/// `--grpc-bind` is load-bearing twice over. Without it the listener sits on
/// `127.0.0.1:50051`, which on a box with no shell and no port forwarding is
/// nobody's door (`reachability::binds_control_plane_offbox`). With
/// `0.0.0.0:50051` — what it said until 2026-08-31 — the listener is on the
/// PUBLIC interface too, and this variant declares no `--admin-key`, so
/// `Bootstrap.Enroll` stands open to the whole internet until the first
/// successful enrolment. Whoever reaches it first becomes gunnar-admin. That
/// window is the reason to name an interface, and it is enough of one.
///
/// ⚠ **This doc comment used to give a second reason that is FALSE, and the
/// correction matters more than the line it replaces.** It said v1 gRPC
/// authentication is off by decision and that `repo list`, `user list` and
/// `repo create` had answered an uncredentialled caller over the open
/// internet on 2026-08-31. RE-MEASURED against the same live appliance
/// (`198.51.100.55:50051`, `v1.1.4`, bootstrap closed) on 2026-08-31 with a
/// raw gRPC request carrying no metadata at all: `Repos/List`,
/// `Repos/ListNamespaces`, `Access/ListPrincipals` and `Admin/Status` every one
/// answered `grpc-status: 16` (UNAUTHENTICATED) with a zero-byte body, and a
/// freshly minted, unregistered ssh key was refused with "the credential did not
/// verify against any registered key". The gate was holding.
///
/// What produced the original reading: `gunnar control` signs with
/// `~/.ssh/id_ed25519` when `-i` is not given (`gunnar-cli/src/control.rs`,
/// `candidate_identities`), and the probing host's default key is enrolled on
/// that box as gunnar-admin. The probe was authenticated as that enrolled
/// principal and read as anonymous.
/// `AUTHENTICATION_ENABLED` is `true`, `ServerConfig::default().auth` is
/// `AuthMode::Required`, and all nine non-Bootstrap services are mounted
/// `with_interceptor` — see `gunnar-grpc/tests/wire.rs`, which asserts that over
/// a real socket.
///
/// Naming the interface is what makes the third option — neither loopback nor
/// everywhere — expressible at all.
///
/// **By ROLE since the lean estate** (owner 2026-10-23, A19 row 17): `utility:50051`, not
/// `eth1:50051`. Every box boots this one medium, and a box laid behind its front has its
/// utility NIC alone, as `eth0`; gunnar-server makes the role the box's NIC at boot (the last
/// NIC [`CLOUD_GUNNAR_NET`] declares that the box has), and from there it is the interface
/// spelling as ever.
pub const OPEN_KORP_ARGS: &str = "korp.args=serve,--grpc-bind,utility:50051";

/// `korp.env.GUNNAR_NET` for a cloud appliance: BOTH interfaces by DHCP.
///
/// One entry is what every medium said before, and it left the utility NIC
/// dark — measured 2026-08-31, `203.0.113.152` answered no ping from a host on
/// the same /22 because nothing had ever configured it. The first entry owns
/// the default route; `eth1` gets an address and no route, which is right for
/// a network that does not reach the internet.
pub const CLOUD_GUNNAR_NET: &str = "eth0:dhcp,eth1:dhcp";
/// The name of the keyless installer variant and of its `[[build.installer]]` row.
pub const OPEN_INSTALLER: &str = "gunnar-installer-open";

// ─────────────────────────────────────────────────────────────────────────────
//  THE ROLE IS NOT THE MEDIUM'S — ONE MEDIUM, THE ROLE OVER THE WIRE
// ─────────────────────────────────────────────────────────────────────────────
//
// Until 2026-09-17 there was a ninth `[[build.installer]]` row, `gunnar-twin`,
// and a ninth pointer variant beside it. It was `open_twin`'s row cloned with
// four lines changed: the volume id, `--role twin` welded into `korp.args`, the
// two `ZNIPPY_*` env tokens and `korp.data.disks=vdb`. Same payload kind, same
// kernel donor, same target disk, same fixtures, same options — and, being a
// clone of the KEYLESS row, `variant = "gunnar-server"`.
//
// **That last word is the defect this deletion exists to close.** gunnar.rs's
// appliance is `gunnar-installer-metered`, whose `variant` is
// `gunnar-server-metered` — the build point that links `monetize-embed` and the
// entitlement gate. Its twin was mastered from `gunnar-twin`, so the twin ran
// `gunnar-server`, and `tests/metered_symbols.rs` GUARANTEES that binary
// contains no entitlement symbol at all. The twin exists to BECOME the primary:
// promoted after a restore, gunnar.rs would have been served by a binary with no
// plan enforcement in it, every suspended tenant live again, and nothing on the
// box able to say why — because nothing on the box would have been wrong. The
// medium was. It could only be found at a takeover, which is the one moment
// nobody is reading a symbol table.
//
// A twin is not a different product. It is the SAME appliance told what it is,
// and the cmdline already carries that kind of fact: the growbench's `SX` rung
// planted `korp.growbench.probe=sx` in the ESP's `korp/cmdline` and korp-boot
// merged it into what the kernel saw, and an install already takes `reinstall`
// and `install.then` as parameters. So the role is one more parameter, and the
// medium that installs a twin is the medium that installs its primary — by
// construction, not by a promise. `tests/twin_is_the_same_medium.rs` holds it.
//
// **Superseded 2026-09-22:** the role is not even a parameter any more — see
// the note below `open_twin`'s ruling. A box is told it is a twin over gRPC
// (`Bootstrap.BecomeTwin`), never on a cmdline.
//
// ## Does the role select a BINARY, or does one binary serve both roles?
//
// **One binary serves both roles, and the role parameter selects nothing at
// link time.** `--role twin` is already a runtime argv switch on the same
// `gunnar-server` (`gunnar_common::cli::ServeRole`): there is no `twin` feature,
// no twin symbol family, nothing a twin links that a primary does not. The axis
// that DOES select a binary is metering, and it stays exactly where it was — on
// the medium's `variant` (`gunnar-server` vs `gunnar-server-metered`), which is
// what `metered_symbols` reads off the artifact.
//
// So "one medium" means one medium PER LINEAGE, not one medium in the world:
// the community row installs a community primary or a community twin, and the
// metered row installs a metered primary or a metered twin. The community build
// gains nothing — it links no `monetize_embed` before this change and none
// after, the open build still never mentions money, and `metered_symbols` is
// untouched and still the proof.
//
// The alternative — one medium carrying BOTH binaries, the role parameter
// picking which to install — is rejected, and it is worth writing down why: it
// would put the money code on the community medium. "Contains none of the
// entitlement families" is a guarantee about what is IN the artifact, and an
// artifact that ships the metered binary beside the community one contains it
// however carefully the installer declines to lay it down. That guarantee is
// load-bearing and is not widened here.
//
// The cost of the choice made instead: an operator mastering a twin must pick
// the same lineage they picked for the primary. That is the same single choice
// they already make, one row rather than two, and picking WRONG is no longer
// expressible as a build — there is no twin row to pick.

// **What a box IS is not the medium's to say** (owner, 2026-09-21/22). From
// 2026-09-17 to 2026-09-22 this file declared a `role` parameter
// (`korp.env.GUNNAR_ROLE=twin`), a `data_disks` parameter (`korp.data.disks=`,
// measured necessary on gunnar.rs's 10 GB + 20 GB twin, `No bootable device`),
// `InstallRole` and `realize` to plant them. All of it is gone:
//
// * the ROLE is conferred over the wire — `cargo xtask elect twin --apply`
//   (private-gunnar-ops) records which box is the weakest and calls
//   `Bootstrap.BecomeTwin` on it; nothing on any cmdline says "twin";
// * the DATA DISKS are korp-installer's own election (≥ 0.1.15: the system on
//   the disk the firmware boots, `/data` on the rest), so no machine's shape is
//   baked into — or handed to — the one medium;
// * the twin's memory bound was a cmdline token list and is code since
//   znippy-plugin-git 0.1.5 (`Engine::hold_twin_memory_bound`, from
//   `ServerStores::mark_twin`) — the measurements are kept there and in
//   `gunnar-server/tests/twin_snapshot_memory.rs`.
//
// `tests/twin_is_the_same_medium.rs` pins that no installer row declares
// either parameter and that no row's cmdline carries either token.

/// **The keyless twin of a sealed installer row.** Same payload, same donor,
/// same options and fixtures; the argv takes door B and the `admin_key`
/// parameter is GONE (not `required = false` — an optional key nothing reads is
/// the v1.1.3 defect in a new place). Everything else is a clone, on purpose:
/// a second hand-written row would be a second place for the medium to drift.
fn open_twin(sealed: &InstallerBuild) -> InstallerBuild {
    let mut open = sealed.clone();
    open.name = OPEN_INSTALLER.to_string();
    open.boot_params = sealed
        .boot_params
        .iter()
        .map(|p| {
            if p == SEALED_KORP_ARGS {
                OPEN_KORP_ARGS.to_string()
            } else {
                p.clone()
            }
        })
        .collect();
    open.params.retain(|p| p.name != "admin_key");
    open
}

fn sealed_installer() -> InstallerBuild {
    InstallerBuild {
        name: "gunnar-installer".to_string(),
        // The same atom the live ISO chains, so the installed disk carries the
        // binary the regression baseline already boots — one artifact, two
        // delivery shapes.
        variant: "gunnar-server".to_string(),
        // EMPTY, exactly as korp's variant leaves it: the PID-1 that installs is
        // `korp-installer`, which lives in `edda` and is `publish = false`.
        // gunnar cannot declare a cargo variant for a binary in another
        // repository, and a variant that NAMED one would fail
        // `validate_installer`. The day korp-installer is reachable, this gets
        // its name — in both files at once.
        installer_variant: String::new(),
        volume_id: "GUNNAR_INSTALL".to_string(),
        // gunnar's app `.znippy` rides the installer ISO itself: airgap and
        // offline, the shape the appliance is sold in.
        payload: PayloadKind::EmbeddedIso,
        // No extra payload entries: gunnar's app is the whole archive (edda 0.2.6
        // added the table for a product that stages more, Holger's scan sandbox).
        payload_entries: std::collections::BTreeMap::new(),
        // LEAN — korp PID-1 only, zero container 3pp. gunnar IS the workload;
        // there is nothing for a container runtime to run.
        container_backend: None,
        target_disk_gib: 64,
        gpu: false,
        boot_params: [
            // Order is load-bearing, same as the live variant: tty0 first so a KVM
            // graphics console gets the printk stream, ttyS0 LAST so it stays
            // /dev/console — the serial line the installer's `INSTALLER:` lines
            // and gunnar's identity line are read off.
            "console=tty0",
            "console=ttyS0,115200",
            "panic=10",
            // `korp.env.<KEY>` / `korp.args=`, NEVER the bare forms. MEASURED
            // 2026-08-15 (`installtest/tests/installer_drive.rs`, first drive
            // of this variant): `korp_installer::install::carry_site_tokens`
            // forwards a token from the INSTALLER's cmdline to the INSTALLED
            // one only when its key starts with `korp.`, so bare
            // `GUNNAR_APPLIANCE=1` and a `-- serve --no-grpc` tail died with
            // the installer's own boot and the appliance came up ungated,
            // exec'ing a bare `gunnar`. The toml variant records the whole story;
            // this mirror must say the same bytes or `tests/toml_parity.rs`
            // goes red — which is exactly what it is for.
            //
            // On this shape the data root is P3, a real filesystem the
            // installer formatted — not the RAM the live ISO's /data is.
            "korp.env.GUNNAR_DATA_ROOT=/data",
            // BOTH NICs. See `CLOUD_GUNNAR_NET`: the utility interface an
            // installed appliance is reached on privately does not exist until
            // something brings it up, and until 2026-08-31 nothing did.
            // The same string the `dhcp` option BAKES (`CLOUD_GUNNAR_NET`).
            // The bake is what actually reaches the medium; this row is what a
            // reader of the declaration sees, and the two disagreeing is how a
            // wrong value shipped while the right one was written down.
            "korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp",
            // THE GATE. Everything above is configuration; this line is what
            // turns gunnar into an appliance with an identity of its own
            // (`gunnar_server::appliance::GATE_ENV`). korp-start decodes it
            // into the exec'd app's environment on the installed disk. The
            // PRIVATE key is not here and never can be — /proc/cmdline is
            // world readable; the ceremony mints it onto P3 at 0600 and
            // prints only the public half.
            "korp.env.GUNNAR_APPLIANCE=1",
            // The app's argv tail, comma-joined; korp-start splits it after
            // argv[0]. Without it PID 1 execs a bare `gunnar`, which prints
            // its help and exits — "boots but does not serve".
            // The bootstrap admin — see `crate::reachability` and the long
            // paragraph on this token in `.nornir/build-variants.toml`. Without
            // it this installer lays down a git server nobody can ever enrol
            // against (measured under KVM 2026-08-25); WITH `--no-grpc` dropped
            // instead, Bootstrap.Enroll would be open to any caller with no
            // credential on a medium mailed to a customer.
            SEALED_KORP_ARGS,
            "korp.entry=gunnar",
            "korp.znippy=/app.znippy",
        ]
        .iter()
        .map(|s| (*s).to_string())
        .collect(),
        // The 0.1.7 spellings (`env = {…}` / `cmd = [...]`) render exactly the
        // `korp.env.*` / `korp.args=` tokens `boot_params` above carries — the
        // toml keeps the proven boot_params spelling for now and both files say
        // so in the same breath, so these stay empty rather than declaring the
        // same tokens twice through two channels.
        cmd: Vec::new(),
        env: std::collections::BTreeMap::new(),
        // The ONE per-site param: the ceremony's hub.conf (public data), landing
        // on the medium and — through the composition — on P3 as wg/hub.conf.
        // The toml also declares a FIXTURE for it (`[build.installer.fixtures]
        // hub_conf = "wg-hub"`, supplied as `--param hub_conf=@fixture`); that
        // table is not expressible under this crate's 0.1.7 pin (the field does
        // not exist there) and the 0.1.7 loader drops it benignly, so the parity
        // compare cannot see it — `installtest/tests/installer_drive.rs` is what
        // holds the fixture honest. `applied_params` is the ATTESTATION side
        // (what a mastering run filled in), never a declaration.
        //
        // `static_ip` and `gateway` joined 2026-08-22 as the DHCP option's
        // `params_when_off`. The OPTION rows that make them conditional are in
        // the toml and NOT here, for the same reason the fixture is not: 0.1.7
        // has no `options` field, `InstallerBuild` is not `deny_unknown_fields`,
        // so the loader drops the block benignly and a mirror that declared it
        // would compare against something the loader never produced. The PARAMS
        // are 0.1.7 vocabulary, so these three DO compare — and the order is
        // load-bearing, `params` being a Vec.
        //
        // `kind: Text` on all three: `ParamKind::Ipv4`/`Ipv4Cidr` do not exist
        // in 0.1.7 either. The sharp shapes are declared in the option's
        // `param_kinds` table, which no reader in this workspace parses.
        params: vec![
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "hub_conf".to_string(),
                label: "Hub declaration (the ceremony hub.conf, public data)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: false,
                boot_param: String::new(),
                staged_file: "hub.conf".to_string(),
            },
            // THE one parameter this variant cannot be built without, and the only
            // required one that is not conditional on an option. A STAGED FILE
            // and not a `boot_param`: an OpenSSH public key line contains
            // spaces, and `korp.env.*` rides a whitespace-split kernel cmdline
            // with no escapes — a boot_param landing would truncate the key at
            // "ssh-ed25519", which `InstallerBuild::validate`'s own encoding law
            // refuses by name. It is not a secret (a public key is published to
            // every host its holder reaches), and `names_a_secret` agrees.
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "admin_key".to_string(),
                label: "Bootstrap-admin (kundens OpenSSH-publika nyckel)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: true,
                boot_param: String::new(),
                staged_file: "admin.pub".to_string(),
            },
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "static_ip".to_string(),
                label: "Statisk adress med prefix (t.ex. 10.0.2.15/24)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: true,
                boot_param: "korp.env.GUNNAR_IP".to_string(),
                staged_file: String::new(),
            },
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "gateway".to_string(),
                label: "Standard-gateway (t.ex. 10.0.2.2)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: true,
                boot_param: "korp.env.GUNNAR_GATEWAY".to_string(),
                staged_file: String::new(),
            },
            // The debug channel for a console-less VM (UpCloud 2026-08-29: no
            // UART, VGA only, flaky VNC, appliance went dark after the kernel).
            // `--param log_udp=<ipv4>:<port>` → `korp.log_udp=` on the cmdline;
            // korp-start spools its narration, `pid1::ship_boot_narration` ships
            // it + dmesg the moment the lease arrives. Optional and inert when
            // absent. `Endpoint` IS 0.1.7 vocabulary, unlike Ipv4/Ipv4Cidr.
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "log_udp".to_string(),
                label: "Boot-narration UDP sink for a console-less VM (debug; a.b.c.d:port)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Endpoint,
                required: false,
                boot_param: "korp.log_udp".to_string(),
                staged_file: String::new(),
            },
            // **The clock** (2026-09-14, `gunnar-clock`). Where "now" comes from is
            // a SITE fact, so it is a parameter and never baked: an airgapped
            // customer's appliance must not dial anybody by default, and
            // gunnar.rs names its front — `peer://10.13.8.99:50123`, a signed
            // exchange over the utility network. Unset = not configured = the
            // report says so. `Text`, not `Endpoint`: the value is a source LIST
            // (`peer://…`, `https://…`), parsed and refused by name in
            // `estate_clock::config`.
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "clock".to_string(),
                label: "Clock sources (peer://ip:port for the front's signed time; https:// fallback, comma-separated)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: false,
                boot_param: "korp.env.GUNNAR_CLOCK".to_string(),
                staged_file: String::new(),
            },
            // The peer's ed25519 PUBLIC key, as the bare base64 blob of its
            // OpenSSH line — the `ssh-ed25519 ` prefix has a space and a boot
            // param must stay one token. Public data; the private half never
            // leaves the front (`/etc/gunnar-clock/time_ed25519`).
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "clock_key".to_string(),
                label: "Clock peer's ed25519 public key (base64 blob of the OpenSSH line)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: false,
                boot_param: "korp.env.GUNNAR_CLOCK_KEY".to_string(),
                staged_file: String::new(),
            },
            // The data-survives rule (edda c09dd54, 2026-08-29: data on an
            // installed disk must survive): a medium run on a disk it already laid UPGRADES
            // it — /data (users, keys, grants, repos, the closed bootstrap) kept
            // byte for byte, only the ESP + rootfs rewritten. `--param
            // reinstall=wipe` → `korp.reinstall=wipe` is the ONE opt-out: lay
            // it fresh. Optional, never baked on its own; the spelling is
            // nornir-build-thing's `reinstall_param_spec()` (the registry pin
            // predates it, so it is spelled here — `tests/toml_parity.rs` holds
            // the row). korp-installer treats the key as plumbing: it never
            // rides to the installed disk's cmdline.
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "reinstall".to_string(),
                label: "Reinstall policy for a disk that already carries data: keep (upgrade, /data survives) | wipe (lay it fresh)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: false,
                boot_param: "korp.reinstall".to_string(),
                staged_file: String::new(),
            },
            // **What the installer does after INSTALL-OK** (korp-installer
            // `korp.install.then`, edda 7b238f2). `poweroff` is for an
            // ORCHESTRATED medium: UpCloud cannot detach the installer from a
            // running box, so a reboot with the cdrom first re-runs the install
            // (MEASURED 2026-09-14, two hours of it). Powered off, the box reads
            // `stopped`, the re-image tool detaches both media and boots the disk
            // with no settle and no eject — the owner's bar is installer boot →
            // banner in under 60 s (2026-09-15), and the fixed settle alone was
            // 120 s. A SITE choice, so a parameter: absent = reboot, the medium
            // a person boots by hand. Plumbing: it never rides to the installed
            // disk's cmdline.
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "install_then".to_string(),
                label: "After INSTALL-OK: reboot (a person at the box) | poweroff (a cloud re-image tool detaches the medium)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: false,
                boot_param: "korp.install.then".to_string(),
                staged_file: String::new(),
            },
            // **The live-update switch** (owner 2026-10-12): `--param live_app=off`
            // → `korp.live_update=off` — korp-start ignores a staged slot and the box refuses a
            // live payload by name. A per-ESTATE choice (pgo `[estate] live_update`), part of
            // the BASE: it is never flipped live. Absent = on.
            nornir_build_thing::params::ParamSpec {
                lands: nornir_build_thing::params::Landing::Data,
                name: "live_app".to_string(),
                label: "Live updates of the app: on (default) | off (every new app is a base update)".to_string(),
                kind: nornir_build_thing::params::ParamKind::Text,
                required: false,
                boot_param: "korp.live_update".to_string(),
                staged_file: String::new(),
            },
            // **No `role` and no `data_disks` parameter** (owner, 2026-09-21: "gunnar
            // twin must be built EXACTLY like the main, it MUST use the SAME iso").
            // The medium says nothing about what a box is: every box installs as a
            // fresh appliance and a twin is made one over the wire
            // (`Bootstrap.BecomeTwin`, after `cargo xtask elect twin`). Where `/data`
            // goes is korp-installer's own election (≥ 0.1.15: the system on the disk
            // the firmware boots, `/data` on the rest). Both parameters existed
            // 2026-09-17 → 2026-09-22; `tests/twin_is_the_same_medium.rs` pins that
            // they are gone.
        ],
        applied_params: Vec::new(),
        // gunnar's mark, not korp's raven (75a32cc). Under the 0.1.5 pin the
        // toml's key was silently dropped by the loader; since the 0.1.7 bump
        // it LOADS, so the declared logo is finally a compared fact here.
        boot_logo: Some("assets/gunnar.webp".to_string()),
        // The kernel DONOR — the mirror of `[build.installer.kernel_donor]`, declared
        // here the day the registry pin advanced to 0.1.9 (the first release whose
        // `InstallerBuild` carries the field); `tests/toml_parity.rs` holds the two
        // spellings equal, and the raw-file guard that stood in for this line while
        // the 0.1.7 loader was blind to the table folded into that assertion.
        kernel_donor: Some(nornir_build_thing::installer::KernelDonor {
            repo: "tunnr".to_string(),
            kernel: "tunnr-kernel".to_string(),
        }),
        // 0.1.10: the generalised edges, the root `preset` bundles and the
        // applied one. gunnar declares NONE of them — the kernel edge is the
        // `kernel_donor` row above, and a preset bundle has no honest product
        // use here yet (tunnr's `preset = "nuked"` is the donor's). Named,
        // because this mirror names every field on purpose.
        edges: BTreeMap::new(),
        presets: Default::default(),
        applied_preset: None,
        // `[build.installer.fixtures] hub_conf = "wg-hub"` — the same story: a table
        // the 0.1.7 loader ignored benignly, visible to 0.1.9, mirrored now.
        fixtures: [(
            "hub_conf".to_string(),
            nornir_build_thing::params::FixtureKind::WgHub,
        )]
        .into_iter()
        .collect(),
        // The two `[[build.installer.options]]` rows — the site-level choices the
        // 0.1.7 loader could not see (`installtest/tests/installer_options.rs` read
        // them through a path dep meanwhile). Visible to 0.1.9, mirrored now, so the
        // parity assertion holds them like every other field. Each row is a
        // cargo feature the installer's app build carries, the params it needs
        // when off, the symbols that must be ABSENT when off, and the boot-param
        // bake the installer writes for it.
        options: vec![
            nornir_build_thing::options::OptionSpec {
                name: "dhcp".to_string(),
                label: "DHCP (eth0 + eth1)".to_string(),
                default: true,
                feature: "dhcp-client".to_string(),
                params_when_on: Vec::new(),
                params_when_off: vec!["static_ip".to_string(), "gateway".to_string()],
                param_kinds: [
                    (
                        "gateway".to_string(),
                        nornir_build_thing::params::ParamKind::Ipv4,
                    ),
                    (
                        "static_ip".to_string(),
                        nornir_build_thing::params::ParamKind::Ipv4Cidr,
                    ),
                ]
                .into_iter()
                .collect(),
                forbid_symbols: vec!["appliance_pid1::dhcp".to_string()],
                // **The bake WINS over `boot_params`.** Measured 2026-08-31:
                // with `--opt dhcp=on` the composer printed
                // `korp.env.GUNNAR_NET=eth0:dhcp` onto the medium even though
                // `boot_params` above declared both interfaces — an option
                // that bakes a boot param is the writer of that param, and the
                // declaration beside it is what the composer replaces. Both
                // say the same thing now, which is the only arrangement in
                // which reading either one is safe.
                //
                // **`eth1` rides the ON arm only.** It used to ride both
                // (`eth0:{static_ip}:{gateway},eth1:dhcp`), and that OFF bake was
                // a medium that stops at boot: unticking DHCP SUBTRACTS
                // `dhcp-client`, and `NetConfig::parse_list` in that binary
                // refuses `eth1:dhcp` by name (no client to run). Measured red in
                // `installtest/tests/installer_options.rs` 2026-09-15. A static
                // site names its public address and nothing else; a private NIC
                // on such a site wants its own static param, not a lease.
                bakes: Some(nornir_build_thing::options::OptionBake {
                    boot_param: "korp.env.GUNNAR_NET".to_string(),
                    when_on: CLOUD_GUNNAR_NET.to_string(),
                    when_off: "eth0:{static_ip}:{gateway}".to_string(),
                }),
                kernel_requires: dhcp_kernel_requires(),
                requires: Vec::new(),
            },
            nornir_build_thing::options::OptionSpec {
                name: "wireguard".to_string(),
                label: "WireGuard-klient".to_string(),
                default: true,
                feature: "wg-client".to_string(),
                params_when_on: vec!["hub_conf".to_string()],
                params_when_off: Vec::new(),
                param_kinds: std::collections::BTreeMap::new(),
                forbid_symbols: vec!["wg_appliance_core".to_string(), "boringtun".to_string()],
                bakes: None,
                kernel_requires: wireguard_kernel_requires(),
                requires: Vec::new(),
            },
        ],
        // **The MACHINE this medium is built for** (`[build.installer.target]`).
        // Spelled at its DEFAULT shape on purpose — `Target::is_default()` stays
        // true, so nothing built today moves a byte — and the row exists to make
        // the four facts READABLE that were declared nowhere and cost a blind
        // attempt against UpCloud each (2026-08-28/29): SeaBIOS only, the ISO
        // attached as an IDE cdrom, no UART (VGA is the only console), two NICs.
        //
        // It reached this file on 2026-08-30 and not before, and the
        // reason is the pin, not the design: `InstallerBuild` on the published
        // `nornir-build-thing 0.1.10` had no `target` field, so the toml's table
        // was dropped benignly here and read only by the composing forge.
        // ⚠ THIS GROUP IS EXACTLY `Target::default()`, AND THEREFORE EMITS
        // NOTHING. `Target::is_default` is the `skip_serializing_if`, so a
        // declaration that happens to equal the default is invisible to every
        // READER of the toml — the kernel-requirement solver included.
        //
        // Measured 2026-08-31: adding `nics = 2` here made the group non-default,
        // the group then emitted for the first time, and `--build kernel` was
        // refused for BOTH installer rows — `CONFIG_ATA must be y for
        // target.buses (ide-cdrom) but the nuked preset says n`. The conflict
        // was always in the code; it had simply never been visible to the
        // solver. That is NOW-AND-NEXT #4/#7's donor-preset decision and it is
        // not this file's to make, so the group stays default and the two-NIC
        // requirement is expressed where it is actually derivable — from the
        // medium's own `korp.env.GUNNAR_NET`, which names the interfaces
        // (`installtest/src/firmware.rs::declared_interface_count`).
        //
        // ── AND THAT IS WHY `preset` IS `None` HERE (2026-09-08) ────────────
        //
        // The composer asks for a machine on this row, by name, on every real
        // build: *"installer 'gunnar-installer-metered' lays a medium that
        // boots off an imported disk (medium_boot = "hybrid") but names no
        // machine (target.preset) … Name the preset."* It is a fair complaint —
        // `upcloud`, `hetzner-cloud`, `ovh-public-cloud` and
        // `scaleway-instance` really do share this exact device set and split
        // two-and-two on the import container, so nothing derivable from the
        // SHAPE can answer it — and the answer is still not `upcloud`, for a
        // reason that is now SMALLER and entirely different.
        //
        // ⚠ THE KERNEL HALF OF THIS BLOCK IS GONE. It stood for eleven days and
        // it was a base defect the whole time, fixed upstream on 2026-09-07
        // (edda `84a281a` + `1f86a1f`): `MediumBring::IsoCdrom` no longer
        // derives `buses ∋ ide-cdrom`, because how a PLATFORM hands a medium
        // over is not which driver the KERNEL must carry — this medium rides
        // TWICE, as the IDE cdrom SeaBIOS boots and as the virtio disk Linux
        // and korp-installer read the ISO-9660 payload off
        // (`deploy/upcloud/src/machine.rs`) — and `target.uarts` (not
        // `target.consoles`) now owns `CONFIG_SERIAL_8250`, because a COM port
        // is hardware and UpCloud HAS ttyS1/ttyS2 at 0x2f8/0x3e8; only ttyS0 is
        // absent. So `CONFIG_ATA` never arrives and the 8250 never flips.
        // MEASURED, not assumed — `tests/machine_named.rs` re-measures both:
        // the `nuked` donor now ACCEPTS `preset = "upcloud"`, and the merged
        // fragment gains exactly `CONFIG_CPU_SUP_AMD=y` and
        // `CONFIG_VIRTIO_PCI_LEGACY=y`, both of which the shipped
        // `.kernel-out/tunnr-kernel/kernel.config` already carries `=y` as
        // `kconfig-derived` rows. That test REPRODUCES the live pin's
        // `fragment_sha256` (`7dc24476…`) before it measures the delta, so this
        // paragraph is a statement about the artifact and not about the model:
        // the bzImage does not move.
        //
        // ── WHAT IS LEFT IS ONE FIELD, AND IT IS NOT A KERNEL FIELD ─────────
        //
        // `preset` is the ONE writer of the group, so naming it replaces every
        // field above with the machine's own derivation — `firmware` included.
        // `upcloud` is `seabios`, `InstallerBuild::installer_iso` turns that
        // into `IsoFormat::Bios`, and `korp-installer::isocompose` lays the EFI
        // El-Torito section only when `firmware.lays_uefi()`. Naming the
        // machine therefore does not narrow a kernel; it DELETES A SECTION OF
        // THE MEDIUM. The legacy half survives either way (`both` is a superset
        // of `seabios`), so the trade is a pure loss: nothing is bought, and
        // what is spent is every UEFI rig this one medium is proven on —
        // `bench/runs/mother-2026-09-05T02-02` booted the open twin under OVMF
        // 4M three days ago, `installtest … --target qemu-local` is UEFI/OVMF,
        // and the SEALED row is the medium that gets MAILED, to UEFI clouds
        // included. the reasoning archive already ruled this
        // field once, in one sentence: *"`both` keeps the OVMF path for the
        // rig."*
        //
        // That is the same category error as the two the base just fixed, one
        // field over: a machine's firmware is what it CAN start, a medium's
        // sections are what it CARRIES, and a medium is allowed to carry more
        // than any one machine needs. It is reported upward rather than worked
        // around here, because a `target` group that could keep `firmware` while
        // naming a machine is a base change, not a consumer's `..Default`.
        //
        // ── WHICH OF THE FOUR IT WOULD BE, AND WHY NOT THE OTHER THREE ─────
        //
        // The composer's sentence lists four candidates because they share one
        // device set, and that is a statement about the SHAPE. The identity is
        // not in doubt for a moment: `private-gunnar-ops/site.toml` names the
        // appliance this medium installs — `server_uuid =
        // "009f96c9-3dca-4f0d-a122-c7a06ecaabeb"`, `verify_addr =
        // "95.111.206.91:2222"`, `zone = "se-sto1"` — an UpCloud Cloud Server
        // in Stockholm, which is the very box `presets::UPCLOUD`'s own
        // `measured` evidence was read off (2026-08-28/29, se-sto1: SeaBIOS
        // only, ISO as IDE cdrom, no ttyS0, VNC the only console). The other
        // three are not near-misses; two of them cannot take this medium at
        // all:
        //
        // * `ovh-public-cloud` and `scaleway-instance` are
        //   `MediumBring::ImportQcow2` — no ISO door, so what they want is an
        //   imported disk and not the `.iso` this row lays. That is the v1.3.0
        //   defect (media mailed to clouds with no door for one), not a choice;
        // * `hetzner-cloud` IS an ISO-attach machine and is still wrong: one
        //   NIC where this appliance has two, and `measured: None` — gunnar has
        //   never booted on it. Naming an unbooted machine would put a guess in
        //   the field whose whole purpose is to stop one.
        //
        // So the machine is `upcloud`, the question was never which of the
        // four, and this row would name it TODAY if naming it were free.
        //
        // ── IT IS NOT FREE — RE-MEASURED 2026-09-13, NOT INHERITED ──────────
        //
        // `preset = "upcloud"` put on this row with nothing else changed, on
        // `nornir-build-thing 0.1.14` (and 0.1.15/0.1.16 do not touch
        // `target.rs`, so the base has not moved since the ruling above):
        //
        //   `InstallerBuild::installer_iso().format`   Uefi  →  Bios
        //   merged Kconfig `fragment_sha256`   7dc24476…  →  25530934…
        //
        // The first is the EFI El-Torito section leaving the medium, which is
        // the ruling above, still true. The second is new to this measurement
        // and is a smaller cost honestly stated: the two lines the machine adds
        // are already `=y` in the shipped config, so the bzImage rebuilds to
        // the same bytes — but the fragment is what the pin keys on, so the
        // live appliance's kernel pin goes stale and the kernel is rebuilt to
        // produce a file it already has. Neither half buys the medium anything.
        //
        // So this row stays silent BY DECISION — `None` costs a sentence in a
        // log, and the alternative costs an ISO that will not boot where the
        // last one did. `aws-nitro` and `azure-gen2` ARE named on their rows
        // (below), and the difference is no longer `ide-cdrom`: it is that both
        // are UEFI machines, so naming them keeps `lays_uefi()` and their media
        // keep the section this one would lose. That is the whole difference,
        // and `tests/machine_named.rs` asserts it on the named rows too — a
        // machine whose firmware lays no EFI section may not be named here
        // until the base separates the two facts.
        target: Target { firmware: Firmware::Both, consoles: vec![Console::Vga, Console::Serial], buses: vec![Bus::IdeCdrom, Bus::VirtioBlk], nic: Nic::Virtio, ..Target::default() },
        // **What `/data` is laid with** (`[build.installer.data]`).
        // `xfs` over FAT32 because a 100 k-user gunnar needs parallel
        // writers, files past 4 GiB and a journal instead of `dirsync`; `uring`
        // because the deepest queue measured 1.14× per accepted op at 64 clients
        // with durability held across three SIGKILL power cuts. Typed
        // consequences: `CONFIG_XFS_FS` + CRC32C + `CONFIG_IO_URING` on the
        // donor kernel under the features `data.fs` / `data.io`, and
        // `korp.data.fs=xfs` / `korp.data.io=uring` baked on the cmdline.
        // Same 0.1.10 gap as `target` above, closed by the same pin.
        data: Data { fs: DataFs::Xfs, io: DataIo::Uring },
        // **THE MEDIUM'S OWN BOOT SHAPE** (`medium_boot`, 2026-08-30).
        //
        // `hybrid` = LBA 0 of the `.iso` carries a real `55 AA` boot sector that
        // chainloads a disk-flavoured copy of the same `korp-boot-bios` stage.
        // Without it the medium is a pure El Torito ISO, and a legacy BIOS that
        // is handed the identical bytes as a DISK reads LBA 0, finds nothing and
        // parks. MEASURED on the 38 680 576 B medium `621808cb…` — `LBA
        // 0[510..512] = 00 00`, the whole first sector zero — which is why
        // `gunnar-installtest installer --target ovh-public-cloud` produced ZERO
        // bytes of serial and burned its whole 420 s budget.
        //
        // That is not one cloud's quirk. **Every platform that IMPORTS a disk
        // image instead of attaching a cdrom** was unreachable by this product:
        // OVH and Scaleway (glance/qcow2), GCE (raw disk.raw in a tar.gz), AWS
        // (VM Import), Azure (VHD) — the roster's own `MediumBring` word says so
        // for 9 of 13 presets. A USB stick is the same shape.
        //
        // It costs the medium NOTHING: the boot sector and the second stage copy
        // both live in the ISO's reserved 32 KiB system area, which was all zeros
        // before. `nornir_build_thing::iso_writer`'s
        // `a_hybrid_medium_has_a_boot_sector_…_and_costs_no_bytes` asserts the two
        // images are the same length and identical from the PVD on.
        //
        // It is declared on the MEDIUM and not in `target`, and that placement is
        // load-bearing: every field of `target` is a kernel-config consequence
        // (`Target::kconfig_needs`), and this one changes 512 bytes of an image
        // and no kernel symbol at all. Declaring it on `target` would ALSO have
        // made `Target::is_default()` false, which lights up the whole group's
        // closure — including `ide-cdrom → CONFIG_ATA`, which the donor's `nuked`
        // preset vetoes by name.
        medium_boot: MediumBoot::Hybrid,
        // TWO row-level requirement blocks, neither owned by an option: PCI
        // hot-plug, which the runtime data-set growth needs on every medium
        // whatever is toggled (see [`hotplug_requires`]), and the paravirt wall
        // clock, without which the appliance boots on the emulated CMOS and was
        // measured two hours fast on 2026-09-20 (see [`clock_requires`]).
        // Everything else it needs of the donor kernel is named on the OPTION
        // that needs it (`kernel_requires` above) or on the group that owns the
        // fact (`target`, `data`), and the appliance-boot contract is implied by
        // `kernel_donor`. Every twin (open, gce, aws, azure, metered ×3, twin) is
        // cloned from this row and carries both.
        requires: hotplug_requires()
            .into_iter()
            .chain(clock_requires())
            .collect(),
        applied_options: Vec::new(),
    }
}

impl BuildThing for GunnarBuildThing {
    fn manifest(&self) -> Manifest {
        Manifest {
            abi: ABI_VERSION,
            repo: "gunnar".to_string(),
            source_crate: KRATE.to_string(),
            summary: "gunnar's six atoms — the service, the git binary, the thin \
                      gRPC client, the (planned) UI, the solo build and the front \
                      host's clock daemon — across the five delivery columns."
                .to_string(),
        }
    }

    /// The declared build space, assembled from [`VARIANTS`].
    ///
    /// This is the whole migration in one function: the same
    /// [`BuildVariantSet`] every downstream consumer already understands, built
    /// from Rust instead of parsed from a file.
    fn variants(&self) -> BuildVariantSet {
        BuildVariantSet {
            offers: offers(),
            allow_unbuilt: allow_unbuilt(),
            allow_unbuilt_bins: allow_unbuilt_bins(),
            variants: VARIANTS
                .iter()
                .map(crate::variants::Variant::to_variant)
                .collect(),
            containers: containers(),
            isos: isos(),
            installers: installers(),
            toolchain: toolchain(),
            ..BuildVariantSet::default()
        }
    }
}