<!-- nornir:class: design -->
# machine.rs spawns → jera (edda), the ONE job/container/VM manager
**jera-dedup audit (2026-07-14), worst offender.** Skidbladnir's `machine` module is a *complete
parallel container+VM engine*: `container::ContainerController` shells straight to `podman`/`docker`
(`run -d` / `rm -f` / `exec`) and `kvm::KvmController` spawns `qemu-system-x86_64` — consuming **zero**
jera despite already depending on draupnir. The constellation law (L5) is that job/container/VM
spawning lives in exactly ONE place: **jera** (`edda/crates/jera`), the durable job ledger over
**draupnir**'s ONE OCI/KVM engine.
This is the **additive, swap-ready first step** (feature `jera`). It adds no new spawn path and
removes none — the working `Command` paths are the untouched fallback. What it adds is the *seam the
swap needs* plus an **honest gap-list** that keeps a live swap from silently regressing airgap
isolation.

## What landed (feature `jera`, `src/machine.rs::jera_route`)
Four pure methods on `MachineSpec`, plus RED-when-broken tests and a `machine-jera` testmatrix row:
| method | does |
|---|---|
| `job_policy() -> jera::JobPolicy` | classify: an OCI image ref → `Container` (draupnir OCI); a disk image (`Path`/`Cached`) → `Vm` (draupnir KVM). The one source of truth for "which jera job kind is this machine". |
| `to_jera_container_spec() -> Result<jera::container::ContainerSpec>` | map a container spec onto jera's `ContainerSpec` — image + name **+ net mode + bind mounts** (LANDED) — which maps onward onto a `draupnir::BootSpec` via jera's `to_draupnir`. Bails on a VM disk (that routes through jera's `vm` seam). |
| `jera_net() -> jera::container::NetMode` | the single container net mapping: `Nat`→`Default` (no `--network`), `Isolated`→`None` (airgap `--network none`), `Named`→`Default` (gap-guarded, never routed). |
| `jera_unsupported() -> Vec<&'static str>` | the HONEST gap, now shrunk to the ONE remaining case: `"net"` when the spec asks for an arbitrary **named** host-network (jera's `NetMode` is `Default/None/Host/Bridge`, no arbitrary name). NAT + airgap net and bind mounts now carry faithfully. |
| `jera_routable() -> bool` | true for a NAT, an **airgap** (`Isolated`), and a **mount-bearing** container (empty gap). Only a named-network container keeps the direct `podman` fallback. The guard a routed `start` checks before delegating to jera. |
The chain **MachineSpec → jera::container::ContainerSpec → draupnir::BootSpec** is proven
daemon-free: the test asserts the mapped `BootSpec` `validate()`s and carries the image, exercising
only draupnir's always-compiled types (no podman, no feature engine).
## The KVM/VM seam (mirror of the container seam, for `kvm::KvmController::start`)
`kvm::KvmController::start` shells straight to `qemu-system-x86_64`. The same swap-ready seam now
classifies + maps a **VM** spec — four more pure `MachineSpec` methods, mirroring the container ones:
| method | does |
|---|---|
| `to_jera_vm_spec() -> Result<jera::vm::BootSpec>` | map a disk VM onto jera's VM payload (`name` + `disk` + size); `kernel`/`rootfs` empty — a Skidbladnir VM is a **bootloader-in-disk** boot. Bails on an OCI ref (that routes through the container seam). |
| `to_jera_vm_payload() -> Result<jera::agent::VmBoot>` | map onto the serde-ready **agent** VM payload (`image`/`cores`/`mem_mb`) — the wire-safe spec a remote worker maps back onto jera's VM runner (`WorkPayload::Vm`) in the multi-machine dispatch path. |
| `jera_vm_unsupported() -> Vec<&'static str>` | the HONEST gap: `"disk-boot"` (**always** — see below), `"net"` (airgap `-nic none`/named), `"provision"` (cloud-init/Ignition seed jera's VM payload drops), `"iso"` (install/data CD). |
| `jera_vm_routable() -> bool` | disk VM with an empty gap. **Currently always `false`** (the disk-boot gap never clears), so every VM keeps the direct `qemu` path. |
## Why the VM seam is a seam only — the load-bearing `disk-boot` gap
A Skidbladnir VM boots a **full qcow2 through its own in-image bootloader** with **no separate
`-kernel`** (`kvm::KvmController::full_argv` attaches the disk as a virtio drive under
`snapshot=on`). But jera/draupnir's VM payload is **kernel-required**: draupnir's KVM image sources are
`KernelRootfs { kernel, rootfs }` and `Disk { kernel, disk }`, and `draupnir::BootSpec::validate()`
**rejects an empty kernel path** — there is **no kernel-less bootloader-in-disk VM mode**. On top of
that, jera's `vm::BootSpec::to_draupnir` is only compiled under the heavy `vm-tunnr` feature (unlike
container's, which is always compiled), so the VM→draupnir leg isn't even reachable from the lean
`jera` build. So `"disk-boot"` is **always** in the gap-list and `jera_vm_routable()` is always
`false` — the direct `qemu` path is the untouched fallback (L2). The day jera/draupnir gain a
kernel-less disk boot (+ net + cloud-init + iso passthrough, and an always-compiled VM `to_draupnir`),
the gap clears, the tripwire test flips, and the live VM route lands.
## Airgap live-route — LANDED (Skidbladnir → jera → draupnir `--network none`)
The container airgap route is now **wired end-to-end at the spec/request layer**. jera grew the two
load-bearing container knobs (`edda/crates/jera`: `NetMode` on `ContainerSpec`, commit `e317d6b`; bind
mounts, `8718d70`), and jera's `ContainerSpec::to_draupnir` threads the net onward
(`d.net = draupnir::NetMode::from_oci_value(self.net.oci_value())`) so draupnir's OCI engine sets
`HostConfig.network_mode = "none"` on the live podman run (draupnir `970dacb`). Skidbladnir now maps
into it:
```
MachineSpec.net = NetMode::Isolated (airgap intent)
│ jera_net()
▼
jera::container::ContainerSpec.net = NetMode::None
│ to_draupnir() (draupnir::NetMode::from_oci_value(Some("none")))
▼
draupnir::BootSpec.net = NetMode::None → live `podman create --network none` (NO egress)
```
The tripwire has **flipped**: `jera_routable()` is now `true` for an airgap (`Isolated`) container, so
the airgap launch is delegatable through jera→draupnir instead of a direct `podman --network none`
subprocess. The old gate — `jera_unsupported()` pushing `"net"` for **any** non-NAT net, which kept
airgap off the route — is **bypassed** for `Isolated`: only an arbitrary **named** network still lands
in the gap (jera cannot express an arbitrary named net). The direct `podman` path (`run_argv` honouring
`--network none`) is **kept unchanged** as the documented fallback (L2) — nothing was removed.
**Asserted, not just claimed (L4).** `airgap_container_routes_through_jera_with_network_none` asserts
the mapped `jera::container::ContainerSpec` carries `net == NetMode::None` AND its onward
`draupnir::BootSpec` Debug carries `net: None` (the live `--network none` knob). RED-when-broken:
neutralizing the `Isolated → None` mapping in `jera_net()` makes both catch the egress-allowed
regression (verified). The `machine-jera` testmatrix row records the same as a green cell.
**Live smoke-test (deferred).** A real `podman create --network none` run + an egress-blocked probe is
a Loki/integration-bencher concern (needs a podman host); this pass lands + asserts the **spec/request**
carrying `net=None`. The remaining container knob for a *lifecycle* swap of `start`/`stop`/`exec` (a
detached handle + `exec`-into model — jera's `run_container` is run-to-completion) stays deferred.
## Why the VM live route is still a seam only
A live route of `ContainerController::start`'s **lifecycle** (and the whole VM path) through jera is
still gated on missing jera API. jera's `run_container` is run-to-completion (no detached handle +
`exec`-into), and the VM payload is kernel-required (no bootloader-in-disk mode). `jera_routable()` /
`jera_vm_routable()` remain the tripwires: any residual gap keeps the direct `podman`/`qemu` path.
### Missing jera knobs (logged for the jera agent in `Skidbladnir_agents_problems.md`)
1. ✅ **`ContainerSpec` net mode** — LANDED (jera `e317d6b`): `NetMode::None` → `--network none`. The
airgap container route is live at the spec/request layer (see the "Airgap live-route" section above).
The one residual sub-gap is an arbitrary **named** host-network (jera has `Default/None/Host/Bridge`).
2. ✅ **`ContainerSpec` bind mounts** — LANDED (jera `8718d70`): `BindMount` → draupnir `-v` run.
Skidbladnir `Mount` now maps onto it; `pack.rs`/`msi.rs` can adopt it next.
3. **detached container with a stateless handle + `exec`-into** — `MachineController` is
`start→Handle` / `stop(Handle)` / `exec(Handle, cmd)`; jera's `DraupnirContainerBootRunner` is a
stateful `BootProc` with no `exec`, and `run_container` is run-to-completion. The lifecycle shapes
don't line up.
4. **image build** — `pack.rs`/`msi.rs` do `podman build`; jera wraps draupnir's run/boot, not build.
5. **VM: kernel-less bootloader-in-disk boot** — draupnir's KVM image sources both require a `-kernel`;
there is no full-disk (in-image bootloader) VM mode. The load-bearing VM gap (the analogue of the
container `net` gap) — Skidbladnir boots exactly this shape, so no VM is routable until it exists.
6. **VM: net mode + cloud-init passthrough on `jera::vm::BootSpec`** — jera's VM payload has no net mode
(airgap `-nic none`) and no cloud-init (draupnir's `BootSpec` carries `cloud_init`, but jera's VM
spec drops it) and no extra-ISO attach. Add these so a routed VM keeps airgap + firstboot provisioning.
7. **VM: `jera::vm::BootSpec::to_draupnir` is gated behind `vm-tunnr`** — unlike container's
(always-compiled), so the VM→draupnir leg is unreachable from a lean `jera` build. Make it
always-compiled (it only needs draupnir's default `BootSpec` constructors) so the seam can prove the
VM→draupnir mapping daemon-free like the container one does.
## Remaining bypass sites in Skidbladnir (documented follow-ups)
- `machine.rs` `kvm::KvmController::start` — `qemu-system-x86_64`. **Seam landed** (`to_jera_vm_spec`/
`to_jera_vm_payload` + `jera_vm_routable` tripwire); the live route is blocked on knobs 5–7 (a
kernel-less disk-boot VM mode + net/cloud-init passthrough + an always-compiled VM `to_draupnir`).
- `machine.rs` `container::ContainerController::{start,stop,exec}` — direct podman (routed once knobs 1–3 land).
- `pack.rs` — `podman build/create/cp/rm` (already has a `pack-oci` draupnir path; jera adds the ledger + build knob 4).
- `msi.rs` — `podman` MSI build (already has `msi-oci` draupnir path; jera adds the ledger + build knob 4).
- `unfold.rs` — `terraform` (needs a jera IaC seam — `FEAT`, out of scope here).