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

nordisk/edda

crates/nornir-build-thing/src/toolchain_archive.rs

//! **The OFFLINE toolchain courier** — a staged OCI archive of the kernel
//! toolchain image, content-addressed, verified before use, and refused BY NAME
//! when it is absent or holds something else (`#14`, lane AO 2026-08-30).
//!
//! # The gap this closes
//!
//! A kernel build compiles airgapped already: the container runs with
//! `--network none` whenever `toolchain_packages` is empty
//! ([`KernelBuild::needs_network`](crate::kernel::KernelBuild::needs_network)),
//! and the tarball plus the kclause formula both ride a content-addressed cache
//! a courier can fill by hand. **The whole remaining gap was the toolchain image
//! itself**: absent from the local store, podman's only remaining move is to
//! reach for a registry — on the one box where reaching out is the failure.
//!
//! So the image travels as BYTES, in the same cache, addressed the same way:
//!
//! ```text
//! <cache_dir>/tarballs/<tarball_sha256>.tar.xz     the kernel source   (already)
//! <cache_dir>/kclause/<tarball_sha256>.kmax-<v>/   the solver formula  (already)
//! <cache_dir>/images/<toolchain_image_sha256>.tar  the TOOLCHAIN       (this module)
//! ```
//!
//! # ⚠ The correction to the spec — MEASURED, oden 2026-08-30
//!
//! edda `.nornir/kernel-guide.md` (lane AH, §"An OFFLINE kernel build") specified
//! answer **(b)**, an OCI archive, and said it *"makes the committed pin true on
//! every box because the bytes travel rather than the recipe"*. **That is false
//! for the pin the format actually declares.** A `podman save` → `podman load`
//! round trip of `nordisk-kernel-toolchain:live` on this box:
//!
//! ```text
//! format           image id (config digest)   manifest digest (.Digest)
//! ────────────────────────────────────────────────────────────────────────────
//! on the origin box  ce66a0100cb8…            f573ca0a86c0…
//! docker-archive →   ce66a0100cb8…  SAME      5e628ed94b35…  CHANGED
//! oci-archive    →   ac4606f05153…  CHANGED   85189a0753c9…  CHANGED
//! ```
//!
//! and on the receiving store, asked directly:
//!
//! ```text
//! ce66a0100cb8…                                   EXISTS   (bare image id)
//! sha256:ce66a0100cb8…                            EXISTS
//! nordisk-kernel-toolchain@sha256:f573ca0a…       ABSENT   ← the committed pin
//! nordisk-kernel-toolchain@sha256:5e628ed9…       EXISTS   (the RECEIVER's own)
//! ```
//!
//! A **manifest digest does not survive the courier**: podman re-synthesises the
//! manifest on load, so the digest the origin box committed names nothing on the
//! receiving one. The **image id — the config digest — does** survive, through a
//! `docker-archive`, and is a legal image reference (`podman run <id>`).
//!
//! Hence the two departures from the spec, both deliberate:
//!
//! 1. **`docker-archive`, not `oci-archive`.** The OCI conversion rewrites the
//!    config too, so it preserves *neither* identity; it is the one format that
//!    makes the archive unverifiable against anything the recipe can hold. (It
//!    is also what draupnir's `export_image`/`import_image` already speak, so
//!    this module adds no second way to move an image — LAW 5.)
//! 2. **The pin that an archive is verified against is the IMAGE ID.** A
//!    declaration whose `toolchain_image_sha256` is a per-box MANIFEST digest is
//!    refused against a staged archive by name, with the id it should carry
//!    instead. This is the same conclusion the guide reached from the other side
//!    (*"the fix is to publish the image … or to pin the image's INPUTS rather
//!    than a locally-built manifest"*) — pinning the id is the third option, and
//!    it is the only one that needs no registry.
//!
//! # The rule, and the failure it refuses to have
//!
//! [`resolve_toolchain`] runs **before the build container is created**, and its
//! whole job is that a missing toolchain can never become a network fetch:
//!
//! | the box has | this module does |
//! |---|---|
//! | the pinned image already | nothing — [`ToolchainSource::AlreadyPresent`] |
//! | a staged archive whose id == the pin | verifies, loads, runs it BY ID |
//! | a staged archive holding something else | [`ARCHIVE_MISMATCH`] — refuses, does **not** load |
//! | neither, and a pin is declared | [`TOOLCHAIN_MISSING`] — refuses, naming the exact path |
//! | neither, and NO pin is declared | nothing: an unpinned image is an online declaration, and this module does not invent a policy the recipe never stated |
//!
//! The last row is the honest limit. `resolve_toolchain` cannot make an
//! *unpinned* build airgapped, because there is no address for a courier to
//! deliver to; it says so via [`ToolchainSource::Unpinned`] and the receipt
//! records which of the five rows a build took.
//!
//! # Producing the archive
//!
//! [`ToolchainImageStore::export_archive`] is the door, and
//! [`stage_toolchain_archive`] is the one-call form that writes the entry under
//! its own content address. A door nobody can execute reads exactly like a law
//! being obeyed — the whole reason `toolchain_images_live.rs` exists — so the
//! export lands with the import in the same commit.

