Gunnar

nordisk/holger

.nornir/build-thing-design.md

# holger build thing — the reasoning behind every row

**What this file is.** Until 2026-09-15 holger's `.nornir/build-variants.toml` was
hand-written, and the argument for each row lived as comments between the rows. The
declaration is Rust now (`holger-build-thing/src/recipe.rs`) and the toml is emitted
from it — an emission carries data, not argument, so the next `emit` would have deleted
every comment below without anybody noticing. They are kept here verbatim, as the last
hand-written file, so the measured closures, the renames and the rulings survive.

Paths named below have moved: `build-space/tests/*` and `holger-build/tests/*` are
`holger-build-thing/tests/*`; edda's `nornir-buildspec-holger` is
`holger-build-thing/src/rules.rs`. The rows themselves may have moved on since; the
recipe is what is true.

```toml
# holger's build-space — the deployment EXECUTABLES and the CLIENTS as data (task
# #36, the first consumer of nornir-build-thing). One source tree
# (`server/cli/src/main.rs`), built three ways, each producing ITS OWN distinctly-
# named binary from the real `holger` crate + its real Cargo
# deployment-mode features; plus the two client binaries a human and a script
# actually run. See `.nornir/holger-tiers.md` (embedded · mini · full).
#
#   embedded — the tiniest airgap/edge executable → bin `holger-embedded`. The
#              in-process engine driven as a standalone binary: `embedded` mode,
#              NO federation, provenance-verify on (`sign` is baked into the CLI's
#              server-lib dep). The leanest, zero-federation build.
#   mini     — the stripped, read-only standalone server for airgap/red-zone →
#              bin `holger-mikro` (mikro). `mini` mode: minimal serve core, NO
#              federation, NO Skidbladnir/cloud client linked.
#   full     — the writable everything-on server → bin `holger-full`. `full` mode
#              is the ONLY tier that compiles in the upstream-registry federation
#              (the Skidbladnir ship: Artifactory/Nexus/Azure/AWS) + proxy/import
#              + the full RON config graph.
#   mvp-server — the `mini` read-only core PLUS the separable Secure Update
#              Gateway (`secure_update`) → bin `holger-mvp`. Zero-egress.
#   server-local — bin `holger-server`: the crate's DEFAULT build (the only
#              `[[bin]]` with no `required-features`), the local operator CLI the
#              systemd unit stages. Was declared NOWHERE until 2026-08-01.
#   ui       — the egui registry/artifact browser (`server/ui`), bin `holger-server-ui`.
#              The client a human runs, and THIN: every pane works over gRPC
#              against a RUNNING holger.
#   ui-embedded — the deliberate exception, bin `holger-server-ui-embedded`. `ui` plus
#              the three panes that open state IN THE UI PROCESS (--demo, the
#              warehouse advisory scan, the holger-ds gateway bench).
#   ui-mini  — the read-only client for the `mini` server, bin
#              `holger-server-ui-mini`. Six read tabs; no signin/tools/demo/helix panes,
#              therefore no reqwest, no ureq, no skade.
#   ui-mikro — the LEANEST client, bin `holger-server-ui-mikro`. Three tabs
#              (Status · Repos · Artifact) and no About card.
#   agent    — the OFFLINE agent CLI (`agent/cli`), bin `holger-agent`, built
#              `--no-default-features`: the zero-egress archive/directory courier.
#   agent-online — the same CLI with the `online` feature, bin `holger-agent-online`:
#              the dev tier that reaches Nexus/Artifactory/crates.io·PyPI over reqwest.
#   mvp-bundle / mvp-verify — the Secure Update Gateway operator CLIs
#              (`holger-bundle` / `holger-verify`), role `client` since 2026-08-01
#              (they SHIP; `Role::Tool` means build-support, not product).
#
# Each tier carries an ARTIFACT KIND (nornir-build-thing's third build axis,
# orthogonal to the fat/thin `linkage`): `embedded` is a plain `exe` (the leanest
# embeddable engine binary — NOT a system-managed daemon), while `mini`/`full` are
# `service` kinds (a nordisk suite member with a `member` identity — they ALSO wrap
# as a skidbladnir ServiceSpec: dedicated system user + unit). The kind drives the
# unified `assemble()` seam: a chosen tier dispatches on its `kind` to the SAME
# cargo builder + emits a build-info lineage per tier (no hand-rolled build loop).
#
# ══════════════════════════════════════════════════════════════════════════════
# ⚠ TWO CORRECTIONS THIS FILE CARRIES (audited against Cargo.toml, 2026-07-22)
# ══════════════════════════════════════════════════════════════════════════════
#
# 1. THE TWINS ARE GONE. This file used to declare `holger` and `holger-thin`
#    ALONGSIDE `full` and `mini`. `holger` was `full` plus `embeds = ["skade"]`;
#    `holger-thin` was `mini` plus `reaches = ["skade"]` and a different bin name.
#    Two entries differing from an existing entry by one field are a twin, and they
#    made the menu read as five servers when holger ships three. The linkage now
#    rides ON the tier that already existed, which is also what holger's own build
#    thing (edda/crates/nornir-buildspec-holger) declares.
#
# 2. holger HAS NO skade DEPENDENCY — so the server tiers' fat claim had nothing
#    behind it. Searching every Cargo.toml in this workspace for `skade` finds
#    exactly two hits and both are PROSE in comments. `holger-server-lib` and
#    `holger` do not depend on skade, optionally or otherwise. The old
#    `embeds = ["skade"]` on the fat server was a declaration with no dependency
#    edge under it: the menu said "self-contained, embeds skade" about a binary
#    that had never heard of skade.
#
#    Where skade actually enters this repo is the exact OPPOSITE variant. The GUI's
#    `tools` feature pulls `modgunn` with `features = ["scan", "warehouse"]` — and
#    modgunn's `warehouse = ["dep:skade", …]`. So `ui` is the ONE artifact in this
#    workspace that genuinely compiles skade in, and it is the one that had been
#    described as a thin client.
#
#    Declared from that fact rather than from the names: the three server tiers are
#    `fat` — each opens its own state directly and needs no nordisk sibling running
#    — and they differ from each other on the TIER axis (`exclusive` below), not on
#    the linkage axis. `agent` is `thin`: `holger-agent-lib` depends on the
#    connectors and `reqwest`, not on `server-lib`, so it reaches a running holger
#    over the wire.
#
#    ⚠ This disagreed with `shape_of()` in nornir-buildspec-holger. It no longer
#    does — the plugin was reconciled with the measurement on 2026-07-23 and its
#    tests now assert the measured facts.
#
# ══════════════════════════════════════════════════════════════════════════════
# ⚠ CORRECTION 3 (2026-07-23) — THE CLIENT LADDER IS A LADDER NOW, IN BYTES
# ══════════════════════════════════════════════════════════════════════════════
#
# The `ui-mini` entry below used to carry this warning: "⚠ THIS
# VARIANT CUTS SURFACE, NOT DEPENDENCIES". `server/ui/Cargo.toml` declared
# `mini = ["gui"]`, and a Cargo feature cannot subtract a dependency — so the
# "mini" client linked the IDENTICAL closure as `ui` and merely added tab-
# filtering code. Measured 2026-07-22 (debug), under the bin names of the day
# (`holger-ui*`, renamed to `holger-server-ui*` on 2026-08-01): `holger-ui`
# 1 048 139 472 B vs `holger-ui-mikro` 1 050 137 592 B. The mini artifact was
# 2 MB BIGGER.
#
# That is fixed at the root: every heavy dep in `server/ui/Cargo.toml` is now
# optional and hangs off the ONE pane that needs it (`about-card` · `signin` ·
# `helix` · `tools` · `demo`), and each tier feature (`gui` / `gui-mini` /
# `gui-mikro`) selects only what it can drive. The tiers below name those
# features, so this file and the manifest cannot drift apart silently.
#
# The ladder is guarded by `server/ui/tests/tier_ladder.rs`, which BUILDS all
# three and fails if a tier is not strictly leaner than the one above it — by
# `cargo tree` crate count and by artifact bytes. A declaration is not evidence.
#
# `nornir build --list-variants` shows all five; `nornir build --variant full
# --attest --seal` validates + materializes + runs the right `cargo build`, then
# captures + znippy-seals a build-provenance attestation.
[build]

