# 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 in [`design.md`](design.md), which by rule mentions
> none of this. Per-function *why* lives in the `*-reasoning.md` chapters.
## 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`](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`](znippy-core-consolidation-unsorted.md) and
[`archive-reasoning.md`](archive-reasoning.md).