use std::path::{Path, PathBuf};

use anyhow::{Result, anyhow, bail};

/// The subdirectory of the shared kernel cache staged toolchain archives live
/// in — beside `tarballs/` and `kclause/`, same courier, same cache.
pub const TOOLCHAIN_ARCHIVE_DIR: &str = "images";

/// The named refusal when neither the image nor a staged archive is on the box.
/// Grep-able on purpose: the airgap claim is only testable if its failure has a
/// name a test can assert instead of a sentence a rewording can silently change.
pub const TOOLCHAIN_MISSING: &str = "NORDISK-TOOLCHAIN-MISSING";

/// The named refusal when a staged archive holds an image that is not the
/// pinned one. **Never** a fallback: a toolchain that can be silently
/// substituted makes every provenance record that names it worthless.
pub const ARCHIVE_MISMATCH: &str = "NORDISK-TOOLCHAIN-ARCHIVE-MISMATCH";

/// The named refusal when the load reported success and the store still does not
/// have the image — the third door, asserted because an engine that answers
/// "loaded" and an engine that has the image are two different claims.
pub const LOAD_UNCONFIRMED: &str = "NORDISK-TOOLCHAIN-LOAD-UNCONFIRMED";

/// Where a staged toolchain archive for `image_pin` lives inside `cache_dir`.
///
/// Keyed by the declared pin itself — exactly like
/// [`kclause_cache_entry`](crate::kernel::kclause_cache_entry) and the tarball
/// cache — so a courier knows the filename from the recipe alone, with nothing
/// to look up and no index to fall out of date.
pub fn toolchain_archive_entry(cache_dir: &Path, image_pin: &str) -> PathBuf {
    cache_dir.join(TOOLCHAIN_ARCHIVE_DIR).join(format!("{image_pin}.tar"))
}

/// A reference that addresses an image by its **id** (config digest). Legal for
/// podman and Docker alike, and — unlike `name@sha256:<manifest digest>` — it
/// still resolves after the image has travelled as an archive.
pub fn image_id_ref(config_digest: &str) -> String {
    format!("sha256:{config_digest}")
}

/// What an image archive turns out to hold, read from the archive itself.
///
/// The shape of draupnir's `ImageArchive`, restated here so the dep-light build
/// (and every test fake) can name it without pulling `bollard`/`tar`. The live
/// store fills it straight from `draupnir::container::inspect_image_archive`.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StagedImage {
    /// Archive size in bytes.
    pub bytes: u64,
    /// Every `RepoTags` entry in the archive, in archive order. May be empty:
    /// an archive can name its content by digest alone.
    pub tags: Vec<String>,
    /// The image config digest as bare lowercase hex, no `sha256:` prefix —
    /// **the image id**, and the one identity a `docker-archive` round trip
    /// preserves.
    pub config_digest: String,
}

/// Which of the five rows in the module table a build took — recorded on the
/// [`KernelPin`](crate::kernel::KernelPin) so a receipt says how the toolchain
/// got onto the box, not merely which one it claims to be.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolchainSource {
    /// The store already had the pinned image; nothing was staged or fetched.
    AlreadyPresent,
    /// A staged archive was verified and loaded (or was already loaded) — the
    /// offline path.
    StagedArchive,
    /// The declaration pins no image digest, so no archive can be addressed and
    /// no offline claim is made.
    Unpinned,
    /// The runner cannot answer presence questions at all (a fake, a
    /// dep-light build). Nothing was checked and nothing is claimed.
    Unchecked,
}

impl ToolchainSource {
    /// The stable string a receipt carries.
    pub fn as_str(self) -> &'static str {
        match self {
            ToolchainSource::AlreadyPresent => "already-present",
            ToolchainSource::StagedArchive => "staged-archive",
            ToolchainSource::Unpinned => "unpinned",
            ToolchainSource::Unchecked => "unchecked",
        }
    }

    /// Whether this build's toolchain was proven to be on the box before the
    /// container was created — i.e. whether the run can be claimed airgapped.
    pub fn verified(self) -> bool {
        matches!(self, ToolchainSource::AlreadyPresent | ToolchainSource::StagedArchive)
    }
}

/// What [`resolve_toolchain`] decided: the reference the run must use, how the
/// toolchain got there, and the archive it came from when it came from one.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolchainResolution {
    /// The image reference the [`KernelRunRequest`](crate::kernel::KernelRunRequest)
    /// must carry. Identical to the declared one on every path but the staged
    /// archive, where it becomes the image id (the declared
    /// `name@sha256:<manifest digest>` names nothing after the courier).
    pub image_ref: String,
    /// Which row of the table this was.
    pub source: ToolchainSource,
    /// The archive that satisfied it, when one did.
    pub archive: Option<PathBuf>,
    /// The image id actually resolved, when it is known.
    pub config_digest: Option<String>,
}