# ══════════════ THE DELIVERY (utgåva) AXIS — holger ships TWO ═════════════════
#
# A **suite** is an utgåva: what a customer receives as ONE thing. An **atom** is one
# build point. holger is the complicated member — it ships `MVP` (the Secure Update
# Gateway that goes out first) and `ORIGINAL` (the full artifact registry). The other
# four members ship one delivery each, also named ORIGINAL, so
# `cargo xtask build --suite <name>` is ONE command shape across the whole house.
# Settled in `.nornir/SUITES.md` + `workspace_nordisk/.nornir/SUITE-MODEL.md`.
#
# `base_suite = "ORIGINAL"`. Read ORIGINAL as **"the full delivery"**, never as "the
# legacy one" — it is now and the future; the MVP is the subset that ships first.
#
# `suites` is a **LIST**, so an atom may ship in more than one delivery — `agent` does,
# unchanged, in both. Same bin names in both suites on purpose: `holger-server
# --version` reports which delivery it was built from, so the suite is in the version
# string and never in the filename (SUITES.md §Rules).
#
# ── WHY THIS TAGGING, ATOM BY ATOM ────────────────────────────────────────────
# MVP  = mvp-server (`mvp-server` = mini + secure_update — SUITES.md's MVP server row)
#      + mvp-bundle + mvp-verify (the gateway PAIR: author + offline verify)
#      + ui-mini (`gui-mini`, six read tabs — see the resolved disagreement below)
#      + agent (the zero-egress intake side, shared with ORIGINAL)
# ORIGINAL = embedded · mini · full · server-local (the four server tiers)
#      + ui · ui-embedded · ui-mini (the richer UIs, `gui` = 11 tabs read+write)
#      + agent · agent-online
#
# ⚠ RECORDED, NOT DRESSED UP — two disagreements this tagging exposes:
#
# 1. ✅ RESOLVED 2026-08-01 by Rickard: **MVP's ui atom is `ui-mini` (`gui-mini`)**.
#    `holger-build`'s 3×3 matrix was right and SUITES.md's row was not; SUITES.md is
#    corrected to match.
#
#    His reason, and it is the one that settles it: **`mikro` means SUPER mega small,
#    and that is not a requirement at the moment.** So MVP should not pay mikro's cost.
#    Concretely, from the ladder in `server/ui/Cargo.toml`:
#        gui-mini  = gui-core + about-card + mini   → six read tabs
#        gui-mikro = gui-core + mikro               → three tabs, NO about-card
#    `gui-mikro` buys its extra leanness by dropping `about-card`
#    (`dep:facett-about` + `dep:image`) — the identity/branding/version surface. An
#    operator running a Secure Update Gateway wants to see which build they are on, so
#    trading that away for bytes nobody is asking for is the wrong trade today.
#
#    `ui-mikro` is NOT deleted: it stays a declared ORIGINAL atom, so the moment
#    super-small IS a requirement it is one `suites` edit away from shipping in MVP.
# 2. SUITES.md lists an `archive` (`holger-archive`) and a `cli` (`holger-cli`) atom in
#    BOTH deliveries. Neither is declared as a variant anywhere in this file and
#    neither bin exists. They are a real gap in the declaration, not something to
#    invent here: `mvp-bundle`/`mvp-verify` are the closest shipped things and are
#    tagged as themselves. Do not read MVP's five atoms as SUITES.md's five.
base_suite = "ORIGINAL"

# The feature UNIVERSE holger actually offers across the workspace. A variant
# feature outside this set is rejected up front.
#
# ⚠ TWO OF THESE ARE NOT FEATURES OF ANY PACKAGE A VARIANT BUILDS. `sign` is a
# feature of `holger-server-lib`, and `served` of `holger-bench-scenarios` —
# neither exists on `holger` or `holger-server-ui`, so a variant naming either
# would be accepted here and then rejected by cargo. `offers` is the list a reader
# audits against, and it is currently wider than the packages it describes.
#
# ⚠ DECLARED-BUT-NEVER-BUILT: `sign`, `testmatrix` and `served` are enabled by NO
# variant below. `gui` used to be in that list too — which is exactly how the
# `holger-server-ui` bin (`required-features = ["gui"]`) went unbuilt: `cargo build
# --all-targets` SILENTLY SKIPS a required-features target, so nothing ever
# compiled it. The `ui` variant below closes that one.
offers = [
    # ── holger's deployment modes ──
    "embedded", "mini", "full",
    # ── the mvp server tier + its separable Secure Update Gateway capability.
    # `secure_update` gates the offline bundle import / scrub / backup / audit verbs
    # (+ the holger-update-bundle link); `mvp-server` is the composite tier
    # (`mini` + `secure_update`) that backs the `mvp/server` matrix entry.
    "secure_update", "mvp-server",
    # ── the agent's online capability. `online` gates the network connectors
    # (Nexus / Artifactory / crates.io·PyPI `world`) + their reqwest closure on both
    # holger-agent-lib and holger-agent-cli; it is what separates the `dev/agent`
    # tier (bin holger-agent-online) from the offline, zero-egress micro agent.
    "online",
    # ── holger-server-ui's three TIER features (a `[[bin]]` selects exactly one) ──
    "gui", "gui-embedded", "gui-mini", "gui-mikro",
    # ── holger-server-ui's building block + its removable PANES. Named here because a
    # variant may legitimately pin one (e.g. a `gui-mini` + `helix` build), and a
    # feature outside `offers` is rejected up front.
    "gui-core", "about-card", "signin", "helix", "tools", "bench", "warehouse-scan",
    "demo",
    # ── holger-server-ui's SURFACE markers, implied by the tier features above.
    "mikro",
    # ── the appliance identity ceremony (server/cli feature `appliance`: edda's
    # wg-appliance-core — WireGuard identity mint + tunnel, env-gated at runtime
    # by HOLGER_APPLIANCE=1). Carried by the mvp-server atom; in no default.
    "appliance",
    # ⚠ STILL DECLARED-BUT-NEVER-BUILT: `sign` is a feature of `holger-server-lib`
    # and `served` of `holger-bench-scenarios` — neither exists on
    # `holger` or `holger-server-ui`, so a variant naming either is accepted
    # here and then rejected by cargo. `testmatrix` is real on both but no variant
    # enables it.
    "sign", "testmatrix", "served",
]

# A build is exactly ONE fatness — the three deployment modes are mutually
# exclusive (each links a different dependency closure). Selecting two rejects.
# NOTE this is the TIER axis, not the linkage axis: all three tiers are `fat` on
# the nordisk fat/thin axis (none of them needs a sibling running), and what
# separates them is federation + the auth-provider closure.
exclusive = [["embedded", "mini", "full"]]

