# Skíðblaðnir — design (the whole system)
> **Class:** `design` · **Function axis:** `f0` (the pipeline) + the ten AIRGAP functions.
> Strict: this file states *only the shape we ship*. Every *why*, every alternative, lives in
> [`reasoning.md`](reasoning.md). Each function has its own `*-design.md` chapter (linked below).

## f0 — the pipeline (the whole)
Skidbladnir folds a *whole deployment* — the infrastructure **and** the software on it — into
**one sealed, content-addressed bundle**, carries it across an airgap, and on the far side
provisions the machine, sets up PKI + a service, and starts every component in dependency order
— with **no network and no shell on the target**. One pipeline (`lib.rs:11`):
```text
pack → archive → fetch ║airgap║ → unfold → install → start(ordered)
```
The crate is **lean by default** (`Cargo.toml:20`): the offline core (pack · archive · fetch ·
platform · layout · product · install · orchestrate · events · identity · systemd) needs only
always-on pure-Rust deps and **always builds offline**. PKI keygen, the znippy engine, and the
Redfish/Terraform/KVM backends sit behind cargo features so the core never pulls a network client
it does not use (`Cargo.toml:91`).
Two laws hold the whole system together:
| law | statement | where |
|---|---|---|
| **content-addressed, verify-on-receipt** | every entry carries a SHA-256; the far side verifies the manifest root **before** unpacking | `archive::BundleManifest` (`archive.rs:52`), `fetch::verify_on_receipt` (`fetch.rs:42`) |
| **verbs as data, no shell** | installs render *files + argv* as data; a thin driver runs them; systemd is driven over **D-Bus**, never `systemctl` | `service::RenderedService` (`service.rs:258`), `systemd::Systemd` (`systemd.rs:472`), `install::apply` (`install.rs:241`) |
**The ten functions and how they wire (the reuse graph):**
| # | function | module(s) | uses | chapter |
|---|---|---|---|---|
| f1 | **pack** | `pack` | archive | [`pack-design.md`](pack-design.md) |
| f2 | **archive** | `archive` | — | [`archive-design.md`](archive-design.md) |
| f3 | **move** | `fetch`, `platform` | archive | [`move-design.md`](move-design.md) |
| f4 | **pki** | `pki` | — | [`pki-design.md`](pki-design.md) |
| f5 | **layout** | `layout` | — | [`layout-design.md`](layout-design.md) |
| f6 | **install** | `service`, `systemd`, `product`, `install`, `identity` | layout · archive · pki · identity | [`install-design.md`](install-design.md) |
| f7 | **unfold** | `unfold` | — | [`unfold-design.md`](unfold-design.md) |
| f8 | **orchestrate** | `orchestrate`, `events` | product | [`orchestrate-design.md`](orchestrate-design.md) |
| f9 | **matrix** | `machine`, `matrix`, `probe/` | archive · service · events | [`matrix-design.md`](matrix-design.md) |
| f10 | **msi** | `msi` | — | [`msi-design.md`](msi-design.md) |
**Optional integrations (off the f-axis, feature-gated):** `vault`/`redfish` (HTTP
seams), and **`openbao`** — provision an OpenBao secret store's structure (k8s auth
method + per-env read policy/role); the platform half of Njord's secret split, with
tillsynia-admin writing the values. See [`openbao-design.md`](openbao-design.md).
The declarative top of the whole system is `config::LakeSpec` (`lib.rs:129`): a version, a set of
`product::ComponentSpec`s, and one `pki::PkiSource`. `LakeSpec::start_waves()` (`lib.rs:137`)
derives the dependency-ordered bring-up (f8) from the specs — one declaration drives pack →
install → ordered start.
## f1 — pack
Gather a product's artifacts + target-identical deps into a versioned **staging tree**
(`pack::Staging`, `pack.rs:31`), with the `VERSION` written *beside* the bundle, not inside it
(`pack::write_version_file`, `pack.rs:84`). See [`pack-design.md`](pack-design.md).
## f2 — archive
Seal the staging tree into one bundle behind the `Archive` trait (`archive.rs:86`). The always-on
default is `TarZstd` (`archive.rs:192`); the `znippy` feature swaps in the REAL znippy-core engine
(seal via `znippy-compress`, selective read/verify/unpack via `znippy-common` — one canonical
`.znippy` format, `ZnippyArchive`). Every bundle carries a per-entry
SHA-256 manifest (`BundleManifest`, `archive.rs:52`). See [`archive-design.md`](archive-design.md).
## f3 — move
Transfer is out-of-band (the bundle is just bytes). Receipt is a local, zero-network gate:
`fetch::verify_on_receipt` (`fetch.rs:42`) recomputes the manifest root, an optional out-of-band
whole-file SHA, and every entry hash before `fetch_and_verify` (`fetch.rs:74`) hands back a
`Bundle`. `platform::detect` (`platform.rs:23`) reads `/etc/os-release` and `platform::unpack`
(`platform.rs:75`) extracts into the software dir. See [`move-design.md`](move-design.md).
## f4 — pki
`pki::PkiSource` (`pki.rs:28`) is `SelfGenerated` (a real ed25519 X.509 CA + leaf via `MachineCa`,
`pki.rs:72`, default-on, offline) | `ExistingVault` | `TempVault` (both HTTP seams behind the
`vault` feature). Certs verify by signature, not clock — airgap targets have no NTP. See
[`pki-design.md`](pki-design.md).
## f5 — layout
`layout::Layout` (`layout.rs:18`) partitions software by date-stamped version
(`software/<YYYYMMDD>/<product>`) beside per-product `data/` and `log/` dirs
(`prepare_product`, `layout.rs:96`). Upgrade is an **atomic symlink swap** of `current` via
temp+rename (`activate`, `layout.rs:115`); old generations are retained as rollback targets. See
[`layout-design.md`](layout-design.md).
## f6 — install
The **install tree**: one declarative `service::ServiceSpec` (`service.rs:137`) renders through the
`ServiceRenderer` trait (`service.rs:285`) to many init systems (`renderer_for`, `service.rs:292`
— systemd-system, systemd-user, OpenRC, SysV, runit, s6-rc, dinit, Kubernetes, OCI, Quadlet and
Windows SCM ALL implemented — 11 of 11, full coverage). Output is a
`RenderedService` (`service.rs:258`): files + `enable/start/status/stop` argv **as data**. The
product lifecycle (`install::{install,update,uninstall}_product`, `install.rs:88`) provisions dirs,
unfolds the product, mints an install `identity` (`identity::ensure`, `identity.rs:135`), and
returns an `InstallOutcome` (`install.rs:57`) a thin `apply` (`install.rs:241`) runs. See
[`install-design.md`](install-design.md).
**The eleven backends group into THREE target families** — host-init (systemd ×2 · OpenRC · SysV ·
runit · s6-rc · dinit · Windows SCM) · container-host (OCI · Quadlet) · cluster (Kubernetes) — one
per *identity + supervision + config-delivery* model, and that family (not the backend) is where an
honest-degrade decision belongs. The **Kubernetes** family ships today as a sync renderer plus an
async applier ([`k8s-unfold.md`](k8s-unfold.md)), with the Talos node tier beneath it
([`talos-unfold.md`](talos-unfold.md)). What `ServiceSpec` must still gain to make that third family
honest — lifecycle **hooks** (`pre_start`/`post_start`/`pre_stop`/`post_stop`), the **health split**
(startup vs liveness vs readiness), a neutral **`secret_env`** (today only `SystemdServiceSpec` has
one, so a secret in `ServiceSpec::env` renders inline into a world-readable unit), a
**`stop_timeout_sec`** (native on 10 of 11 backends), and an observable **`RenderedService::degraded`**
so a dropped field is data instead of a doc comment — is derived capability-by-capability against
Kubernetes and the vetra/Njord Helm+Talos deployer in
[`kubernetes-target-design.md`](kubernetes-target-design.md) (rejected alternatives:
[`kubernetes-target-reasoning.md`](kubernetes-target-reasoning.md)).
## f7 — unfold
One `unfold::Unfold` trait (`unfold.rs:34`) over four `Target`s (`unfold.rs:22`): KVM
(`unfold.rs:44`, local test), libvirt (pure-Rust RPC, `unfold.rs:138`), Redfish (BMC, feature
`redfish`, `unfold.rs:451`), Terraform (feature `terraform`, `unfold.rs:638`). Every backend's
request/config builder is pure and testable without the tool; only `unfold()` executes. See
[`unfold-design.md`](unfold-design.md).
## f8 — orchestrate
`orchestrate::start_waves` (`orchestrate.rs:42`) Kahn-topo-sorts the products into dependency
waves; `start_order`/`teardown_order` (`orchestrate.rs:23`,`:31`) flatten and reverse them. Each
boundary emits an `events::AirgapEvent` (`events.rs:49`) to a warehouse `JsonlSink`
(`events.rs:76`). See [`orchestrate-design.md`](orchestrate-design.md).
**Tier-0.5 common-infra bring-up** — `product::common_infra()` describes the app-agnostic platform
substrate (ingress-nginx · cert-manager · holger-registry · OpenBao) as `ComponentSpec`s;
`product::common_infra_waves()` orders them through the SAME `start_waves` engine
(`[[cert-manager, ingress-nginx], [holger-registry, openbao]]`), each rendering to k8s manifests via
the ONE k8s renderer. The OpenBao structure is populated by the pure `openbao::PopulatePlan`
(`openbao` feature). Renderer half is pure/laptop-testable; live apply is deferred. See
[`tier0-common-infra-design.md`](tier0-common-infra-design.md).
## f9 — matrix
`machine::MachineController` (`machine.rs:273`) boots a real KVM guest (`KvmController`,
`machine.rs:481`) or container (`ContainerController`, `machine.rs:891`) from a content-addressed
`ImageCache` (`machine.rs:312`). `matrix::run_matrix` (`matrix.rs:347`) runs the `(OS × backend)`
install cells, injects the static-musl `skidbladnir-probe`, and collects a typed `ProbeReport`
(`matrix.rs:63`) back over virtio-serial. See [`matrix-design.md`](matrix-design.md).
These spawns are being routed through **jera** (edda), the ONE job/container/VM manager, instead of
raw `podman`/`qemu` (feature `jera`): `MachineSpec::job_policy` classifies into a `jera::JobPolicy`
and `to_jera_container_spec` maps the container case onto a `jera::container::ContainerSpec`
(→ `draupnir::BootSpec`), with an honest `jera_unsupported()` gap-list gating a live swap so airgap
isolation is never dropped. See [`machine-jera-routing.md`](machine-jera-routing.md).
## f10 — msi
`msi::guid_v5` (`msi.rs:244`) mints stable UUID-v5 component GUIDs; `msi::render_wxs`
(`msi.rs:58`) emits real WiX v4 sources; `msi::WixBuilder` (`msi.rs:145`) builds the MSI under
Wine-in-podman. See [`msi-design.md`](msi-design.md).
## Which recipe reader this repo uses, and why (lane AJ, 2026-08-30)
**Reader:** `nornir-build-thing` **0.1.12**, PATH.
**Why:** in-constellation; recipe is schema 1. This edge had carried a `path` and **no
`version` at all** — the float with nowhere left to read it: the manifest made no
claim about which reader the guard runs while compiling whatever edda's tree
happened to be that hour (the lock said 0.1.10).
The rule itself — `[nornir-build-thing] schema = N`, a refusal that names the
version to upgrade to, and a loader that refuses to DROP a key it cannot read —
lives once, in **edda `.nornir/build-design.md`, "THE COMPATIBILITY RULE"**. The
cross-version guard that would have caught the 2026-08-30 skew is
`edda/nornir-build-thing-skew`.