/// **The image-store seam** — presence, inspect, load, export — so the whole
/// decision tree above is drivable by a fake with no daemon, and so the live
/// impl is nothing but four calls into draupnir's existing image verbs.
///
/// Deliberately NOT part of [`KernelContainerRunner`](crate::kernel::KernelContainerRunner):
/// a runner that cannot answer these questions is a legitimate runner (that is
/// the `Unchecked` row), and folding them in with a default `Err` would make
/// every fake in the crate carry a method it has no opinion about.
pub trait ToolchainImageStore {
    /// Is this reference resolvable in the local store? `Ok(false)` must be
    /// reserved for the engine answering *no such image*; a broken socket is an
    /// `Err`, never a `false` — mistaking one for the other is how a build
    /// decides to rebuild (or re-fetch) the world.
    fn image_present(&self, image_ref: &str) -> Result<bool>;
    /// Read an archive's identity **with no daemon** — the check that must
    /// happen before any byte is handed to an engine.
    fn inspect_archive(&self, path: &Path) -> Result<StagedImage>;
    /// Load an archive into the local store, returning what was loaded.
    fn load_archive(&self, path: &Path) -> Result<StagedImage>;
    /// Write the local image out as a `docker-archive` — the staging door.
    fn export_archive(&self, image_ref: &str, dest: &Path) -> Result<StagedImage>;
}

/// **Stage the toolchain for an offline box**: export `image_ref` from this
/// store into `<cache_dir>/images/<image id>.tar`, named by what it actually
/// holds rather than by what the caller hoped for.
///
/// Returns the path written. The caller copies that one file to the airgapped
/// machine's cache, and puts the id (the file's stem) in
/// `toolchain_image_sha256` — the whole recipe, and the reason the entry is
/// named by its content instead of by the exporter's intent.
pub fn stage_toolchain_archive(
    store: &dyn ToolchainImageStore,
    image_ref: &str,
    cache_dir: &Path,
) -> Result<PathBuf> {
    if !store.image_present(image_ref)? {
        bail!(
            "{TOOLCHAIN_MISSING}: cannot stage `{image_ref}` — this box does not have it. \
             Build it first (`DraupnirKernelRunner::build_toolchain_image`, or the \
             `[[build.image]]` kind), then stage."
        );
    }
    let dir = cache_dir.join(TOOLCHAIN_ARCHIVE_DIR);
    std::fs::create_dir_all(&dir)
        .map_err(|e| anyhow!("create toolchain archive cache {}: {e}", dir.display()))?;
    // Write to a per-process staging name and RENAME on success, the same rule
    // the tarball cache follows: a half-written archive must never be able to
    // look like a cache entry to a concurrent build.
    let staging = dir.join(format!(".stage.{}.part", std::process::id()));
    let got = store
        .export_archive(image_ref, &staging)
        .map_err(|e| {
            let _ = std::fs::remove_file(&staging);
            // MEASURED 2026-08-30: podman normalises a short name on the way
            // OUT (`nordisk-kernel-toolchain` comes back as
            // `docker.io/library/nordisk-kernel-toolchain:latest`), and
            // draupnir's export refuses to file an archive under a tag the
            // archive does not carry — correctly, but the message says nothing
            // about normalisation, so the operator reads it as "my image is
            // wrong" rather than "spell it the way podman prints it".
            anyhow!(
                "{e:#}\n\nIf that says the archive carries a DIFFERENT tag than the one you \
                 asked for, the two are the same image under podman's normalisation: pass the \
                 reference exactly as `podman images` prints it (repository AND tag, e.g. \
                 docker.io/library/<name>:latest), not the short name."
            )
        })?;
    if got.config_digest.is_empty() {
        let _ = std::fs::remove_file(&staging);
        bail!(
            "{ARCHIVE_MISMATCH}: the archive exported from `{image_ref}` names no image \
             config, so it has no content address and nothing could ever verify it"
        );
    }
    let dest = dir.join(format!("{}.tar", got.config_digest));
    std::fs::rename(&staging, &dest)
        .map_err(|e| anyhow!("publish toolchain archive {}: {e}", dest.display()))?;
    Ok(dest)
}

