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)
- ✅
ContainerSpecnet mode — LANDED (jerae317d6b):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 hasDefault/None/Host/Bridge). - ✅
ContainerSpecbind mounts — LANDED (jera8718d70):BindMount→ draupnir-vrun. SkidbladnirMountnow maps onto it;pack.rs/msi.rscan adopt it next. - detached container with a stateless handle +
exec-into —MachineControllerisstart→Handle/stop(Handle)/exec(Handle, cmd); jera'sDraupnirContainerBootRunneris a statefulBootProcwith noexec, andrun_containeris run-to-completion. The lifecycle shapes don't line up. - image build —
pack.rs/msi.rsdopodman build; jera wraps draupnir's run/boot, not build. - 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 containernetgap) — Skidbladnir boots exactly this shape, so no VM is routable until it exists. - 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'sBootSpeccarriescloud_init, but jera's VM spec drops it) and no extra-ISO attach. Add these so a routed VM keeps airgap + firstboot provisioning. - VM:
jera::vm::BootSpec::to_draupniris gated behindvm-tunnr— unlike container's (always-compiled), so the VM→draupnir leg is unreachable from a leanjerabuild. Make it always-compiled (it only needs draupnir's defaultBootSpecconstructors) 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.rskvm::KvmController::start—qemu-system-x86_64. Seam landed (to_jera_vm_spec/to_jera_vm_payload+jera_vm_routabletripwire); the live route is blocked on knobs 5–7 (a kernel-less disk-boot VM mode + net/cloud-init passthrough + an always-compiled VMto_draupnir).machine.rscontainer::ContainerController::{start,stop,exec}— direct podman (routed once knobs 1–3 land).pack.rs—podman build/create/cp/rm(already has apack-ocidraupnir path; jera adds the ledger + build knob 4).msi.rs—podmanMSI build (already hasmsi-ocidraupnir path; jera adds the ledger + build knob 4).unfold.rs—terraform(needs a jera IaC seam —FEAT, out of scope here).