# Cargo `[[bin]] required-features`, mirrored here so a bad tier/feature combo
# fails fast with a clear message instead of a raw cargo error. Each deployment
# executable requires its own mode feature (see server/cli/Cargo.toml), and
# `holger-server-ui` requires `gui` (server/ui/Cargo.toml:143).
[build.required]
"holger-embedded" = ["embedded"]
"holger-mikro" = ["mini"]
"holger-mvp" = ["mvp-server"]
"holger-agent-online" = ["online"]
"holger-full" = ["full"]
"holger-server-ui" = ["gui"]
"holger-server-ui-embedded" = ["gui-embedded"]
# ⚠ RENAMED TWICE. 2026-07-23: the middle client rung became `-mini` and a new,
# leaner bottom rung took the `-mikro` name — before that ONE client bin called
# `holger-ui-mikro` did the middle rung's job and collided by name with the SERVER
# binary `holger-mikro`, reading as if the two were the same tier. They are not.
# 2026-08-01: `holger-ui*` → `holger-server-ui*`, so the bin says WHICH component
# it drives (`server/cli` is `holger` and builds the daemons these UIs
# talk to). The PACKAGE followed on the same day: `holger-ui` → `holger-server-ui`.
"holger-server-ui-mini" = ["gui-mini"]
"holger-server-ui-mikro" = ["gui-mikro"]

# ── The suite DEPENDENCY GRAPH as an embed-allowlist (the fat/thin service axis —
# ORTHOGONAL to the embedded/mini/full deployment tiers above). Per the nordisk
# suite-build matrix, holger sits DOWNSTREAM of skade ONLY: a fat holger MAY
# compile IN skade and nothing else. Any peer/upstream member (nornir, dwarves) is
# absent, so a fat holger that tried `embeds = ["nornir"]` inverts the graph and is
# rejected by `BuildVariantSet::validate` (the asymmetry rule).
#
# The allowlist stays as the rule it is even though no SERVER variant exercises it
# today — see correction 2 above: the server tiers link no skade, so they embed
# nothing, and the one artifact that does link it (`ui`) is an `exe`, on which
# `validate_kind` does not permit `embeds`.
[build.downstream]
holger = ["skade"]

# ══════════════ role: server — three tiers, one source tree ══════════════════

# ── embedded: the tiniest airgap/edge executable ────────────────────────────
# Kind `exe`: the embeddable engine driven as a standalone binary — the leanest
# tier, NOT a system-managed daemon (so no `member`/ServiceSpec). `embedded` mode
# (no federation); `sign`/provenance-verify rides in via the CLI's server-lib dep.
# Default features off ⇒ the leanest closure, and no `online`/`bench` ⇒ no reqwest,
# no ureq: the provably zero-egress airgap tier.
#
# `linkage = "fat"`: it opens its own storage directly and needs nothing else
# running. It embeds no NORDISK sibling — hence the empty `embeds` — which is a
# different statement from the old `embeds = ["skade"]` fiction.
[[build.variant]]
name = "embedded"
suites = ["ORIGINAL"]
role = "server"
default = false
package = "holger"
bin = "holger-embedded"
kind = "exe"
linkage = "fat"
features = ["embedded"]
no_default_features = true
profile = "release"

# ── mini: stripped read-only standalone server (mikro) ──────────────────────
# Kind `service`: a read-only holger daemon — the suite `holger` member installed
# under its own dedicated system user (skidbladnir ServiceSpec). `mini` mode:
# minimal serve core, no federation, default features off. Airgap tier: no
# outbound HTTP client is linked.
#
# `linkage = "fat"`: a read-only server is still a state OWNER. It needs no other
# service up, which is what fat means — "read-only" is not "thin".
[[build.variant]]
name = "mini"
suites = ["ORIGINAL"]
role = "server"
default = false
package = "holger"
bin = "holger-mikro"
kind = "service"
member = "holger"
linkage = "fat"
# `appliance` (2026-08-13): the identity ceremony + userspace WireGuard (edda's
# wg-appliance-core), compiled in and gated at RUNTIME by HOLGER_APPLIANCE=1 —
# a plain `holger-mikro start` is behaviorally unchanged. Carried HERE (the
# ORIGINAL airgap tier — the tier the ISO shape installs) as well as by
# `mvp-server`, which keeps MVP a subset of ORIGINAL over resolved features
# (`check_suites_resolved`, proven in build-space/tests/suite_build_shape.rs).
# Zero-egress preserved: boringtun/base64ct/rand_core/libc, no HTTP client —
# `airgap_tiers_link_no_outbound_http_client` keeps asserting it on this tier.
features = ["mini", "appliance"]
no_default_features = true
profile = "release"

# ── full: the writable everything-on server (federation on) ─────────────────
# `full` mode turns on the upstream-registry federation (the Skidbladnir ship).
# `no_default_features` drops the default `mini` marker so the three tiers stay
# mutually exclusive in the Cargo graph (a `compile_error!` guard in the
# server-cli binary rejects a `mini` + `full` union at the tier the operator
# selects); the CLI's `full` feature is self-sufficient — it re-enables `online`
# (reqwest+ureq) and `bench` itself. (The guard lives only in the binary, not
# server-lib: a shared-lib guard would misfire under Cargo feature-unification.)
# Kind `service`: the writable holger daemon under its own dedicated system user.
#
# `linkage = "fat"`: no nordisk sibling is compiled in and none is reached over
# gRPC. The federation clients this tier links reach THIRD-PARTY registries
# (Artifactory/Nexus/Azure/AWS), which are not suite members and are not what the
# fat/thin axis is about — `reaches` is deliberately empty rather than stretched to
# cover them, because a `reaches` entry means "a nordisk owner must be running".
[[build.variant]]
name = "full"
suites = ["ORIGINAL"]
role = "server"
# THE NORMAL SERVER: the writable everything-on registry. embedded/mini are the
# airgap exceptions, chosen deliberately.
default = true
package = "holger"
bin = "holger-full"
kind = "service"
member = "holger"
linkage = "fat"
features = ["full"]
no_default_features = true
profile = "release"

# ── mvp-server: the mini read-only core + the Secure Update Gateway ─────────
# The `mvp/server` entry of holger-build's 3×3 matrix, now a REAL feature-selected
# variant. bin `holger-mvp` (`required-features = ["mvp-server"]`); the composite
# `mvp-server` cargo feature = `mini` + `secure_update`, so this tier is the
# read-only `mini` serve core PLUS the offline bundle import / scrub / backup /
# audit gateway (the `holger-update-bundle` link). Before this, the gateway was an
# UNCONDITIONAL dep of every server tier and there was no separable feature — so
# the `mvp/server` entry had no cargo variant on main and stayed pending. It does
# now.
#
# Kind `service`: a holger daemon under its own dedicated system user (skidbladnir
# ServiceSpec), like `mini`/`full`. `linkage = "fat"`: it owns its own storage and
# needs no sibling running. Still ZERO-EGRESS — `mvp-server` enables neither
# `online` nor `bench`, and `secure_update`/`holger-update-bundle` link no outbound
# HTTP client (asserted by `build-space/tests/auth_ship_small.rs`). It is NOT in the
# `exclusive` fatness group directly, but `mvp-server` turns on `mini`, so the
# server-cli tier guard still admits exactly one deployment mode.
#
# ⚠ REMAINDER (recorded, not dressed up): the server-side modgunn content-verdict
# (scan / provenance Pass·Warn·Block) and the Critical-profile / meta-door read-gate
# panes are not yet a compiled surface here — this variant is the `mini` +
# `secure_update` gateway tier, which is what makes `mvp/server` buildable today.
#
# 2026-08-13 — the atom also carries `appliance` (edda's wg-appliance-core: the
# WireGuard identity ceremony + userspace tunnel). Compiled IN, gated at RUNTIME
# by HOLGER_APPLIANCE=1, which only the appliance container row below declares —
# a plain `holger-mvp start` is behaviorally unchanged. Still zero-egress: the
# ceremony links boringtun/base64ct/rand_core/libc and no HTTP client, which
# `build-space/tests/auth_ship_small.rs` keeps asserting on this very tier; the
# tunnel's UDP socket is the declared data plane, not egress tooling.
[[build.variant]]
name = "mvp-server"
suites = ["MVP"]
role = "server"
default = true
package = "holger"
bin = "holger-mvp"
kind = "service"
member = "holger"
linkage = "fat"
features = ["mvp-server", "appliance"]
no_default_features = true
profile = "release"

