Skíðblaðnir — reasoning (why this overall shape)
Class:
reasoning. The why, the trade-offs, and the alternatives we rejected — for the system as a whole. The descriptive shape is indesign.md, which by rule mentions none of this. Per-function why lives in the*-reasoning.mdchapters.
Why one sealed bundle instead of an agent + repo
The obvious way to install software on a remote machine is an agent that pulls from a package repo (Ansible-over-SSH, cloud-init fetching, a k8s pull). Every one of those assumes a network path back to the internet — the exact thing an airgap forbids. Skidbladnir inverts it: fold the whole deployment (infra provisioning and software and config and images and data) into one content-addressed bundle, carry it by any means (disk, one-way diode, courier), and make the far side self-sufficient. The bundle is just bytes, so the transport is unconstrained; the far side needs no agent, no SSH, no package mirror. That is why fetch does zero network I/O — transfer is out-of-band by design, and receipt is a pure local verify.
Why content-addressed, verify-before-unpack
Across an airgap there is no TLS session to the source and no way to "re-pull if corrupt". The only defence is to make the bundle self-verifying: every entry carries a SHA-256, the manifest carries a root hash over the sorted entries, and the far side recomputes all of it before extracting a single file (fetch::verify_on_receipt). We reject "extract then check" because a malicious or corrupt entry is dangerous the moment it lands on disk — the check has to gate the unpack, not follow it. The verify is also two-way: an entry present in the archive but absent from the manifest is as bad as a missing one (the extra-entry rejection), because a tampered bundle would add files, not just change them.
Why "verbs as data" and D-Bus, not shell scripts
The whole reason Skidbladnir exists is to delete the per-OS bespoke install scripts (the "x14-datalabb Ansible problem"). If the installer were a pile of shell, we would have re-created the thing we set out to remove. So a renderer produces data — the unit files plus the enable/start/status/stop argv — and a single thin driver runs it. This buys three things a script cannot: the whole tree is unit-testable at zero privilege (an OpenRC enable is the assertion ["rc-update","add","holger","default"], not a side effect); one driver runs every backend uniformly; and systemd is driven straight over its D-Bus API (systemd::Systemd) with no systemctl subprocess, so there is no shell on the target at all.
Why lean-by-default with feature gates
A leaf product embedded on a target device (tunnr, an appliance) must not drag a Vault HTTP client, a UEFI image builder, or the znippy CLI onto the device just to unpack itself. So the core is deliberately small — pure-Rust, always-offline deps only — and every heavy or network-touching capability (PKI keygen, znippy, Redfish/Terraform/KVM, the matrix) sits behind a cargo feature. The rejected alternative — one crate with everything always compiled — would make the embedded client fat and couple its build to tools it never runs. The cost we accept is a feature matrix; the payoff is that cargo build with no features still does the entire offline pipeline.
Why a separate leaf repo (extracted from nornir)
Skidbladnir was pulled out of the nornir monorepo into its own repo (Cargo.toml:11) so leaf consumers — nornir, holger, tunnr — can depend on the airgap framework without riding a release-cascade through nornir. Keeping it in-tree meant every airgap change waited on a full nornir release. As a leaf, it versions and publishes on its own clock.
Rejected: absorbing tunnr
tunnr (the OS-less UEFI appliance endpoint) is described by a ComponentSpec and carried by the bundle, exactly like holger — a sibling, not a dependency. Folding it in would re-couple what the extraction decoupled and drag a UEFI image toolchain into the lean core. The dependency stays one-way: tunnr → depends on → Skidbladnir; Skidbladnir only describes tunnr. See install-reasoning.md for the appliance-arm discussion.
Rejected: linking the real znippy crate
The znippy engine here is znippy-format-faithful but re-implemented on std + zstd rather than linking the real znippy crate, which would pull znippy-cli + rayon and blow the lean/no-rayon budget. The trade-off, and the plan to converge on a shared znippy-core, is in znippy-core-consolidation-unsorted.md and archive-reasoning.md.