[package]
name = "skidbladnir"
version = "0.2.3"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
description = "Skíðblaðnir — 100% Rust airgap pack/move/unfold/install framework (Redfish · Terraform · KVM)."
repository = "https://git.gunnar.rs/nordisk/Skidbladnir"
readme = "README.md"
# Development notes, agent instructions and machine-local tooling stay in the
# repository; they are not part of the published crate.
exclude = [
".nornir/",
"docs/",
"brokkr.toml",
".gitattributes",
"tests/gated_runs_are_declared_guard.rs",
]
# Standalone workspace: extracted from the nornir repo into its own leaf repo
# (Skidbladnir) so leaf consumers (nornir, holger, tunnr) can pull the airgap
# framework without a release-cascade through nornir. The crate is published as
# `skidbladnir` (renamed from the original `nornir-airgap`), matching the repo
# name Skidbladnir. It
# has no sub-crates, so this `[workspace]` just pins it as its own root
# (and stops a parent dir's workspace from accidentally absorbing it).
[workspace]
resolver = "3"
# `crates/Skidbladnir-build-thing` — this repo's `impl BuildThing` (lane AG,
# 2026-08-30). A member, not a dependency: nothing Skidbladnir ships links it. It
# exists so `.nornir/build-variants.toml` can be EMITTED from code instead of
# hand-written. The name carries the repo's capital S because dwarves' forge looks
# for a package named `<repo>-build-thing` and the repo IS `Skidbladnir`.
#
# `crates/installer/nordisk-pairing` — the operator-side installer (moved from edda
# 2026-09-26, owner ruling: installer functionality lives in Skidbladnir, not in a
# product). It connects OUT to every VM's bootstrap listener over a temporary
# WireGuard + mTLS link, feeds each VM, collects the pairing signal and leaves.
# The crate keeps its name; the root `skidbladnir` crate does not depend on it.
#
# `crates/installer/nordisk-twin` — a box's twin over mutual TLS 1.3 (2026-09-27,
# gunnar agent's word): the primary's own replication issuer, certify, pin, dial.
# Pairing's neighbour because the installer CA is bootstrap only and THIS is what
# the boxes speak afterwards. holger consumes it first; gunnar adopts it itself.
members = [".", "xtask", "crates/skidbladnir-systemd", "crates/Skidbladnir-build-thing", "crates/installer/nordisk-pairing", "crates/installer/nordisk-twin", "crates/installer/plugin-door-wire", "crates/installer/nordisk-narrate"]
# Lean by design (like nornir-testmatrix): a leaf consumer can pull the framework
# without nornir's heavy deps. The pure-Rust offline core (pack · archive ·
# layout · orchestrate · events) needs only the always-on deps below. Heavier or
# optional capabilities (PKI keygen, the znippy archive engine, the
# Redfish/Terraform/KVM backends) sit behind features so the core stays lean and
# always builds offline.
[dependencies]
# The systemd lifecycle (lane B4): its own crate, re-exported here at the old
# paths (`systemd`, `service`, `dropin`, `running`, `uninstall`, `identity`).
skidbladnir-systemd = { path = "crates/skidbladnir-systemd", version = "0.1.0" }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# RON — the declarative-flow description language for the orchestration layer
# (`src/orchestration.rs`): a `.ron` `[Step, …]` provisioning flow the interpreter
# runs. Pure-Rust, offline, serde-native (no egress) so the lean airgap core stays
# offline-safe. Same RON-as-description shape the constellation uses for `.runa.ron`.
ron = "0.12"
# Archive engine (tar + zstd) — the real default `Archive` impl. znippy (content
# cache + delta + read-without-decompress) lands behind the `znippy` feature; the
# tar+zstd path is the offline fallback that always works.
tar = "0.4"
zstd = "0.14"
# Content-addressed verify-on-receipt (the airgap law): the per-entry manifest +
# the SHA-256 integrity root (`root_sha256`) that the ed25519 bundle signature
# (feature `pki`) attests for authenticity.
sha2 = "0.11"
# The ONE shared content-hash primitive (edda/crates/nornir-hash) — `sha256_hex`
# / `sha256_file` / `Sha256Hasher` / `hex_encode`. Skidbladnir's `archive` module
# CONSUMES it instead of hand-rolling the same `Sha256::digest` + `{b:02x}` hex
# twin (L5 dedup). Dep-light (sha2-only, no egress) so the airgap core stays lean.
nornir-hash = { path = "../edda/crates/nornir-hash", version = "0.1.0" }
# blake3 — the constellation's NATIVE integrity hash (znippy's per-chunk checksum;
# korp-installer's kernel pin). The airgap-rust `toolchain.pins.json` records BOTH
# sha256 (matches rustup's published `.sha256`) AND blake3 per bundle member — the
# "pinned like the kernel" table (`edda/crates/korp-installer/src/kernel.rs`). Same
# 1.8.5 the znippy stack already vendors; pure-Rust, no egress, so the pin writer
# stays offline-testable in the lean core (no `znippy` feature needed to WRITE pins).
blake3 = "1.8.5"
# The prod-build fallback for `functional_status`/`surface_status!` (stinky #28):
# without `testmatrix` every surface verdict is a `tracing` event (RED = WARN),
# so a RED surface never vanishes. Lean (no default features, no proc-macros) and
# zero-cost when no subscriber is installed.
tracing = { version = "0.1", default-features = false, features = ["std"] }
# The app-agnostic APP-ALIVE liveness+capability contract (edda/crates/app-heartbeat) —
# the socket layer's `HeartbeatProbe` reads it to turn a node's console into a Verdict +
# one test per declared function. Optional, pulled by the `sockets` feature.
app-heartbeat = { path = "../edda/crates/app-heartbeat", version = "0.1.0", optional = true }
# The ONE shared markup-escaping primitive (edda/crates/nornir-escape) — `xml`
# (`& < > "`). The MSI WXS renderer CONSUMES it instead of hand-rolling the same
# `.replace()` chain twin (`fn xml_escape`, dup-audit B2). Dep-light (std-only,
# no egress) so the airgap core stays lean.
nornir-escape = { path = "../edda/crates/nornir-escape", version = "0.1.0" }
# Scratch-dir for verify-on-receipt (extract-and-rehash) and test fixtures.
tempfile = "3"
# `watch-refresh` feature only — read git `HEAD` for the watch-refresh poll through
# gix (pure-Rust, ZERO-SHELL: no `git rev-parse` subprocess). Lean leaf: no network,
# no working-tree status — just open the repo + peel HEAD, so `default-features = false`
# + `revision`/`sha1` (matches the workspace's lean gix leaf). OFF by default so the
# core (and every product that doesn't wire `--watch-refresh`) never compiles gix.
gix = { version = "0.88", optional = true, default-features = false, features = ["max-performance-safe", "revision", "sha1"] }
# `znippy` feature only — the REAL znippy stack (../znippy), so Skidbladnir's
# airgap bundle IS the one canonical znippy archive (Arrow-IPC index, blake3
# chunk integrity, DuckDB/Polars/`znippy`-CLI readable), not a bespoke twin.
# `znippy-compress` = the parallel seal path (`compress_dir`); `znippy-common` =
# the reader/decompress + random-access `get_file` + `verify_archive_integrity`.
# OFF by default (pulls arrow/openzl/gatling); the lean offline core stays on the
# always-Rust `TarZstd` fallback. Path+version so local dev and publish resolve.
znippy-compress = { version = "0.10.0", path = "../znippy/znippy-compress", optional = true }
znippy-common = { version = "0.10.0", path = "../znippy/znippy-common", optional = true }
# The ONE OCI engine (draupnir) — route the WiX-in-Wine MSI build through
# `run_to_completion_with_binds` instead of shelling `podman run -v` (dup-audit
# P1, zero-shell, single bollard engine). Optional + default-off so the lean core
# never pulls bollard/tokio; enabled by the `msi-oci` feature.
draupnir = { path = "../draupnir", version = "0.2", default-features = false, optional = true }
# The ONE job/container/VM manager (edda/crates/jera) — the durable job ledger over
# draupnir's ONE OCI/KVM engine. Skidbladnir's `machine` module spawns containers/VMs
# straight to `podman`/`qemu` (jera-dedup audit's worst offender); the `jera` feature
# begins routing those through jera ADDITIVELY: a swap-ready seam that classifies a
# `MachineSpec` into a `jera::JobPolicy` and maps the container case onto a
# `jera::container::ContainerSpec` (→ `draupnir::BootSpec`), keeping the direct
# `Command` path as the untouched fallback. Default-off + `default-features = false`
# so the lean core/embedded client never pulls jera's redb/tokio ledger; only
# server-side orchestration opts in. Path+version so local dev and publish resolve.
jera = { path = "../edda/crates/jera", version = "0.2.1", default-features = false, optional = true }
# Version-date stamps for the software dir (`software/<YYYYMMDD>/<product>`) and
# airgap_event timestamps.
chrono = { version = "0.4", default-features = false, features = ["clock"] }
# Install identity (`identity` module): a fresh, time-ordered UUIDv7 `install_id`
# per install (reinstall timeline), a BLAKE3-derived, reinstall-stable `machine_fp`
# (systemd `sd_id128_get_machine_app_specific` pattern), and hex encoding for the
# fingerprint + signature wire forms. All pure-Rust, always-on (identity is minted
# on every install, so it stays in the lean core).
uuid = { version = "1", features = ["v7"] }
hex = "0.4"
# Pure-Rust self-signed per-machine PKI (AIRGAP4 `SelfGenerated`). Seed the
# keypair from `getrandom` (OS CSPRNG) to avoid coupling to a rand_core RNG trait.
ed25519-dalek = { version = "3", optional = true }
getrandom = { version = "0.4", optional = true }
# Real X.509 generation: rcgen issues a genuine Ed25519 self-signed CA + SAN leaf
# certs that rustls actually parses/loads (the bespoke hand-rolled cert it
# replaces could not). `ring` backend (not `aws_lc_rs`) keeps it pure-Rust with no
# C/cmake build, matching the workspace's rustls-via-ring posture.
# HELD at rcgen 0.13 (2026-10-02, W3 crypto bump): rcgen 0.14 re-encodes the SEEDED
# machine CA differently (KeyUsage bit-string length, extension order) — same key,
# same subject, different certificate bytes, which `pki_kat` refuses: a re-built CA
# must be the SAME CA. rcgen shares no traits with the RustCrypto set (the PKCS#8 is
# built here from the seed), so holding it costs nothing. Move deliberately, with a
# re-pinned fixture, if a changed CA encoding is ever acceptable.
rcgen = { version = "0.14", default-features = false, features = ["pem", "ring"], optional = true }
# Parse + cryptographically verify the issued X.509 leaf against its CA (the
# `verify` feature uses ring, already in the tree). Backs `verify_leaf`.
x509-parser = { version = "0.18", default-features = false, features = ["verify"], optional = true }
# Name the PKCS#8 DER type rcgen consumes when rebuilding the CA keypair from a
# deterministic seed (`from_seed`); already transitive via rustls.
rustls-pki-types = { version = "1", optional = true }
# rcgen validity windows are `time::OffsetDateTime`; convert our epoch-seconds.
time = { version = "0.3", default-features = false, optional = true }
# External-integration HTTP client (Vault PKI + Redfish BMC). Blocking client so
# the airgap core stays sync (no tokio runtime); rustls so there is no OpenSSL/C
# build dep — matches the workspace's gix `blocking-http-transport-reqwest-rust-tls`.
# Optional + only pulled in by the `vault`/`redfish` features → the lean offline
# core never compiles reqwest.
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls-no-provider"], optional = true }
# reqwest 0.13 has no bundled roots and its `rustls` feature means aws-lc-rs (C) + the
# OS trust store; ring + the Mozilla bundle keep 0.12 `rustls-tls` behaviour (see the
# `tls_roots` helpers). 2026-10-02.
rustls = { version = "0.23", default-features = false, features = ["ring", "std"], optional = true }
webpki-root-certs = { version = "1", optional = true }
# Base64 for Vault token / Redfish HTTP Basic auth headers.
base64 = { version = "0.23", optional = true }
# `kubernetes` feature only — the async applier for the k8s-unfold target. kube +
# tokio are async/network, so they are OPTIONAL and gated: the lean sync core
# never compiles them. rustls-tls (no OpenSSL/C) matches the workspace posture.
# kube 0.99 -> 4.2, k8s-openapi 0.24 -> 0.28 (2026-10-02, W6): API level stays `latest`;
# TLS stays rustls-on-ring (no aws-lc / OpenSSL pulled in — checked with cargo tree).
kube = { version = "4.2", default-features = false, features = ["client", "runtime", "rustls-tls"], optional = true }
k8s-openapi = { version = "0.28", features = ["latest"], optional = true }
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "time", "net", "sync"], optional = true }
serde_yaml = { version = "0.9", optional = true }
# `promote-k8s` only — Helm 3 stores each release revision in a Secret whose
# `release` field is base64 around a GZIP stream around JSON. Reading it with
# flate2 is what lets `stored_release_values` answer what `helm get values` would
# answer WITHOUT a helm binary on PATH and without a subprocess carrying the
# slot's secrets in its stdout — i.e. runnable as a plain Kubernetes Job.
# `rust_backend`: no zlib to link into a distroless image.
flate2 = { version = "1", default-features = false, features = ["rust_backend"], optional = true }
# `talos` feature only — the apid gRPC push (machine.MachineService). tonic +
# prost speak the Talos machine API protobuf directly (no `talosctl`, no `protoc`
# in the build: the messages are hand-written prost). `tls-ring` keeps it
# pure-Rust (rustls/ring, no OpenSSL/C), matching the workspace posture; tokio +
# tokio-stream drive the async transport. All OPTIONAL + gated so the lean sync
# core never compiles gRPC.
# tonic 0.14 (was 0.13, bumped 2026-08-23). 0.14 moved prost codegen into
# `tonic-prost`, so the `prost` FEATURE is gone and the codec is its own optional
# dependency. One tonic across the constellation — two majors in one graph make
# `Routes` and `ServerTlsConfig` distinct types.
tonic = { version = "0.14", default-features = false, features = ["transport", "router", "codegen", "tls-ring"], optional = true }
tonic-prost = { version = "0.14", optional = true }
prost = { version = "0.14", optional = true }
tokio-stream = { version = "0.1", default-features = false, features = ["net"], optional = true }
# Pure-Rust D-Bus (no libdbus, no `systemctl` subprocess) → talk to systemd's
# org.freedesktop.systemd1 Manager directly (start/stop/enable/disable/reload/
# reset-failed/is-active). The 100%-Rust replacement for shelling `systemctl`.
# 6 (was 5, 2026-10-19): RUSTSEC-2026-0370..0372 — zbus 6 absorbs zbus_names/
# zvariant/zvariant_derive; this crate only ever used the `zbus::zvariant`
# re-export, so the module paths stay.
zbus = "6"
# SIGTERM the qemu PID directly (`libc::kill`) instead of shelling `kill` —
# only the `kvm` MachineController needs it, so it's optional + feature-gated.
libc = { version = "0.2", optional = true }
# FAT12/16 authoring is NO LONGER owned here — the cloud-init NoCloud **seed**
# (label CIDATA) and the matrix **payload** (label SKIDDATA) images are authored by
# the ONE seed builder `draupnir::seed::{build_seed_image,build_fat_image}` (feature
# `draupnir/seed`, pulled by `kvm`), not a twinned `fatfs` call here (dup-audit L5).
# `fatfs` survives only as a test dev-dep, to READ BACK the image draupnir authored.
# `testmatrix` — routes this crate's `functional_status(component, check, ok,
# detail)` markers into the nornir introspection warehouse (the constellation
# coverage gate). OFF by default: `functional_status` is a compiled-out `#[inline]`
# no-op with NO nornir dep, so the lean offline core stays dep-free. Path dep to
# the sibling nornir repo (siblings under .../git); lean itself (serde-only), so
# no dependency cycle back into Skidbladnir. Mirrors ordning-core / korp-collectors.
# PATH added 2026-08-01: `tests/silenced_tests_guard.rs` needs the `gatedtests`
# module, which exists only in edda's unpublished 0.2.8, and cargo requires ONE
# canonical source path for a package across [dependencies] and
# [dev-dependencies] ("different source paths depending on the build target").
# `version` is kept so `cargo publish` still resolves it from the registry for
# external consumers; locally the sibling path wins. Same path+version shape the
# `gatling` dev-dep below already uses.
nornir-testmatrix = { path = "../edda/crates/nornir-testmatrix", version = "0.2.10", default-features = false, optional = true }
# `federation` feature only — the upstream-registry FEDERATION ("the ship").
# SYNC, runtime-agnostic HTTP over rustls/ring (NO OpenSSL, NO aws-lc-rs/C/cmake —
# the pure-Rust charter), so the harbour clients (Artifactory · Nexus · Azure ·
# AWS CodeArtifact) are safe from an async handler and testable offline. Optional
# + gated → the lean airgap core (and holger's minimal install) never compiles it.
# 3.4.1 is the floor: 3.3.0 let the response-head deadline run on into the body
# and cut a slow download (ureq #1194; federation::tests::a_slow_but_moving_*).
ureq = { version = "3.4.1", default-features = false, features = ["rustls"], optional = true }
[features]
# `pki` (real ed25519 SelfGenerated certs) is ON by default — it is pure-Rust and
# offline-safe. The external backends stay OFF (trait seams only).
default = ["pki"]
pki = ["ed25519-dalek", "getrandom", "skidbladnir-systemd/pki", "dep:rcgen", "dep:x509-parser", "dep:rustls-pki-types", "dep:time"]
# External integrations live behind features so the lean offline core never pulls
# reqwest/podman/terraform. Each is a REAL call when its feature is on.
# - `vault` → Vault PKI HTTP API (issue leaf via `POST /v1/pki/issue/<role>`).
# - `redfish` → BMC provisioning over Redfish REST (power/boot/virtual-media).
# - `terraform` → drive the `terraform` binary (init/plan/apply) — std::process.
# - `znippy` → the REAL znippy-core archive engine (seal/read/verify/unpack via
# ../znippy: znippy-compress + znippy-common); off by default.
vault = ["dep:reqwest", "dep:rustls", "dep:webpki-root-certs", "dep:base64", "pki"]
redfish = ["dep:reqwest", "dep:rustls", "dep:webpki-root-certs", "dep:base64"]
# - `openbao` → provision the OpenBao secret store's STRUCTURE (k8s auth method,
# per-env read policy + role) over the REST API. The platform's
# half of the secret split; values are written by tillsynia-admin.
openbao = ["dep:reqwest", "dep:rustls", "dep:webpki-root-certs"]
# `download` → the CONNECTED side's resumable HTTP fetch (src/download.rs):
# `Range: bytes=N-` resume, 206-vs-200 restart, verify-on-partial, atomic rename.
# The complement of the zero-network `fetch` gate — it runs where bundles are
# ASSEMBLED, never inside the gap. Carries NO URLs (pins are the consumer's).
# Same blocking-reqwest/rustls stack the other integration features use.
download = ["dep:reqwest", "dep:rustls", "dep:webpki-root-certs"]
terraform = []
# `msi-oci` → build MSIs (WiX v4 under Wine) through draupnir's ONE OCI engine
# (`ContainerBoot::run_to_completion_with_binds`, `-v host:/work` bind). This is
# the ONLY container path — the old `podman run -v` subprocess was DELETED under
# the zero-shell law (dup-audit P1). OFF by default so the lean core never pulls
# draupnir's bollard/tokio; with the feature off the build renders the WXS for real
# then bails HONESTLY (no shell, no fake MSI). `cargo build --features msi-oci`.
msi-oci = ["dep:draupnir", "draupnir/backend-oci"]
# `pack-oci` → the container dep-capture build in `pack.rs` (`PodmanBuild`) drives
# draupnir's ONE OCI engine (`ContainerBoot::build_image` + `extract_path`). This is
# the ONLY container path — the old `podman build` + `podman create`/`cp` subprocess
# trio was DELETED under the zero-shell law (dup-audit P1, the twin draupnir's
# `build_image`/`extract_path` were added to kill). OFF by default so the lean core
# never pulls draupnir's bollard/tokio; with the feature off the build renders the
# Containerfile for real then bails HONESTLY (no shell). `cargo build --features pack-oci`.
pack-oci = ["dep:draupnir", "draupnir/backend-oci"]
# `oci` → convenience umbrella: route BOTH the MSI (WiX/Wine) build and the pack
# dep-capture build through the one draupnir OCI engine.
oci = ["msi-oci", "pack-oci"]
# `service-drive` → the `service` BUILD-THING KIND's drivable seam (`src/service_drive.rs`):
# install a unit, start it, wait for a NAMED READINESS MARKER the service printed to its
# `StandardOutput=append:` file, ask it a real question, tear it down. The systemd twin of
# draupnir's `exe` backend, and it lives HERE because line 87 above already points
# Skidbladnir → draupnir: cargo forbids the reverse arrow even through an optional dep, so
# a systemd backend inside draupnir is structurally impossible (MEASURED 2026-08-15,
# `edda/crates/nornir-testmatrix/tests/chain_matrix.rs:190` — 12 of 44 chain-matrix rows).
#
# The dep is the LEAN draupnir surface only (`default-features = false`: pure std + gatling,
# no bollard/tokio/fatfs) and it buys exactly one thing — `draupnir::Seen`, the ONE stage-2
# observation vocabulary. Twinning that enum here would be ~60 duplicated lines and two
# definitions of "seen" that drift apart, which is the twin this constellation keeps paying
# for. OFF by default because it is the only reason the lean core would pull draupnir at all
# (the three features above pull it for a heavy backend a lean consumer already opts out of).
# Rescued from the silenced-test guard by an arm in `.nornir/testmatrix-arms.json`.
service-drive = ["dep:draupnir"]
# Server-side machine lifecycle control (the `machine` module): the
# MachineController trait + MachineSpec/Handle + the content-addressed ImageCache.
# Pure-Rust + lean (sha2 + std only) but gated so the embedded CLIENT (which runs
# ON the target device) never pulls it — only the server-side airgap orchestration
# does. The actual hypervisor/runtime drives are the two sub-features below.
machine = []
# `kvm` = the qemu/KVM MachineController (real init/PID1, airgap egress isolation).
# Adapts unfold::kvm. Implies `machine` (the trait it implements). Pulls draupnir's
# ONE seed builder (`draupnir/seed`: pure-Rust FAT authoring for the cloud-init
# NoCloud seed + the matrix payload images — no twin here, dup-audit L5) and
# `base64` for decoding the qemu-guest-agent `guest-exec` output (server-side test).
kvm = ["machine", "dep:libc", "dep:draupnir", "draupnir/seed", "draupnir/backend-tunnr", "dep:base64"]
# `container` = the podman/docker MachineController (fast, stateless tests).
container = ["machine"]
# `jera` = route the `machine` module's container/VM spawns through jera (edda) — the
# ONE job/container/VM manager — instead of raw `podman`/`qemu` `Command`. THIS FIRST
# STEP is additive + swap-ready: it adds a `MachineSpec` → `jera::JobPolicy` classifier
# and a `MachineSpec` → `jera::container::ContainerSpec` (→ `draupnir::BootSpec`) mapper
# plus an honest `jera_unsupported()` gap-list, WITHOUT touching the working direct
# `Command` path (kept as the fallback). Implies `machine` (the trait the seam maps
# from). Pulls jera's LEAN default surface only (policy + container mapping always
# compile; no bollard) — the live `DraupnirContainerBootRunner`/`run_container` engine
# lands later behind `jera/container-oci`. See `.nornir/machine-jera-routing.md`.
jera = ["machine", "dep:jera", "jera/runners"]
# The live multi-OS install matrix driver (boots KVM guests, applies the rendered
# unit, asserts via the injected probe). Heavy/server-side; implies `kvm`.
matrix = ["kvm"]
znippy = ["dep:znippy-compress", "dep:znippy-common"]
# `kubernetes` → async applier (src/k8s_apply.rs) that drives the sync k8s
# renderer's manifests over the real k8s API (server-side apply / scale /
# rollout-wait) — the systemd-over-D-Bus analog for Kubernetes, no `kubectl`.
# Gated so the core stays sync + dep-free when off.
kubernetes = ["dep:kube", "dep:k8s-openapi", "dep:tokio", "dep:serde_yaml"]
# `talos` → the Talos bare-metal rebirth target (B5), downstream of the libvirt
# ISO boot. The machineconfig GENERATOR is pure/sync in the core; only the async
# apid gRPC push (ApplyConfiguration/Bootstrap) needs a runtime, so it is gated
# here. No `talosctl` shell — tonic + hand-written prost speak the machine API.
talos = ["dep:tokio", "dep:tonic", "dep:prost", "dep:tonic-prost", "dep:tokio-stream"]
# `testmatrix` — turn the `functional_status` emit markers into real nornir matrix
# rows (pulls the lean serde-only nornir-testmatrix crate + its own `testmatrix`
# mode). OFF by default so the emit compiles out.
testmatrix = ["dep:nornir-testmatrix", "nornir-testmatrix/testmatrix", "skidbladnir-systemd/testmatrix"]
# `federation` — the upstream-registry FEDERATION library ("the ship"): the
# `UpstreamRegistry` trait + the Artifactory and Nexus harbour clients, over a
# pure-Rust sync `ureq`/rustls stack (no C toolchain). OFF by default so the lean
# airgap core never links an HTTP client for this. Consumers (holger's `full`
# tier) opt in.
federation = ["dep:ureq", "dep:base64"]
# The two cloud harbours. Each is pure-Rust HTTP over the documented endpoint —
# they add only the module (no heavy async SDK, no aws-lc-rs/C), so they simply
# imply `federation`.
# - `upstream-azure` → Azure Artifacts feeds (Azure DevOps), PAT/AAD auth.
# - `upstream-aws` → AWS CodeArtifact repository endpoints, bearer-token auth.
upstream-azure = ["federation"]
upstream-aws = ["federation"]
# `sockets` — the Build Thing bulb→SOCKET deployment/topology system (`src/socket.rs`):
# the Socket/Fixture/NetworkBackend/Probe trait seams, `Mode { Test, Prod }`, the
# BuildThingType→socket registry, the `TunnrTestSocket` fixture composition, and the
# WireGuard keys-panel data model (base64 pubkey validation → `dep:base64`). Additive +
# OFF by default so the shipping install/PKI core never links it. See
# `.nornir/socket-deployment.md` (authoritative design in workspace_nordisk/.nornir).
sockets = ["dep:base64", "dep:app-heartbeat"]
# `promote` — the PROMOTE module (`src/promote/`): the public, versioned
# environment-ladder SCHEMA (sites · declared roles · guard · promotion-DAG edges ·
# consumer `meta`), the slot registry whose fence a config file can raise but never
# lower, the two-key prod guard + its `Cleared` capability token, blue/green
# generations, the `AppData` seam (behind which every application-specific payload
# fact stays), the payload-agnostic bundle envelope, and the serde topology a UI
# panel renders. LEAN: the only new dep is serde_yaml (pure Rust, already in the
# tree) so a ladder file can be `slot.yaml`; the async traits use a std
# `Pin<Box<dyn Future>>`, so NO async runtime enters the core. OFF by default —
# with the feature off not one byte of the existing build changes.
promote = ["dep:serde_yaml"]
# `promote-k8s` — the live half: release operations (set-image · wait rollout ·
# roll back · scale · server-side apply · ingress switch) over kube-rs, plus
# `stored_release_values`, which reads a Helm release's stored values straight out
# of its `sh.helm.release.v1.*` Secret (base64 → gzip → JSON) so the check needs no
# helm binary and no subprocess. Implies `kubernetes` (kube + tokio + serde_yaml);
# adds flate2 + base64. NEVER shells out — see `promote::release`'s module docs for
# the honest statement about what a Helm chart path would still require.
promote-k8s = ["promote", "kubernetes", "dep:flate2", "dep:base64"]
# `watch-refresh` — the gix-backed HEAD reader (`watch::GitHead`) behind the
# "always-fresh oden" verb. The whole poll/diff/stamp DECISION + the timer/oneshot
# renderers compile in the lean core WITHOUT this feature (HEAD is an injected seam);
# turning it on adds only the ONE production `HeadSource` impl that reads HEAD via gix.
# ADDITIVE + OFF by default, so korp's existing (path-dep) build is unchanged — a
# product opts in when it wires `--watch-refresh`.
watch-refresh = ["dep:gix"]
[dev-dependencies]
# ROOT-LAW-#0: the ONLY sanctioned home for a raw `std::thread::spawn` is the
# gatling crate (znippy-zoomies). The test-only mock HTTP servers (mockhttp,
# federation::testutil, service::spawn_oneshot_http) run a long-lived loopback
# accept-loop on a background thread; they route that ONE background thread
# through `gatling::background::Job` instead of a bare `std::thread::spawn`, so
# all threading in the constellation still originates in gatling. Dev-only: never
# linked into the shipped lean core. Published as `rotaryengine` (lib name
# `gatling`); path+version so local dev and a future publish both resolve.
gatling = { package = "rotaryengine", version = "0.1.11", path = "../znippy-zoomies/gatling" }
# Pure-Rust HTTP (rustls, no C, no `curl` subprocess) for the heavy real-download
# delta proof. Reuses the workspace-vendored ureq.
ureq = { version = "3.4.1", default-features = false, features = ["rustls"] }
# READ-BACK only: the seed/payload FAT images are now authored by draupnir's owned
# builder; `fatfs` here just re-opens what draupnir wrote to prove the round-trip
# (label + files survive), so cloud-init will find them in the guest.
fatfs = "0.3"
# Prove the rcgen-issued leaf actually LOADS into a real rustls ServerConfig (the
# whole point of moving to X.509). `ring` provider only — no aws-lc/C build.
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls-pemfile = "2"
# The SILENCED-TEST guard (`tests/silenced_tests_guard.rs`): edda's
# `nornir_testmatrix::gatedtests::audit_repo`, which fails this repo's own
# `cargo test` the moment a `tests/*.rs` goes dark behind an off-by-default
# feature. PATH, and deliberately NO `version` — crates.io's newest
# nornir-testmatrix (0.2.7) does not contain the `gatedtests` module at all; the
# guard exists only in edda's unpublished 0.2.8, so a version requirement here
# would be a claim the registry cannot honour. Dev-only + path-only, so
# `cargo publish` strips it and no consumer ever sees this edge. (The optional
# [dependencies] `nornir-testmatrix` above is the separate, published,
# functional-status emit sink; it keeps its registry `version`.)
nornir-testmatrix = { path = "../edda/crates/nornir-testmatrix", default-features = false }
# The DECLARED-RUN guard (`tests/gated_runs_are_declared_guard.rs`) reads this
# repo's own build space (`.nornir/build-variants.toml`) through the ONE shared
# model — `nornir_build_thing::variant::BuildVariantSet` — rather than parsing the
# file a second time next to it (LAW 5). PATH + no `version`, same edge as the
# `nornir-testmatrix` dev-dep above: dev-only, so `cargo publish` strips it. Its
# own optional `skidbladnir` dependency is registry-versioned and OFF, so nothing
# here forms a path cycle.
# `version` ADDED 2026-08-30 (lane AJ): a path dep with no version at all is the
# float with nowhere to read it — the manifest made no claim about which reader
# this guard runs, while compiling whatever edda's tree happened to be.
nornir-build-thing = { path = "../edda/crates/nornir-build-thing", version = "0.2.0", default-features = false }
# Reading `brokkr.toml`'s `run = { variant = "…" }` step targets. Only the dwarves
# forge OWNS that schema; the guard reads the two keys it needs as plain TOML
# instead of dev-depending on dwarves, which depends on THIS crate.
toml = "1"
# Live, idempotent OpenBao k8s-auth turn-on (structure half of the secret split).
[[example]]
name = "openbao-provision"
required-features = ["openbao"]
# N5 — Skidbladnir-driven system install of the knut Arrow Flight server: stage the
# binary → TarZstd bundle → SystemdInstaller (verify/unpack/unit/enable+start). Uses
# only the always-on lean core (archive + layout + product), so no required-features.
[[example]]
name = "install-knut-flight"
path = "examples/install-knut-flight.rs"
# N5 — render the knut-flight k8s manifests through the ONE Kubernetes renderer
# (`Runtime=Kube`), from the SAME ComponentSpec as the systemd install. Lean core.
[[example]]
name = "render-knut-flight-k8s"
path = "examples/render-knut-flight-k8s.rs"
# The VM-systemd-suite bencher (bencher law: own `main`, `harness = false`, printed
# table, exit-1 gate). DECLARED here on purpose — an undeclared `benches/*.rs` is a
# file `cargo bench` never builds and nothing ever runs, which this session found
# three times elsewhere in the constellation.
#
# Its gate is AMORTISATION, not speed: boot time is real time, so the property that
# decides whether the suite stays affordable is "many cases per boot, not one boot per
# case" (`cases_per_boot >= 4`) — computable from the plan's shape without booting, so
# the non-heavy leg runs anywhere. `SKID_BENCH_LIVE=1` adds the real two-boot run and
# reports per-case + per-boot wall-clock.
# `required-features = ["matrix"]`: the suite lives behind the same gate as the module.
[[bench]]
name = "vm_systemd_suite"
harness = false
required-features = ["matrix"]
# Minimal CLI runner: `skidbladnir run <flow.ron>` drives the declarative RON
# orchestration layer (`src/orchestration.rs`) — parse a `Flow`, run it on
# `Registry::native_default()`, print each `StepOutcome`. NO required-features: the
# orchestration interpreter lives in the always-on lean core, so the bin builds with
# just the default (`pki`) surface. Thin driver, adds no orchestration logic itself.
[[bin]]
name = "skidbladnir"
path = "src/bin/skidbladnir.rs"
# ── wg-appliance-core is ONE crate, whoever reaches it ───────────────────────
# `nordisk-pairing --features mullvad` takes `tunnr-mullvad` from crates.io, and
# tunnr-mullvad builds its `TunnelSpec` from ITS `wg-appliance-core`. Without this
# patch that would be the registry copy — a different crate from edda's, and two
# `TunnelSpec` types. Same patch edda's workspace carries (moved with the crate).
[patch.crates-io]
wg-appliance-core = { path = "../edda/crates/wg-appliance-core" }