# ── mvp-server-musl: the STATIC appliance payload (the 14th atom) ───────────
# Ruled in by Henrik 2026-08-13 after the musl wall fell (see the §musl recipe
# below): the same mvp-server + appliance closure, cross-built for
# x86_64-unknown-linux-musl into a 21.8 MB STATICALLY LINKED holger-mvp —
# `file` says "statically linked", the appliance-pack PT_INTERP check agrees —
# i.e. the ONE binary that runs on the OS-less installed shape (no loader, no
# /lib) AND rides `scratch` in the container row below. Kind `exe`, not
# `service`: this artifact ships inside images/ISOs, it is never a
# systemd-managed host daemon. Same bin NAME as `mvp-server` on purpose (the
# manifest's `required-features` names it); no artifact collision — the
# `target` field lands it under target/x86_64-unknown-linux-musl/release/.
# The toolchain surface it needs is DECLARED in .cargo/config.toml
# ([target.x86_64-unknown-linux-musl]): a linker answering to the canonical
# cross name, fail-loud when absent.
[[build.variant]]
name = "mvp-server-musl"
suites = ["MVP"]
role = "server"
default = false
package = "holger"
bin = "holger-mvp"
kind = "exe"
linkage = "fat"
features = ["mvp-server", "appliance"]
no_default_features = true
target = "x86_64-unknown-linux-musl"
profile = "release"

# ── server-local: `holger-server`, the DEFAULT build — and it was UNDECLARED ──
#
# ⚠ THE ROT THIS CLOSES. `server/cli/Cargo.toml` declares FIVE `[[bin]]`s. Four of
# them (`holger-embedded`, `holger-mikro`, `holger-mvp`, `holger-full`) carry a
# `required-features` and each had a variant above. The fifth — `holger-server`,
# the one with NO `required-features`, i.e. the binary `cargo build -p
# holger` and `cargo install holger` actually produce — was
# named by NOTHING in this file. An undeclared bin is an unbuilt bin: no variant
# selected it, so `nornir build --list-variants` could not offer the crate's own
# default artifact, and nothing attested or sealed it. That is the same class of
# rot as the `gui` bin (see the DECLARED-BUT-NEVER-BUILT note at the top) arriving
# from the other direction — there, cargo skipped a target nobody named; here, the
# menu omitted the target cargo builds by default.
#
# It is the binary Henrik's MVP lifecycle calls (`holger-server bundle import`),
# and the one `holger-server install` / `holger-server service` stages into the
# systemd unit — hence `kind = "service"` + `member = "holger"`, exactly like
# `mini`/`full`/`mvp-server`, and `linkage = "fat"`: it links `server-lib` and
# opens its own storage, needing no nordisk sibling running.
#
# ── FEATURES: THE MANIFEST IS THE ONE WRITER (no `features` key here) ─────────
# `no_default_features` is deliberately absent (⇒ false) and `features` is empty,
# so this variant builds `server/cli/Cargo.toml`'s own `default` set —
# `["mini", "online", "bench", "secure_update"]` — WITHOUT copying it. Spelling
# those four out here would create a second writer of the same list, and a crate
# that added a fifth default would leave this file silently building the old set:
# the declaration would still look right and would no longer be what cargo does.
# The verbs Henrik's lifecycle needs ride in on `secure_update`, which is IN that
# default set (`bundle` / `scrub` / `backup` / `audit` are `#[cfg(feature =
# "secure_update")]` in `server/cli/src/main.rs`) — proven on the built binary's
# `--help`, not on this comment.
#
# ⚠ CORRECTION, recorded rather than dressed up: this tier is NOT zero-egress, and
# was described as "the offline CLI" when it was specified. Its VERBS are local —
# `promote` and `replicate` are documented in `main.rs` as "Local and in-process
# (no network)", and the whole `secure_update` gateway is network-free by
# construction (`build-space/tests/auth_ship_small.rs`). But the LINK is not: the
# default set carries `online` (→ `server-lib/online` → reqwest + ureq, the OIDC
# `/userinfo` + `jwks_uri` fetch) and `bench` (→ holger-bench-scenarios → reqwest).
# The provably zero-egress tiers are `embedded`, `mini` and `mvp-server`, which are
# built `--no-default-features`. "Local verbs" and "no HTTP client linked" are two
# different claims and only the first one holds here.
#
# It is also NOT a twin of `mvp-server`: that tier is `mini + secure_update` with
# `no_default_features`, i.e. the zero-egress gateway daemon. This one adds
# `online` + `bench` on the same `mini` core, which is a genuinely different
# dependency closure (reqwest in vs out), not a one-field restatement.
[[build.variant]]
name = "server-local"
suites = ["ORIGINAL"]
role = "server"
default = false
package = "holger"
bin = "holger-server"
kind = "service"
member = "holger"
linkage = "fat"
profile = "release"

# ══════════════ role: client — what is driven AGAINST a holger ═══════════════

# ── ui: the egui registry/artifact browser (server/ui) ──────────────────────
# The client a human runs. Its bin carries `required-features = ["gui"]`, and
# `cargo build --all-targets` silently skips such a target — which is why this
# binary was never built until it was declared here by name.
#
# ══════════════════════════════════════════════════════════════════════════════
# ⚠ CORRECTION 4 (2026-07-23) — EVERY UI VARIANT IS THIN, AND IT IS MEASURED
# ══════════════════════════════════════════════════════════════════════════════
#
# Constellation rule: *"se till att alla UI alltid är default thin byggda"* —
# every client defaults to thin and talks to a RUNNING server. A UI that embeds
# its own server shows its own in-process world instead of the server's, with
# nothing on screen telling the operator which, and becomes a second writer
# against the warehouse lock. ("Fat is the default" governs the SERVER/forge, the
# thing that owns state. A client is the other case.)
#
# This variant was `linkage = "fat"`, and honestly so — it really did embed. The
# declaration was not flipped ahead of the closure; the closure was changed first
# and then measured. Three separate things had to move to `gui-embedded`:
#
#   feature          what it embedded                     symbols it left in the bin
#   ──────────────── ──────────────────────────────────── ──────────────────────────
#   demo             LocalHolger over a znippy store it   LocalHolger 34
#                    fills itself
#   warehouse-scan   an Iceberg catalog opened from       iceberg 5642, parquet 2393
#                    $HOLGER_WAREHOUSE (modgunn → skade)
#   bench            a real holger HTTP gateway, stood    tonic::transport::server 5,
#                    up in-process to be benchmarked      FastRoutes 19
#
# Measured after the split (release, `nm -C target/release/holger-server-ui`):
# tonic::transport::server 0 · FastRoutes 0 · LocalHolger 0 · iceberg 0 ·
# parquet 0 · skade 0. Bytes 64 733 488 → 31 603 160; crates 608 → 534. The
# `reqwest` that remains (1095 symbols) is CLIENT work — the OIDC device-code
# sign-in and the Nexus/Artifactory migrate connectors — and is not egress the
# thin rule is about.
#
# Guarded by `server/ui/tests/thin_client.rs`, which builds all four tiers and
# fails if a shipped one links a server entry point or the storage engine — and
# also fails if `gui-embedded` STOPS embedding, because a check that passes
# everything proves nothing.
[[build.variant]]
name = "ui"
suites = ["ORIGINAL"]
role = "client"
# The UI is what a human reaches for; the agent is the scripted case.
default = true
package = "holger-server-ui"
bin = "holger-server-ui"
kind = "exe"
linkage = "thin"
reaches = ["holger"]
features = ["gui"]
no_default_features = true
profile = "release"