/// **The preflight.** Decide — before a container is created — which image
/// reference this build runs, and refuse by name rather than let a missing
/// toolchain become a registry fetch.
///
/// `declared_ref` is
/// [`KernelBuild::toolchain_image_ref`](crate::kernel::KernelBuild::toolchain_image_ref),
/// `image_name` and `pin` are the two halves it is built from, and `kernel_name`
/// is only used to make the refusals name the row an operator has to edit.
pub fn resolve_toolchain(
    store: Option<&dyn ToolchainImageStore>,
    declared_ref: &str,
    image_name: &str,
    pin: &str,
    cache_dir: &Path,
    kernel_name: &str,
) -> Result<ToolchainResolution> {
    let unchecked = |source| ToolchainResolution {
        image_ref: declared_ref.to_string(),
        source,
        archive: None,
        config_digest: None,
    };
    let Some(store) = store else {
        return Ok(unchecked(ToolchainSource::Unchecked));
    };
    if pin.is_empty() {
        // Nothing to address an archive by. An unpinned image is an ONLINE
        // declaration by construction (`debian:trixie` resolves by pulling), and
        // inventing a refusal here would break every such build for a promise
        // the recipe never made. Said out loud on the receipt instead.
        return Ok(unchecked(ToolchainSource::Unpinned));
    }

    let archive = toolchain_archive_entry(cache_dir, pin);
    let by_id = image_id_ref(pin);

    if archive.is_file() {
        // 1. WITH NO DAEMON: what does this file actually hold?
        let held = store
            .inspect_archive(&archive)
            .map_err(|e| anyhow!("kernel '{kernel_name}': staged toolchain archive {}: {e:#}", archive.display()))?;
        // 2. Is it the pinned toolchain? Measured: a docker-archive round trip
        //    preserves the image id and NOT the manifest digest, so this is the
        //    one comparison that can be true on a box that did not mint it.
        if held.config_digest != pin {
            bail!("{}", mismatch_refusal(kernel_name, &archive, pin, image_name, &held));
        }
        // 3. Load only if it is not already there — a 1.2 GB load per build is
        //    a courier bill nobody asked for.
        let already = store.image_present(&by_id)?;
        if !already {
            let loaded = store.load_archive(&archive)?;
            if !loaded.config_digest.is_empty() && loaded.config_digest != pin {
                bail!(
                    "{ARCHIVE_MISMATCH}: kernel '{kernel_name}': the archive {} inspected as \
                     image id {pin} but the engine says it loaded {}. Two readers of one file \
                     disagree; nothing here is safe to build with.",
                    archive.display(),
                    loaded.config_digest
                );
            }
            // A DIFFERENT door than the one that just claimed success: "the
            // load streamed cleanly" and "the store has the image" are two
            // claims, and only the second one is the one the build needs.
            if !store.image_present(&by_id)? {
                bail!(
                    "{LOAD_UNCONFIRMED}: kernel '{kernel_name}': loaded {} and the engine then \
                     said it has no image `{by_id}`. The archive is not usable on this box; \
                     re-stage it.",
                    archive.display()
                );
            }
        }
        return Ok(ToolchainResolution {
            image_ref: by_id,
            source: ToolchainSource::StagedArchive,
            archive: Some(archive),
            config_digest: Some(pin.to_string()),
        });
    }

    // No archive. The image may still be here — either under the declared
    // reference (the box that minted the pin) or under the pin read as an id.
    if store.image_present(declared_ref)? {
        return Ok(ToolchainResolution {
            image_ref: declared_ref.to_string(),
            source: ToolchainSource::AlreadyPresent,
            archive: None,
            config_digest: None,
        });
    }
    if store.image_present(&by_id)? {
        return Ok(ToolchainResolution {
            image_ref: by_id,
            source: ToolchainSource::AlreadyPresent,
            archive: None,
            config_digest: Some(pin.to_string()),
        });
    }
    bail!("{}", missing_refusal(kernel_name, &archive, declared_ref, image_name, pin));
}

/// The [`TOOLCHAIN_MISSING`] sentence. Split out so a guard can render it
/// without a store, and so the wording lives in exactly one place.
pub fn missing_refusal(
    kernel_name: &str,
    archive: &Path,
    declared_ref: &str,
    image_name: &str,
    pin: &str,
) -> String {
    let name = if image_name.is_empty() { "<the pinned image>" } else { image_name };
    format!(
        "{TOOLCHAIN_MISSING}: kernel '{kernel_name}': the toolchain image `{declared_ref}` is \
         not in this box's image store, and no staged archive is either.\n\
         \n\
         The build STOPS here on purpose. The alternative is for the container engine to go \
         and fetch a toolchain, which is exactly what an airgapped build may not do — and \
         what makes an airgap claim untestable when it happens quietly.\n\
         \n\
         Two ways forward:\n\
         \n\
         \x20 OFFLINE — stage the archive (the answer this cache exists for):\n\
         \x20   on a box that HAS the image:\n\
         \x20     podman save --format docker-archive -o <id>.tar {name}\n\
         \x20     # <id> = podman image inspect --format '{{{{.Id}}}}' {name}\n\
         \x20   copy it to this box as:\n\
         \x20     {archive}\n\
         \x20   and pin `toolchain_image_sha256 = \"<id>\"` for the [[build.kernel]] row\n\
         \x20   named '{kernel_name}' — in the repo's `*-build-thing` CODE, which is the\n\
         \x20   one writer; the .nornir/build-variants.toml is EMITTED from it (re-run\n\
         \x20   `cargo run -p <repo>-build-thing -- emit`), so a hand edit there is\n\
         \x20   reverted by the next emit and fails the parity gate meanwhile.\n\
         \x20   MEASURED: an image id survives a save/load round trip and a manifest\n\
         \x20   digest does NOT, so the id is the only 64 hex characters that mean the\n\
         \x20   same image on two machines. It is still a MINTING EVENT, not a property\n\
         \x20   of the recipe: two mints of one Containerfile 90 s apart on one box give\n\
         \x20   two different ids (lane AT, 2026-08-30).\n\
         \n\
         \x20 ONLINE — mint the image on this box and re-pin:\n\
         \x20   DraupnirKernelRunner::build_toolchain_image(<ctx>, \"{name}\")\n\
         \n\
         (the declaration currently pins {pin})",
        archive = archive.display(),
    )
}

