<!-- nornir:class: design -->
# build-thing launch axis + the machine spine — design
> **Class:** `design` · **Home:** draupnir, because draupnir is the hub the whole
> machine spine hangs from. The *how-to* is in
> [`build-thing-launch-guide.md`](build-thing-launch-guide.md). This doc is the
> map for the deep-wiring that connects a **built artifact** to a **running,
> asserted machine**, all Rust, no shell.

---
## 1. The thesis: two axes, not one list
A build-thing produces an **artifact** and then (optionally) **launches** it. Those
are two different things, and conflating them is what made `exe / container / kvm /
iso` read as one list when it is really a matrix:
| BUILD (what comes out) | LAUNCH (how you run it) | verdict channel |
|---|---|---|
| `exe` — cargo build | **run** — spawn the binary | exit code + stdout |
| `container` — `ContainerBuild` (bollard) | **deploy** — `podman run` (bollard) + **logs pane** | container stdout stream |
| `vm` — `VmBuild` (kernel+rootfs) | **boot console** — `tunnr_vm::boot_test` + **serial pane** | contract markers in serial |
| `iso` — bootloader-in-disk *(new)* | **boot console** · or → USB → bare metal | same serial contract |
`kvm` is **not** an artifact — it is a launch *target*. An artifact declares which
targets it supports; the UI renders the launch control that fits. This is the whole
correction: build-thing already has `ArtifactKind ∈ {Exe,Lib,Wasm,Container,Vm,Service}`
(edda `nornir-build-thing/src/variant.rs`); what it lacks is a first-class **launch**
axis beside the build one, and a **kind-aware** rendering of it.
## 2. The machine spine — who owns what
The launch axis only works because there is exactly one machine spine underneath,
and every layer has a single job:
- **tunnr** — the muscle. Owns the QEMU/OVMF/KVM primitive (`boot_test`,
`src/boot_primitive.rs`) **and** the OS-less appliance semantics (boot contract,
markers, PID-1 payloads). It already boots **both** models: direct-kernel when a
kernel is set, firmware/bootloader-in-disk when it is empty.
- **draupnir** — the thin seam. The boot/provisioning library: one `BootSpec`,
`ImageSource`, `Boot`/`Lifecycle`, fleet fan-out, the seed builder, three backend
adapters (`kvm`→tunnr, `oci`, `redfish`). It is the **ruler from tunnr's thinnest
appliance to the fattest full distro** — the same axis as *direct-kernel ↔
bootloader-in-disk*.
- **Skidbladnir** — airgap-install application. Should **delete** its own qemu argv
builder (`unfold::kvm` / `full_argv` / `Command::new("qemu-system-x86_64")`) and
route through `draupnir::kvm::KvmBoot`. It already consumes `draupnir::seed`; the
launcher is the last thing it twins.
- **dwarves** — the CI/CD forge. Pure orchestrator; it **never instantiates a VM
itself**. It calls the ready-made tool (jera → draupnir → tunnr). Its `run_vm`
today is a TODO stub — the wiring turns it into a real call.
**No cycle:** `draupnir → tunnr-vm` (feature `backend-tunnr`) is one-directional; the
`tunnr → draupnir` edge lives only in leaf crates (`tunnr-demo`, `payloads/*`), never
in `tunnr-vm`. Clean DAG, verified against the Cargo.tomls.
## 3. The keystone change — draupnir grows a fat-dist boot model
Today `draupnir::ImageSource` (`src/lib.rs:124`) can express direct-kernel
(`KernelRootfs`) and kernel+disk (`Disk`) — but **`validate()` requires a kernel on
both** (`src/lib.rs:622-624`), so it cannot express the **kernel-less,
bootloader-in-disk** boot that a fat distro (and Skidbladnir's airgap install) needs.
That single gap is why `jera_vm_routable()` is always `false`.
The five focused edits (all in draupnir; **zero in tunnr** — the engine already does it):
1. `src/lib.rs:124` — add `ImageSource::BootloaderDisk { disk: String }` (self-booting
disk, no `-kernel`).
2. `src/lib.rs:154` — `suits()`: `BootloaderDisk` suits `Backend::Kvm`.
3. `src/lib.rs:622` — `validate()`: require only the disk path, **not** a kernel.
4. `src/lib.rs:~463` — a `bootloader_disk` constructor peer.
5. `src/kvm.rs:74` — `to_tunnr_spec`: map `BootloaderDisk` to an **empty-kernel**
`tunnr_vm::BootSpec` (tunnr firmware-boots the disk). And, while here, fix the
**seed drift** (`kvm.rs:104`): use tunnr's native `BootSpec::with_seed` instead of
hand-rolling the seed `-drive` into `extra_qemu_args`, so the seed argv is authored
in one place.
This unlocks **both** downstream moves at once: the fat-dist launch, and Skidbladnir
routing onto draupnir.
## 4. The launch axis — declared in the model, rendered elsewhere
The wiring adds an explicit **launch** action, distinct from **build**, to the
build-thing **data model** (`edda/crates/nornir-build-thing/src/surface.rs`,
`state_json`) — kind-aware:
- **Container** → **DEPLOY** (a future bollard `podman run`).
- **Vm / iso** → **BOOT CONSOLE** (a future `tunnr_vm::boot_test`, serial + contract
markers).
- **Exe** → **RUN** (spawn + exit code).
This is a **declaration** of what launch a cell supports, carried in `state_json` for a
consumer (dwarves, or a future UI) to render. Two deliberate scope lines:
- **nornir is out of scope.** nornir is huge and its viz (`src/viz/build_tab.rs`,
`test_boot.rs`) is *not* touched by this wiring. The model declares the axis; where
it renders is a separate decision.
- **logs get their own framework.** The surface exposes a typed **seam**
(`LaunchOutput { Logs, Serial }`) as the hand-off point — it does **not** implement
log/serial streaming. That is a separate framework, wired later.
No shell — when the executor is built, container work is the bollard Rust API and VM
work is `boot_test`; neither lives in this model phase.
## 5. Caching & sealing
- **holger** is the artifact cache/store: built container/VM artifacts are content-
addressed and stored via dwarves' existing `HolgerArtifactStore` seam, so a launch
can pull a prior build instead of rebuilding.
- **znippy** can seal/package an artifact set, but is **not required** for the launch
axis today — noted as optional, not wired in this pass.
## 6. The shell island — ported to Rust
tunnr's outer Buildroot orchestration **was** `variants/buildroot/build.sh` +
`board/tunnr/post-build.sh` (shell). Both are now **deleted and ported to pure Rust**:
- **`tunnr-buildroot`** (`tunnr-vm::buildroot`) — the orchestrator: reuses
`build_rust_init` for the inner PID-1, clones Buildroot via **gix** (behind the
`buildroot-fetch` feature — no shelled `git`, law L9), and runs Buildroot `make`
via a direct `Command::new("make")` (a sanctioned build-tool exec, not shell glue).
- **`tunnr-postbuild`** — the `BR2_ROOTFS_POST_BUILD_SCRIPT` hook as a **compiled Rust
binary**. Because it is compiled it is executable by construction, so the
`post-build.sh` mode-100644 / Error-126 bug is gone rather than patched.
`BuildShVmBuilder` calls the Rust path; the defconfigs point at the compiled hook. No
shell remains in the Buildroot path — the last island is drained.
## 7. What the wiring touches (the deep pass)
| Repo | Change | Gate |
|---|---|---|
| draupnir | `ImageSource::BootloaderDisk` + validate + `to_tunnr_spec` + seed-drift fix | `cargo test --features backend-tunnr,seed` |
| Skidbladnir | route `machine::kvm` through `draupnir::kvm`; enable `draupnir/backend-tunnr`; delete own launcher | `cargo build --features kvm,matrix` |
| tunnr | `tunnr-demo` adopts draupnir fleet fan-out for the two-node OS-less boot | `cargo build -p tunnr-demo` |
| dwarves | `run_vm` real spec via jera→draupnir; new `vm-boot` feature enabling `jera/vm-tunnr`; `VmBoot` step carries a real spec | `cargo build` + `cargo build --features vm-boot` |
| edda | launch axis **declared** in build-thing surface (kind-aware) + `LaunchOutput` logs seam | `cargo build -p nornir-build-thing` |
| ~~nornir~~ | out of scope — huge; viz rendering + logs are a separate framework | — |
Every row is compile-gated; nothing is reported wired that does not build.