# ── ui-embedded: the ONE client allowed to embed, asked for by name ─────────
# Same source tree, bin `holger-server-ui-embedded` (`required-features =
# ["gui-embedded"]`). `gui` plus `demo` + `warehouse-scan` + `bench`: the guided
# tour over a znippy store it fills itself, the warehouse-backed advisory corpus,
# and the holger-ds gateway benchmark. This is dwarves' `--embedded` shape — the
# zero-config path is thin and the fat one is something you ask for.
#
# `linkage = "fat"` and it is the truth: 64 732 640 bytes, and `nm` finds
# `LocalHolger` 34 · `FastRoutes` 19 · `tonic::transport::server` 5 ·
# `iceberg` 5642 · `parquet` 2393 · `skade` 783. It is the ONLY artifact in the
# holger workspace that compiles skade in.
#
# ⚠ SCHEMA GAP, recorded: that skade edge is REAL but cannot be written down.
# `validate_kind` permits `embeds` only on a `service`/`container` kind, and this
# is an `exe` (a desktop app, not a system-managed daemon). So the linkage is
# stated and the composition is not expressible — a reader sees "fat,
# self-contained" but not "fat via server-lib + modgunn/skade".
#
# ⚠ It also opens the warehouse READ-WRITE-capable in the UI process. Do not run
# this build against a warehouse a `dwarves-server` is serving.
[[build.variant]]
name = "ui-embedded"
suites = ["ORIGINAL"]
role = "client"
default = false
package = "holger-server-ui"
bin = "holger-server-ui-embedded"
kind = "exe"
linkage = "fat"
features = ["gui-embedded"]
no_default_features = true
profile = "release"

# ── ui-mini: the read-only GUI for the mini SERVER ──────────────
# ⚠ RENAMED 2026-08-01 from `henriks-mini-holger`. This is a UI TIER — the middle
# rung of the client ladder, the one that pairs with the `mini` server — and the
# person who asked for it is not a build axis. A variant name that carries a
# requester's name cannot be reasoned about by anyone who does not know him, and it
# sorts nowhere near the tier it belongs to. `ui` / `ui-embedded` / `ui-mini` /
# `ui-mikro` is the ladder, and now it reads as one.
#
# The same `holger-server-ui` source tree as the `ui` variant
# above, built as bin `holger-server-ui-mini` (`required-features = ["gui-mini"]`) with
# the tab strip cut to the six read tabs the mini server's gRPC surface actually
# backs: Status / Repos / Browse / Archive / Artifact / Search. No Upload (write),
# no Tools (federation + host surface), no Sign in (OIDC needs `online`), no
# Graph / Helix / Lineage (scan- and proxy-cache-fed). `app::TAB_ORDER` shrinks
# under the `mini` marker and the scene, View menu and Ctrl-K palette follow.
#
# ✅ THIS VARIANT NOW CUTS DEPENDENCIES, NOT ONLY SURFACE. `gui-mini` is
# `gui-core + about-card + mini` — it does NOT enable `signin`, `tools`, `helix`
# or `demo`, and `server-lib` is taken with `default-features = false` so its
# `online` (reqwest + ureq) never enters. Measured 2026-07-23, release:
#
#     variant       bin                  bytes     crates  reqwest ureq skade
#     ui-embedded   holger-server-ui-embedded  64 732 640     608     1500    5   783
#     ui            holger-server-ui           31 603 160     534     1095    0     0*
#     this          holger-server-ui-mini      20 685 888     493        0    0     0*
#
#   * the two `skade` hits `nm` reports are `facett_core::look::Theme::
#     skade_vinter` — a THEME NAME, not the skade crate. The skade crate is
#     absent from `cargo tree` entirely.
#
# 41 crates drop versus `ui` and 115 versus `ui-embedded`, among them skade,
# skade-katalog, reqwest, ureq, openssl, native-tls, iceberg-arrow58 and the whole
# nornir-auth-* closure. This
# tier is the ZERO-EGRESS client: no outbound HTTP client is linked at all, which
# is what pairs it with the airgapped `mini` server.
#
# `linkage = "thin"` + `reaches = ["holger"]`, and this is a CORRECTION from the
# previous `fat`: without `demo`/`bench`/`warehouse-scan` there is no code path in
# this binary that opens storage or stands up a server. Measured (release, `nm`):
# tonic::transport::server 0 · FastRoutes 0 · LocalHolger 0 · iceberg 0 ·
# parquet 0. Every verb it has goes over gRPC, so a holger must be running or it
# can do nothing — which is exactly what `reaches` states, and what
# `server/ui/tests/thin_client.rs` checks.
#
# `kind = "exe"`: a desktop app, not a system-managed daemon — so no `member` and
# no ServiceSpec, exactly as the `ui` variant.
[[build.variant]]
name = "ui-mini"
# Ships in BOTH deliveries: ORIGINAL's read-only client for a `mini` server, and
# MVP's ui atom (Rickard 2026-08-01 — `mikro` is super-mega-small and not required).
suites = ["ORIGINAL", "MVP"]
role = "client"
# `ui` is the default client; this is the deliberate read-only exception, chosen
# when the server it drives is a `mini` tier.
default = false
package = "holger-server-ui"
bin = "holger-server-ui-mini"
kind = "exe"
linkage = "thin"
reaches = ["holger"]
features = ["gui-mini"]
no_default_features = true
profile = "release"

# ── ui-mikro: the LEANEST client, the bottom rung ───────────────
# ⚠ RENAMED 2026-08-01 from `henriks-mikro-holger`, for the same reason as
# `ui-mini` above: it is the bottom rung of the UI tier ladder, not a person.
# Same source tree again, bin `holger-server-ui-mikro` (`required-features =
# ["gui-mikro"]`). A STRICT SUBSET of `ui-mini`: three tabs —
#
#   Status    AdminService.Health                — is it up, what version
#   Repos     RepositoryService.ListRepositories — what does it carry
#   Artifact  RepositoryService.GetArtifact      — is THIS coordinate on it
#
# It drops the three PAGING/SCANNING surfaces the mini tier keeps — Browse
# (ListArtifacts), Archive (ListArchiveFiles + ArchiveInfo) and Search
# (SearchService.Search) — because each is a list-and-filter DISCOVERY view, and
# the mikro tier's premise is that the caller already knows the coordinate. Every
# op it keeps is one round trip with no cursor. It also drops `about-card`, so
# `facett-about` and the splash PNG decode go with it.
#
# Measured 2026-07-23, release: 20 481 176 bytes, 492 crates, 0 reqwest, 0 ureq,
# 0 skade — 198 224 bytes and one crate below the mini tier.
#
# ⚠ HONEST LIMIT, recorded rather than hidden: the mikro rung is thin (≈1% of the
# binary). Its dependency drop is small because `facett` — the umbrella the whole
# UI is built on — depends on `facett-helix` and `facett-model3d` (→ `image`)
# UNCONDITIONALLY, so a holger-server-ui feature cannot remove them. Making that rung
# deeper needs a change in facett, not in holger; it is named here so nobody reads
# `mikro` as a dramatic saving.
[[build.variant]]
name = "ui-mikro"
# Kept DECLARED but out of MVP: one `suites` edit away the day super-small is a
# requirement. Trades `about-card` (identity/version) for bytes, which MVP does not want.
suites = ["ORIGINAL"]
role = "client"
default = false
package = "holger-server-ui"
bin = "holger-server-ui-mikro"
kind = "exe"
linkage = "thin"
reaches = ["holger"]
features = ["gui-mikro"]
no_default_features = true
profile = "release"