/// The [`ARCHIVE_MISMATCH`] sentence — the one an operator reads when the
/// committed pin is a per-box MANIFEST digest, which is the state every
/// declaration in the constellation was in on 2026-08-30.
pub fn mismatch_refusal(
    kernel_name: &str,
    archive: &Path,
    pin: &str,
    image_name: &str,
    held: &StagedImage,
) -> String {
    let name = if image_name.is_empty() { "<the pinned image>" } else { image_name };
    let tags = if held.tags.is_empty() { "<none>".to_string() } else { held.tags.join(", ") };
    format!(
        "{ARCHIVE_MISMATCH}: kernel '{kernel_name}': the staged toolchain archive\n\
         \x20   {archive}\n\
         holds image id {got} (tags: {tags}), but this declaration pins {pin}.\n\
         \n\
         Refusing to load it. A toolchain that can be swapped for another one without \
         anybody noticing makes every provenance record naming it worthless, so this is a \
         stop and never a warning.\n\
         \n\
         If {pin} is a MANIFEST DIGEST, that is the bug, and it is not yours: a manifest \
         digest is a PER-BOX fact that does not survive the courier. Measured on oden \
         2026-08-30, `podman save --format docker-archive` + `podman load` of \
         nordisk-kernel-toolchain:\n\
         \x20   image id        ce66a0100cb8…  -> ce66a0100cb8…   SAME\n\
         \x20   manifest digest f573ca0a86c0…  -> 5e628ed94b35…   CHANGED\n\
         so `{name}@sha256:<manifest digest>` names nothing on the receiving box while the \
         id resolves. Pin the ID:\n\
         \x20   toolchain_image_sha256 = \"{got}\"\n\
         for the [[build.kernel]] row named '{kernel_name}', in the repository's \
         *-build-thing CODE — the one writer. Do NOT edit .nornir/build-variants.toml: \
         it is EMITTED from that code and held byte-identical by a parity gate, so a hand \
         edit is reverted by the next `cargo run -p <repo>-build-thing -- emit` and fails \
         the gate meanwhile.",
        archive = archive.display(),
        got = held.config_digest,
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    /// The pin used everywhere below — 64 lowercase hex, the shape a real image
    /// id has.
    const ID: &str = "ce66a0100cb84cd83aab99c0d37648e82ad4ef80bf4a536c736574d1ef7c61f0";
    /// A DIFFERENT 64 hex characters, standing in for the per-box manifest
    /// digest the constellation actually commits today.
    const MANIFEST: &str = "f573ca0a86c020810baff6085b65408cf38c35b045ce45919e106eb6bb005146";

    /// A store with a scripted local image set, recording every call so a test
    /// can assert what did NOT happen (that a mismatched archive was never
    /// loaded is the whole point, and only a call log can say it).
    #[derive(Default)]
    struct FakeStore {
        present: RefCell<Vec<String>>,
        held: RefCell<Option<StagedImage>>,
        calls: RefCell<Vec<String>>,
        inspect_fails: bool,
        load_lies: bool,
    }

    impl FakeStore {
        fn with_image(refs: &[&str]) -> Self {
            FakeStore {
                present: RefCell::new(refs.iter().map(|s| s.to_string()).collect()),
                ..Default::default()
            }
        }
        fn holding(mut self, digest: &str) -> Self {
            self.held = RefCell::new(Some(StagedImage {
                bytes: 42,
                tags: vec!["nordisk-kernel-toolchain:live".into()],
                config_digest: digest.into(),
            }));
            self
        }
        fn calls(&self) -> Vec<String> {
            self.calls.borrow().clone()
        }
    }

    impl ToolchainImageStore for FakeStore {
        fn image_present(&self, image_ref: &str) -> Result<bool> {
            self.calls.borrow_mut().push(format!("present({image_ref})"));
            Ok(self.present.borrow().iter().any(|r| r == image_ref))
        }
        fn inspect_archive(&self, path: &Path) -> Result<StagedImage> {
            self.calls.borrow_mut().push(format!("inspect({})", path.display()));
            if self.inspect_fails {
                bail!("not an image archive: no manifest.json at the tar root");
            }
            self.held
                .borrow()
                .clone()
                .ok_or_else(|| anyhow!("fake store holds no archive identity"))
        }
        fn load_archive(&self, path: &Path) -> Result<StagedImage> {
            self.calls.borrow_mut().push(format!("load({})", path.display()));
            let held = self
                .held
                .borrow()
                .clone()
                .ok_or_else(|| anyhow!("fake store holds no archive identity"))?;
            if !self.load_lies {
                self.present.borrow_mut().push(image_id_ref(&held.config_digest));
            }
            Ok(held)
        }
        fn export_archive(&self, image_ref: &str, dest: &Path) -> Result<StagedImage> {
            self.calls.borrow_mut().push(format!("export({image_ref} -> {})", dest.display()));
            let held = self
                .held
                .borrow()
                .clone()
                .ok_or_else(|| anyhow!("fake store holds no archive identity"))?;
            std::fs::write(dest, b"a docker-archive would be here").unwrap();
            Ok(held)
        }
    }

    fn cache() -> tempfile::TempDir {
        tempfile::tempdir().expect("cache dir")
    }

    fn stage(dir: &Path, pin: &str) -> PathBuf {
        let p = toolchain_archive_entry(dir, pin);
        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
        std::fs::write(&p, b"pretend this is 1.2 GB of docker-archive").unwrap();
        p
    }

    /// ★ **THE RED the whole item exists for**: no image, no archive, and the
    /// build stops with a name instead of quietly asking a registry.
    ///
    /// The assertion is on the REFUSAL's content, not on `is_err()`: a build
    /// that fails for a different reason is not this guard passing.
    #[test]
    fn a_missing_toolchain_and_a_missing_archive_refuse_by_name() {
        let dir = cache();
        let store = FakeStore::with_image(&[]);
        let err = resolve_toolchain(
            Some(&store),
            &format!("nordisk-kernel-toolchain@sha256:{MANIFEST}"),
            "nordisk-kernel-toolchain",
            MANIFEST,
            dir.path(),
            "lil-kernel",
        )
        .expect_err("no image and no archive must refuse");
        let msg = format!("{err:#}");
        assert!(msg.contains(TOOLCHAIN_MISSING), "{msg}");
        // It names the EXACT path a courier must fill…
        assert!(
            msg.contains(&toolchain_archive_entry(dir.path(), MANIFEST).display().to_string()),
            "the refusal must name the path, not describe one:\n{msg}"
        );
        // …the runnable command that produces it…
        assert!(msg.contains("podman save --format docker-archive"), "{msg}");
        // …and the row to edit.
        assert!(msg.contains("lil-kernel") && msg.contains("toolchain_image_sha256"), "{msg}");
        // Nothing was fetched, because nothing CAN be: only presence was asked.
        assert!(
            store.calls().iter().all(|c| c.starts_with("present(")),
            "the preflight must not touch the archive when there is none: {:?}",
            store.calls()
        );
    }

    /// ★ **The GREEN under the same denial**: the identical call, with the
    /// archive staged, resolves — and the reference it hands back is the ID,
    /// because the declared `name@sha256:<manifest>` is the thing that does not
    /// survive the courier.
    #[test]
    fn a_staged_archive_satisfies_the_same_build_the_missing_one_refused() {
        let dir = cache();
        let archive = stage(dir.path(), ID);
        let store = FakeStore::with_image(&[]).holding(ID);
        let r = resolve_toolchain(
            Some(&store),
            &format!("nordisk-kernel-toolchain@sha256:{ID}"),
            "nordisk-kernel-toolchain",
            ID,
            dir.path(),
            "lil-kernel",
        )
        .expect("a staged, matching archive is the offline path");
        assert_eq!(r.source, ToolchainSource::StagedArchive);
        assert!(r.source.verified());
        assert_eq!(r.image_ref, format!("sha256:{ID}"), "the run must address the image by ID");
        assert_eq!(r.archive.as_deref(), Some(archive.as_path()));
        assert_eq!(r.config_digest.as_deref(), Some(ID));
        // Inspected BEFORE loaded, and confirmed through a second presence call
        // AFTER the load — the load's own "ok" is not the evidence.
        let calls = store.calls();
        let inspect = calls.iter().position(|c| c.starts_with("inspect(")).expect("inspected");
        let load = calls.iter().position(|c| c.starts_with("load(")).expect("loaded");
        assert!(inspect < load, "the archive must be read before it is handed to an engine: {calls:?}");
        assert!(
            calls.iter().skip(load).any(|c| c.starts_with("present(")),
            "the load must be confirmed through a different door: {calls:?}"
        );
    }

    /// ★ The failure mode this must not have: a staged archive holding a
    /// DIFFERENT image is refused, and — asserted through the call log — is
    /// never handed to the engine.
    ///
    /// This is also the state every declaration in the constellation is in
    /// today, because the committed pin is a manifest digest: the refusal has
    /// to say so and hand over the id to pin instead, or it is a dead end.
    #[test]
    fn an_archive_that_is_not_the_pinned_toolchain_is_refused_and_never_loaded() {
        let dir = cache();
        stage(dir.path(), MANIFEST);
        // The file staged under the manifest-digest name holds, as it must, an
        // image whose id is something else entirely.
        let store = FakeStore::with_image(&[]).holding(ID);
        let err = resolve_toolchain(
            Some(&store),
            &format!("nordisk-kernel-toolchain@sha256:{MANIFEST}"),
            "nordisk-kernel-toolchain",
            MANIFEST,
            dir.path(),
            "lil-kernel",
        )
        .expect_err("an archive holding another image must refuse");
        let msg = format!("{err:#}");
        assert!(msg.contains(ARCHIVE_MISMATCH), "{msg}");
        assert!(msg.contains(ID) && msg.contains(MANIFEST), "both digests must be named:\n{msg}");
        // The measured explanation, not a shrug.
        assert!(msg.contains("manifest digest") && msg.contains("CHANGED"), "{msg}");
        assert!(msg.contains(&format!("toolchain_image_sha256 = \"{ID}\"")), "{msg}");
        assert!(
            !store.calls().iter().any(|c| c.starts_with("load(")),
            "a mismatched archive must never reach the engine: {:?}",
            store.calls()
        );
    }

    /// A store that says "loaded" while the image is still not there must not
    /// pass. Without this, the whole preflight rests on one engine's word about
    /// its own success.
    #[test]
    fn a_load_that_does_not_produce_the_image_is_caught() {
        let dir = cache();
        stage(dir.path(), ID);
        let mut store = FakeStore::with_image(&[]).holding(ID);
        store.load_lies = true;
        let err = resolve_toolchain(
            Some(&store),
            &format!("nordisk-kernel-toolchain@sha256:{ID}"),
            "nordisk-kernel-toolchain",
            ID,
            dir.path(),
            "lil-kernel",
        )
        .expect_err("a load the store cannot confirm must refuse");
        assert!(format!("{err:#}").contains(LOAD_UNCONFIRMED), "{err:#}");
    }

    /// The image is already here: no archive is needed, none is looked for, and
    /// the declared reference is used unchanged. (The row that keeps every
    /// existing build byte-identical.)
    #[test]
    fn an_image_already_on_the_box_needs_no_courier() {
        let dir = cache();
        let declared = format!("nordisk-kernel-toolchain@sha256:{MANIFEST}");
        let store = FakeStore::with_image(&[&declared]);
        let r = resolve_toolchain(
            Some(&store),
            &declared,
            "nordisk-kernel-toolchain",
            MANIFEST,
            dir.path(),
            "lil-kernel",
        )
        .expect("the box that minted the pin still builds");
        assert_eq!(r.source, ToolchainSource::AlreadyPresent);
        assert_eq!(r.image_ref, declared);
        assert!(r.archive.is_none());
    }

    /// ★ **The row a RE-PINNED declaration takes on the box that minted it** —
    /// the one thing that has to hold before anybody changes a
    /// `toolchain_image_sha256` from a manifest digest to an image id.
    ///
    /// After a re-pin the declared reference becomes
    /// `name@sha256:<image id>`, and that spelling resolves to **nothing**: a
    /// `name@sha256:` reference names a MANIFEST digest, so podman reads an id
    /// there as a registry pull (measured 2026-08-23, the diagnosis recorded on
    /// [`KernelBuild::toolchain_image_ref`](crate::kernel::KernelBuild::toolchain_image_ref)).
    /// The minting box must therefore be carried by the *second* presence
    /// question — the pin read as an id — or a re-pin would break the builds of
    /// the very box that produced the image, with no archive anywhere to explain
    /// why.
    ///
    /// Watched red 2026-08-30 by deleting the `image_present(&by_id)` arm:
    /// `NORDISK-TOOLCHAIN-MISSING`, on a box holding the image.
    #[test]
    fn a_declaration_repinned_to_an_image_id_still_builds_on_the_box_that_minted_it() {
        let dir = cache();
        // What the recipe composes after the re-pin — and what podman cannot
        // resolve. The store deliberately does NOT hold this spelling.
        let declared = format!("nordisk-kernel-toolchain@sha256:{ID}");
        let store = FakeStore::with_image(&[&image_id_ref(ID)]);
        let r = resolve_toolchain(
            Some(&store),
            &declared,
            "nordisk-kernel-toolchain",
            ID,
            dir.path(),
            "lil-kernel",
        )
        .expect("the minting box has the image under its id and must still build");
        assert_eq!(r.source, ToolchainSource::AlreadyPresent);
        assert_eq!(
            r.image_ref,
            image_id_ref(ID),
            "the run must be handed the ID, not the `name@sha256:<id>` the declaration \
             composes — that one is a registry pull, which is the failure this whole module \
             refuses to have"
        );
        assert_eq!(r.config_digest.as_deref(), Some(ID));
        assert!(r.archive.is_none(), "nothing was staged: {:?}", r.archive);
        // And the declared spelling really was asked for and really did answer
        // no — otherwise this test passes on a store that answers `true` to
        // everything and proves nothing.
        assert!(
            store.calls().iter().any(|c| c.contains(&declared)),
            "the declared reference must be tried FIRST: {:?}",
            store.calls()
        );
    }

    /// An unpinned declaration is an ONLINE one and this module says so rather
    /// than inventing a refusal the recipe never asked for. The identity-value
    /// trap in reverse: a `verified()` that answered `true` here would make
    /// every unpinned build look airgapped.
    #[test]
    fn an_unpinned_declaration_makes_no_offline_claim() {
        let dir = cache();
        let store = FakeStore::with_image(&[]);
        let r = resolve_toolchain(
            Some(&store),
            "docker.io/library/debian:trixie",
            "docker.io/library/debian",
            "",
            dir.path(),
            "lil-kernel",
        )
        .expect("an unpinned image is not this module's business");
        assert_eq!(r.source, ToolchainSource::Unpinned);
        assert!(!r.source.verified(), "an unpinned build may never read as airgapped");
        assert_eq!(r.image_ref, "docker.io/library/debian:trixie");
        assert!(store.calls().is_empty(), "nothing to ask: {:?}", store.calls());
    }

    /// No store at all (a fake runner, a dep-light build): nothing is checked
    /// and — critically — nothing is CLAIMED. `Unchecked.verified()` is false.
    #[test]
    fn no_store_means_no_claim() {
        let dir = cache();
        let r = resolve_toolchain(None, "whatever:latest", "whatever", ID, dir.path(), "k")
            .expect("a runner with no image store still builds");
        assert_eq!(r.source, ToolchainSource::Unchecked);
        assert!(!r.source.verified());
        assert_eq!(r.image_ref, "whatever:latest");
    }

    /// An unreadable archive is a refusal that names the file, not a fall
    /// through to a fetch. (draupnir's `inspect_image_archive` is what
    /// distinguishes truncated from not-an-archive; this asserts the path the
    /// fault travels, not the fault taxonomy, which is draupnir's own test.)
    #[test]
    fn an_unreadable_archive_refuses_naming_the_file() {
        let dir = cache();
        let archive = stage(dir.path(), ID);
        let mut store = FakeStore::with_image(&[]).holding(ID);
        store.inspect_fails = true;
        let err = resolve_toolchain(
            Some(&store),
            "x",
            "nordisk-kernel-toolchain",
            ID,
            dir.path(),
            "lil-kernel",
        )
        .expect_err("an archive that cannot be read is not a green");
        let msg = format!("{err:#}");
        assert!(msg.contains(&archive.display().to_string()), "{msg}");
        assert!(msg.contains("no manifest.json"), "{msg}");
        assert!(!store.calls().iter().any(|c| c.starts_with("load(")), "{:?}", store.calls());
    }

    /// The staging door writes the entry under the ID it actually exported —
    /// never under the name the caller passed, which is how an archive ends up
    /// filed as something it is not.
    #[test]
    fn staging_files_the_archive_under_what_it_holds() {
        let dir = cache();
        let store = FakeStore::with_image(&["nordisk-kernel-toolchain:live"]).holding(ID);
        let p = stage_toolchain_archive(&store, "nordisk-kernel-toolchain:live", dir.path())
            .expect("stage");
        assert_eq!(p, toolchain_archive_entry(dir.path(), ID));
        assert!(p.is_file());
        // And the staging name is gone: no `.part` may survive a success.
        let leftovers: Vec<_> = std::fs::read_dir(dir.path().join(TOOLCHAIN_ARCHIVE_DIR))
            .unwrap()
            .filter_map(|e| e.ok())
            .map(|e| e.file_name().to_string_lossy().to_string())
            .filter(|n| n.contains(".part"))
            .collect();
        assert!(leftovers.is_empty(), "{leftovers:?}");
    }

    /// Staging an image the box does not have refuses by name instead of
    /// writing a zero-byte file that later "verifies" as present.
    #[test]
    fn staging_an_absent_image_refuses() {
        let dir = cache();
        let store = FakeStore::with_image(&[]);
        let err = stage_toolchain_archive(&store, "nordisk-kernel-toolchain:live", dir.path())
            .expect_err("nothing to stage");
        assert!(format!("{err:#}").contains(TOOLCHAIN_MISSING), "{err:#}");
        assert!(!dir.path().join(TOOLCHAIN_ARCHIVE_DIR).join("x").exists());
    }
}