# ── agent: the OFFLINE, zero-egress airgap courier (the micro/agent tier) ────
# `holger-agent-cli` built `--no-default-features`, so the `online` capability
# (the Nexus / Artifactory / crates.io·PyPI `world` connectors + their reqwest
# closure) is COMPILED OUT. What remains is the archive↔directory↔archive courier:
# pack a staged tree into a `.znippy` / `.tar.zst`, unpack one, convert between the
# two — every verb network-free, over the generic `ConnectorTrait` seam. It links
# NO outbound HTTP client (verified by `cargo tree -p holger-agent-cli
# --no-default-features`), which is what pairs it with the airgapped `mini` server.
#
# Before the `online` feature existed, holger-agent-lib linked reqwest through the
# connectors UNCONDITIONALLY, so this variant could not actually be zero-egress and
# there was no separable online tier — which is exactly why the `dev/agent` matrix
# entry stayed pending. Now the lean floor omits it and `agent-online` below turns it
# on. `no_default_features = true` is what makes the omission real.
#
# It does NOT depend on `server-lib`: there is no holger core in this binary, and
# (once online) every remote verb goes over the wire. That makes it the one
# genuinely THIN artifact this repo builds, and `reaches = ["holger"]` states the
# runtime cost — a holger must be running or the online verbs can do nothing.
[[build.variant]]
name = "agent"
suites = ["MVP", "ORIGINAL"]
role = "client"
default = false
package = "holger-agent-cli"
bin = "holger-agent"
kind = "exe"
linkage = "thin"
reaches = ["holger"]
no_default_features = true
profile = "release"

# ── agent-online: the dev/agent tier — the courier PLUS live network reach ───
# The `dev/agent` entry of holger-build's 3×3, now a REAL feature-selected variant.
# Same source tree, bin `holger-agent-online` (`required-features = ["online"]`);
# the `online` cargo feature turns on the Nexus / Artifactory / crates.io·PyPI
# `world` connectors + reqwest and the online Source/Target routes in `main.rs`
# (export FROM a live registry, push TO a live registry / Holger over OIDC·mTLS).
# Before this, holger-agent-lib linked reqwest UNCONDITIONALLY, so `online` was not
# expressible as a cargo feature and this entry had no separable variant on `main` —
# it does now (mirrors how `mvp-server` made the Secure Update Gateway separable).
#
# `kind = "exe"`: a CLI a script drives, not a system-managed daemon (so no
# `member` / ServiceSpec). `linkage = "thin"` + `reaches = ["holger"]`: it embeds no
# holger core and reaches a running one over the wire. `no_default_features = true`
# with `features = ["online"]` so the tier is exactly "offline courier + online",
# named explicitly — `cargo build --all-targets` silently SKIPS a required-features
# bin, so this variant is what makes the online agent a real, selectable target.
[[build.variant]]
name = "agent-online"
suites = ["ORIGINAL"]
role = "client"
default = false
package = "holger-agent-cli"
bin = "holger-agent-online"
kind = "exe"
linkage = "thin"
reaches = ["holger"]
features = ["online"]
no_default_features = true
profile = "release"

# ═══════ role: client — the MVP Secure Update Gateway CLIs (was `tool`) ═══════
#
# ⚠ ROLE CORRECTED 2026-08-01: `tool` → `client`. `nornir_build_thing::plugin::Role`
# documents its arms, and `Tool` is "Tooling that supports the build itself and is
# NOT shipped as the product" — a build-support role, like a codegen or a linter.
# These two are the OPPOSITE of that: `holger-bundle` and `holger-verify` are
# DELIVERED to the customer as the Secure Update Gateway MVP (Henrik's lifecycle
# runs `holger-bundle` on the intake side and `holger-verify` on the protected
# side). Declared `tool`, they would be filtered out of every shipped-artifact view
# the role axis exists to drive. `Role::Client` is "Something a human or a script
# drives AGAINST a server — a UI, a CLI", which is what these are: the operator-side
# CLIs of the gateway. (`mvp-bundle` also LOSES `default = true` here — it was the
# `tool` group's normal case, and the client group already names `ui`. Two defaults
# in one role is exactly what `BuildVariantSet::check_defaults` refuses.)
#
# ⚠ NEW — the Secure Update Gateway MVP merged to main (6341b95). The
# `holger-update-bundle` crate ships TWO real, network-free bins: `holger-bundle`
# (authoring — pack a signed `holger.update-bundle/v1`, two-person approval + the
# release-signature trust model) and `holger-verify` (the offline verifier —
# single-level cert-chain check + ed25519 over stored bytes, no OpenSSL, no network).
# They are the AGENT-tier provenance GATEKEEPER: the signature / integrity layer,
# usable with NO server and NO network. The crate is network-free BY CONSTRUCTION —
# `build-space/tests/auth_ship_small.rs` asserts it never gains reqwest/ureq — so both
# are the zero-egress airgap tools they claim to be. This is the `mvp/agent` entry of
# holger-build's 3×3, now a REAL declared+discoverable pair rather than a pending view.
#
# These two were the first buildable artifacts the MVP merge added at the CLI level.
# The gateway is no longer an unconditional server dep: it now rides the separable
# `secure_update` feature, and the `mvp-server` variant above (bin `holger-mvp`,
# `mini` + `secure_update`) is the distinct `mvp/server` tier — so that entry is LIVE
# in holger-build's `compare()`. `dev/agent` is LIVE too now: holger-agent-lib's
# reqwest closure moved behind the separable `online` feature, so the `agent-online`
# variant (bin `holger-agent-online`) is the distinct online tier and the plain
# `agent` variant is the zero-egress offline floor — the whole 3×3 builds.

# holger-bundle — the authoring CLI (pack + two-person-approve a signed update-bundle).
[[build.variant]]
name = "mvp-bundle"
suites = ["MVP"]
role = "client"
default = true
package = "holger-update-bundle"
bin = "holger-bundle"
kind = "exe"
linkage = "fat"
profile = "release"

# holger-verify — the offline verifier (M-of-N cert-chain + ed25519, no network).
[[build.variant]]
name = "mvp-verify"
suites = ["MVP"]
role = "client"
default = false
package = "holger-update-bundle"
bin = "holger-verify"
kind = "exe"
linkage = "fat"
profile = "release"

# ══════════════ the appliance container row (a packed artifact, NOT an atom) ═

# ── holger-mvp-appliance: the MVP gateway as a container appliance ───────────
# The ceremony shape Rickard's 2026-08-13 brief asks every product for: the
# hub's WireGuard public key arrives as a FILE on the mounted volume
# (/data/wg/hub.conf), the appliance mints its own keypair into that volume on
# first start, prints the canonical identity line on stdout (podman logs shows
# it) AND writes /data/wg/identity.pub for a no-log readback. The volume — ONE
# volume, /data, carrying config + registry state + wg identity — is what
# survives container replacement and image upgrade.
#
# The ROW still carries no `suites` tag — korp's atoms-removed-2026-08-03
# ruling stands: the suite axis is VARIANTS-ONLY
# (`BuildVariantSet::suites()` iterates `self.variants`), so a
# [[build.container]] row has nowhere to carry one and would be a tag nothing
# reads. What CHANGED (2026-08-15 vocabulary ruling + the drive handover): the
# tag was missing, not the row. The tagged POINTER VARIANT below is the atom
# and carries the suite; this row is its PAYLOAD — the exact seam gunnar
# already used twice (gunnar-image → gunnar-scratch). korp's untagged rows
# were deleted because they had no pointer AND no drives; this row HAS a
# drive (9/9 container rows, 2026-08-15, incl. tunnel_up_traffic_crossed), so
# it gets its tag instead of the axe.

# ── the pointer variant: the atom the container row is payload FOR ──────────
# `kind = "container"` carries no cargo argv of its own; `container_ref` points
# at the [[build.container]] row that owns the descriptor (they share a name
# here because the row IS this variant's only payload — the ref field is what
# resolves, exactly as in gunnar-image). With this row the drive matrix shows a
# real suite instead of (untagged).
[[build.variant]]
name = "holger-mvp-appliance"
suites = ["MVP"]
role = "server"
default = false
kind = "container"
linkage = "fat"
container_ref = "holger-mvp-appliance"
#
# `base = "scratch"` — ruled in by Henrik 2026-08-13, the day the musl wall
# fell: the layer is the `mvp-server-musl` atom's STATICALLY LINKED binary, so
# the image carries NOTHING but holger (no libc, no shell, no distro — the
# brief's FROM-scratch posture, gunnar-scratch's shape). Two consequences,
# recorded not discovered later:
#   * the glibc-floor law that forced cc-debian13 while the layer was
#     glibc-built (host 2.39 vs debian12's 2.36, measured as a GLIBC_2.39
#     refusal) is MOOT for a static layer — kept in history via git;
#   * `scratch` carries no /etc/passwd and the schema declares no USER, so
#     the container runs uid 0 (root-in-userns under rootless podman — maps to
#     the invoking user). The identity files' 0600 discipline is unaffected.
# No base to pull: a scratch build needs no network at all. The built image is
# exported to `archive` so the release carries a FILE (docker-archive format;
# podman load and docker load both read it).
#
# HOLGER_APPLIANCE=1 is DECLARED here — the runtime ceremony gate lives in the
# artifact's declaration, not in an operator's shell history. The binary
# refuses to start as an appliance without the `appliance` feature compiled in,
# and the mvp-server atom above carries that feature.
#
# Runtime contract (the robot in build-space/tests/appliance_container_robot.rs
# proves it): -v <vol>:/data plus --cap-add NET_ADMIN --device /dev/net/tun
# when a hub is declared; without them a declared hub FAILS LOUDLY by name and
# the container exits nonzero — never half-up.
[[build.container]]
name = "holger-mvp-appliance"
base = "scratch"
entrypoint = ["/holger-mvp"]
cmd = ["start", "--config", "/data/holger.ron"]
workdir = "/data"
expose = [50051, 8443]
tags = ["holger-mvp-appliance:0.6.9"]
format = "oci"
archive = ".container-out/holger-mvp-appliance.tar"

[build.container.env]
HOLGER_APPLIANCE = "1"

[[build.container.layers]]
from_variant = "mvp-server-musl"
dest = "/holger-mvp"
executable = true

# ── musl: the wall FELL 2026-08-13 — the recipe, measured on the T14s ────────
# `cargo build -p holger --bin holger-mvp --no-default-features
# --features mvp-server,appliance --target x86_64-unknown-linux-musl --release`
# now produces a 21.8 MB STATICALLY LINKED holger-mvp (`file`: "statically
# linked"; the appliance-pack PT_INTERP check agrees), with zig as the cross
# C/C++ toolchain. The recipe, so it becomes a declared build environment and
# not folklore:
#   1. zig 0.16.0 (ziglang.org tarball, sha256-verified) at ~/toolchains/
#   2. wrappers named x86_64-linux-musl-gcc/-g++ (the names cc/cmake derive)
#      that exec `zig cc|c++ -target x86_64-linux-musl` and rewrite what the
#      glibc-minded callers spell: strip incoming --target flags (cc-rs passes
#      the RUST triple, zig refuses it), map -lstdc++ → -lc++ (zig's C++
#      runtime is its bundled libc++); plus x86_64-linux-musl-ar → host ar
#   3. env: CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc
#      CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C link-self-contained=no"
#      (zig owns crt+libc — rustc's self-contained musl crt otherwise collides:
#      duplicate _start)
#   4. one REAL bug fell out: the address writer's ioctl request type was the
#      glibc spelling (musl says c_int) — fixed in wg-appliance-core (libc::Ioctl)
# openzl-sys-rs (C++/cmake), ring, zstd, lzma: all compile and link. RULED
# 2026-08-13 (Henrik): the 14th atom (`mvp-server-musl`, above) and the
# `scratch` base flip are IN; the linker name + rustflags are declared in
# .cargo/config.toml. Still open: landing the toolchain itself in the declared
# build environment (dwarves/build-thing owns that vocabulary) rather than in
# ~/toolchains — the .cargo declaration is the fail-loud floor until then.

# ── distribution/Dockerfile: NOT declared as a file-mode row, and why ────────
# The "hand-rolled containers become declared rows" law wants this file declared
# (ContainerBuild file mode exists for exactly it), but a declaration is a
# CLAIM and this one cannot build — MEASURED 2026-08-13 on the exact
# origin/main tree (172fd75, `git archive` into an empty dir, no siblings):
#
#   error: failed to load manifest for workspace member `<tree>/server/lib`
#   Caused by: failed to load manifest for dependency `holger-handler-rust-toolchain`
#   Caused by: failed to load manifest for dependency `znippy-plugin-rust-toolchain`
#   Caused by: failed to read `<parent>/znippy/plugins/native/znippy-plugin-rust-toolchain/Cargo.toml`
#   Caused by: No such file or directory (os error 2)
#
# The Dockerfile's builder stage does `COPY . .` + `cargo build` — the context
# carries the repo ONLY, so the sibling paths cannot resolve inside it. Note
# this is a DIRECT path dep (server/lib → holger-handler-rust-toolchain →
# ../../../znippy/plugins/native/…), not just the removable [patch.crates-io]
# table: the release gate's "remove all patches" step is NECESSARY but no
# longer SUFFICIENT. The row becomes declarable the day the committed tree
# builds sibling-free (toolchain-plugin deps version-resolved); until then a
# declared-but-red row would be worse than the documented manual command in
# the Dockerfile's own header (korp's atoms-removed ruling, applied).

# ══════════════ the installer-ISO row — per-site, declared, parameterized ═══

# ── holger-installer: the per-site installer ISO (2026-08-13) ────────────────
# The row `cargo xtask installer-iso` composes and masters, through edda's
# ONE implementation (korp-installer isocompose::compose_and_master — the same
# code the KVM proofs drive): the REAL korp-boot first stage, the REAL
# static-musl korp-installer PID-1 (/init) and korp-start, an operator-
# supplied kernel pinned by its own blake3, and the PAYLOADS ON THE ISO-9660
# VOLUME (app.znippy from `cargo xtask appliance-pack`, the target boot
# chain, and the per-site hub.conf) — El Torito's boot-image ceiling forbids
# them in the ESP, and the installer PID-1 materializes them off /dev/sr0
# (the wired iso9660 door). `installer_variant` is EMPTY on purpose: the
# composition builds the PID-1 itself; a cargo-variant-chained installer is
# the forge's own path.
#
# The ROW carries no suites tag — korp's 2026-08-03 ruling, same as the
# container row above — but since 2026-08-15 the tagged POINTER VARIANT below
# carries the suite: like an iso, a `kind = "installer"` variant resolves its
# payload BY ITS OWN NAME (`assemble()` → `require_installer(&variant.name)`,
# no delegating ref field), so the variant and this row share the name
# `holger-installer` by construction. This row drove on 2026-08-15 (ISO
# mastered first time ever, installed disk serves on both boots), so it gets
# its tag instead of korp's axe.
#
# The PARAMS are the per-site inputs (the --param mechanism, secrets refused
# by name): `site` lands on the INSTALLER's cmdline as korp.site (visible
# provenance on the mastered media), `hub_conf` (usually `--param
# hub_conf=@site7-hub.conf`) lands BOTH on the ISO volume (inspectable) and —
# through the composition — at /run/installer/hub.conf, which the installer
# lays onto P3 as wg/hub.conf for the first boot's ceremony. The applied
# values ride the attested descriptor: the build record answers which ISO
# holds which hub key without booting it.

# ── the pointer variant: the atom the installer row is payload FOR ──────────
[[build.variant]]
name = "holger-installer"
suites = ["MVP"]
role = "server"
default = false
kind = "installer"
linkage = "fat"

[[build.installer]]
name = "holger-installer"
variant = "mvp-server-musl"
volume_id = "HOLGER_INSTALL"
target_disk_gib = 64
# ── THE MARK ON THE BOOT SCREEN ──────────────────────────────────────────────
#
# Rides `KORP_BOOT_LOGO` into `korp-installer/src/splash.rs`, which decodes it
# into a fitted 520×360 BGRA blob the `korp-boot` first stage blits before it
# reads a byte off the ESP. Without it holger's installer boots wearing korp's
# raven — MEASURED 2026-08-16: gunnar's, korp's and holger's captured boot
# framebuffers were BYTE-IDENTICAL (md5 32b65f91… on all six frames of each).
#
# **This is the only image holger has, and it is not a purpose-made boot mark.**
# `holger/assets/` does not exist; `.nornir/assets/holger-znippy-logo.png` is a
# 1024×1536 portrait drawn for the znippy write-up, fitted here to 240×360 —
# tall, so it uses a third of the box's width. It is holger's own committed
# artwork and it is unmistakably not korp's, which is what this line has to
# achieve today. Replace it with a landscape mark when there is one; nothing
# else needs to change.
boot-logo = ".nornir/assets/holger-znippy-logo.png"
boot_params = ["console=ttyS0", "korp.appliance=lean", "korp.entry=holger"]
# The app's argv tail — the metal twin of the container row's `cmd` above
# (ONE word for "how the product starts" across both shapes). Rides the
# installed disk's cmdline as `korp.args=` (comma-joined); korp-start decodes
# it after argv[0]. MEASURED need (2026-08-14, the fleet first-boot proof):
# without it holger exec'd bare, printed its help and PID 1 died — a machine
# that boots but does not serve.
cmd = ["start", "--config", "/data/holger.ron"]

[[build.installer.params]]
name = "site"
label = "Site name"
kind = "text"
required = false
boot_param = "korp.site"

[[build.installer.params]]
name = "hub_conf"
label = "Hub declaration (the ceremony hub.conf, public data)"
kind = "text"
required = false
staged_file = "hub.conf"

# REQUIRED: the server declaration the cmd above names. Absent => the BUILD
# refuses (the params engine's required-by-label), because what this appliance
# serves — ports, repos, storage — is a SITE claim no default may invent: a
# synthesized config reproduces "boots but does not serve" one level down,
# wearing a green boot. holger's own Start already refuses a missing --config;
# required=true moves that verdict from a serial console in a rack to the
# operator's terminal. Lands on P3 as /data/holger.ron (the same path the
# container row's cmd names — one dialect, both shapes).
[[build.installer.params]]
name = "holger_ron"
label = "Server declaration (holger.ron)"
kind = "text"
required = true
staged_file = "holger.ron"

# The app's environment — the metal twin of the container row's env (the
# other half of the shape-parity story cmd started). Rides the installed
# cmdline as korp.env.* tokens; korp-start decodes them into the exec's envp.
# HOLGER_APPLIANCE=1 is the ceremony gate — without this channel the served
# metal appliance ran with its tunnel dark (measured 2026-08-14).
[build.installer.env]
HOLGER_APPLIANCE = "1"

# Per-site UNDERLAY address (static-IP appliance sites): <addr>/<prefix>@<if>,
# applied by holger's appliance path through the shared addr writer before the
# tunnel initiates. OPTIONAL — a site whose network hands out addresses some
# other way simply omits it; the KVM proof's value names QEMU slirp's guest
# address. A per-SITE value, so it is a param (not a row constant), landing
# as a korp.env.* token through the same env channel.
[[build.installer.params]]
name = "underlay"
label = "Underlay address (addr/prefix@interface)"
kind = "text"
required = false
boot_param = "korp.env.HOLGER_UNDERLAY"

# ── the DECLARED FIXTURE for hub_conf (2026-08-22) ───────────────────────────
# `--param hub_conf=@fixture` (CLI, xtask, or the dwarves pane's "använd
# fixtur") makes the FORGE mint the hub instead of an operator pasting one: a
# userspace WireGuard hub (edda `wg-appliance-core::hubfixture`, one UDP
# socket, no root, no /etc/wireguard), reserved before the appliance exists;
# its PRIVATE half stays 0600 under the forge's state dir, its PUBLIC half is
# rendered into hub_conf through the same renderer the ceremony parses, and
# the build record says the value came from fixture `wg-hub`. Row-level table
# (not a key on the params row) so published 0.1.x readers still load this
# file — see edda's nornir-build-thing `params.rs` ("Declared fixtures").
[build.installer.fixtures]
hub_conf = "wg-hub"

# ── the PRE-composition wall record (kept for history; the row above is the
#    2026-08-13 resolution): ──
# A candidate row (name=holger-installer, variant=mvp-server,
# installer_variant=holger-installer-pid1, boot_params korp.entry=holger) was
# probed against this very declaration on 2026-08-13; the set-level validation
# refuses it verbatim:
#
#   installer 'holger-installer': chains cargo variant 'holger-installer-pid1'
#   (its PID-1 payload) which is not declared (known variants: embedded, mini,
#   full, mvp-server, server-local, ui, ui-embedded, ui-mini, ui-mikro, agent,
#   agent-online, mvp-bundle, mvp-verify)
#
# and behind that first refusal sit three real absences, each with an owner:
#   1. the PID-1 installer variant — edda's korp-installer is the product-
#      neutral installer crate (Rickard's 2026-08-13 ruling keeps the name),
#      and its korp.entry=<name>/korp.znippy=<path> cmdline contract is the
#      declared seam holger would ride; holger declaring a pid1 variant of its
#      own would twin it. Owner: edda (korp-installer), consumed here.
#   2. ✅ CLOSED 2026-08-13: `cargo xtask appliance-pack` packs a built server
#      binary into .znippy-out/holger-app.znippy (entry `holger` — the
#      korp.entry selector) + its blake3 pin, via the shared znippy codec, and
#      proves the entry reads back byte-identical. Remaining honesty note: the
#      OS-less installed shape execs with no loader, so the PRODUCTION payload
#      must be static — the musl wall above still stands between the packed
#      glibc binary and real metal (the verb warns by PT_INTERP).
#   3. kernel + rootfs artifacts — no packed kernel exists in this repo;
#      gunnar's lives in its own tree. Owner: the suite's kernel pack.
# "A declaration is a claim": the row lands the day the remaining inputs
# exist and the mastered ISO actually BUILDS (an ISO with no artifact is
# always red).
```