Gunnar

nordisk/dwarves

.nornir/build-thing-code-is-source-design.md

<!-- nornir:class: design -->
# build thing — code is the source, toml is emitted — design

> **Class:** `design` · **Function axis:** `f0` code is the one source · `f1` the emitter (`code → toml`, one-directional) · `f2` the per-typ consequence engine · `f3` bijection by construction (no parity test) · `f4` one evaluator behind two doors (CLI ↔ ⚒)
> **Strict:** this file states *only what the build-thing declaration model IS* after the 2026-08-27 decision. It **refines** [build-thing-model-design.md](build-thing-model-design.md): where that file calls `.nornir/build-variants.toml` "the still part", this file corrects it — the toml is **not a source, it is a projection**.
> Written down 2026-08-27 from Rickard's decision, in his vocabulary: **variant → features → settings / sub-settings → typ**. Never "cell".

A build thing **is the code that builds** — the Rust `impl` of the trait — and that code is the **single source** of every variant, feature, setting/sub-setting, typ and BUILT edge.

> **THE RULE (Rickard, 2026-08-27):** a `.nornir/build-variants.toml` must **NEVER exist as a committed, read file** — in any repo. The code (the trait `impl`, compiled to the wasm module) is the only source. TOML exists **only as on-demand OUTPUT** the code emits to *describe* a build (`info --format toml` and the like) — it is produced when someone asks to see it, and nothing is stored and nothing reads it back. Code → toml, one-directional, ephemeral. There is nothing to keep in parity, so the parity test dies and the bijection holds **by construction**. A committed toml that anything reads is the twin, and it goes.

The kernel is why this must be code and not a file: toggling a kernel setting does not change dependencies, it flips **Kconfig modules** in and out of the bzImage — a computation, not a table lookup. Only code can compute it, and the same code both describes it (emits toml) and performs it (builds).

## The system as one — the function map (f0)

| part | lives in | what it is |
|---|---|---|
| `f1` the source | each repo's `<repo>-build-thing` `impl BuildThing` | the ONE declaration of variants/features/settings/typ/edges, in Rust. Never parsed from a file |
| `f1` the emitter | edda `nornir-build-thing` (`code → toml`) | a pure projection of the impl to a toml that DESCRIBES the build. Output only; the committed toml, if kept at all, is its artifact |
| `f2` cargo consequence | edda `variant.rs` (`closure` / `leaf_closure`) | for typ `cargo`: a toggled feature → the dependency/feature closure delta. wireguard off → `boringtun`/`wg_appliance_core` absent (measured 70 → 0, 57 → 0) |
| `f2` kernel consequence | edda `kernel.rs` (`merge_kconfig` / `merged_fragment`) | for the kernel: a toggled setting → the **Kconfig module** delta. `kernel.proc-fs`, `kernel.memfd-create`, … flip `CONFIG_*` in/out; consequence is measured in modules and the bzImage blake3, not a `Cargo.lock` |
| `f3` bijection | edda `laws.rs` (`assert_bijective`) | the choice → the receipt fragment sha, and its inverse names the choice from the result (law D). One writer, so `toml_parity` is unnecessary |
| `f4` the two doors | dwarves `buildthing` (CLI) · `dwarves-gui` (⚒ pane) | thin doors. Neither holds declaration logic; both call `nornir-build-thing`. With `--server` the CLI delegates to the forge, so the SERVER's `nornir-build-thing` is the one evaluator |
| `f4` the chain runner forwards the step | dwarves `build_steps.rs` (`run_build_chain`) → edda `kernel::set_for_kernel_step` | a kernel step is built from the declaration the plan pinned: the step's `kernel.*` line + the consumers' `requires` landed on the donor's row by the ENGINE's bridge (`chosen_kernel_for`); the pin check (`resolve_built_kernel_for`) and the consumer's kernel-edge reader (`requirements_of_consumer_line`) resolve WITH the closure. The forge never re-derives the closure or spells the merge. Measured 2026-08-29 before: `kernel.why.toml` `feature = 0, preset_only = 69`; after: every `appliance-boot` row names its requirer |

## Why code, not toml — the decision

The recipe used to live in **both** places: the Rust `recipe.rs` **and** a committed toml, held equal by `gunnar/crates/gunnar-build-thing/tests/toml_parity.rs`, with `bijection_laws.rs` reaching the toml half "transitively through toml_parity's equality". Two hand-maintained copies watched by a test is the twin LAW 5 forbids — the same shape as "cell", one artifact with two names.

The inversion: the `impl` is the source. Two **pure projections** fall out of it, and only out of it:

- `declare() → toml` — **describes**. This is the emitter. If a committed toml exists, it is this function's output, regenerated, never edited by hand.
- `declare() + choice → artifact + receipt` — **builds**. The receipt's fragment sha is a function of the declared choice.

Because both projections read the *same* source, the receipt is bijective with the choice by construction. `toml_parity` guarded an equality that can no longer be violated, so it is deleted and its guarantee moves into the single writer. An edited toml has **zero** effect on a build — that is the load-bearing test.

## The consequence is typed (f2)

Build thing does not list variants; it **computes the consequence** of a choice, and the consequence's *type* depends on typ:

- **cargo / app typ** — consequence = the **dependency / feature closure**. cargo's own feature resolution answers "what does this feature pull in / drop", reused through `closure` / `leaf_closure`. The visible proof is the binary: wireguard off removes the `boringtun` and `wg_appliance_core` symbol families entirely.
- **kernel typ** — consequence = **Kconfig modules**. A setting maps to `CONFIG_*` symbols; toggling flips modules in and out of the bzImage. Reused through `merge_kconfig` (a pure function over kconfig text). The six groups that made the thin kernel *runnable* — `PROC_FS`, `MEMFD_CREATE`, `SHMEM`+`TMPFS`, `FILE_LOCKING`, and the `EPOLL EVENTFD TIMERFD SIGNALFD FUTEX POSIX_TIMERS` group — are exactly this: not dependencies, modules, each measured by a distinct boot failure without it.

The two do not share one closure function. The trait carries a **per-typ** consequence method, and that separation is the point, not an accident.

## Place in the nordisk constellation

The evaluator is **one crate, `edda/crates/nornir-build-thing`** — the trait, the declaration reader, the per-typ consequence engine, and the emitter all live there. Every `<repo>-build-thing` crate implements the trait for its repo. `dwarves buildthing` (CLI) and the ⚒ pane in `dwarves-gui` are **thin doors** onto that crate — the logic is never copied into the GUI crate or the CLI binary. With `--server <url>` the CLI delegates the build to the forge over the same RPC the pane uses, so even at runtime there is a single evaluator: the server's `nornir-build-thing`. Acyclicity holds: repos depend on `nornir-build-thing`, never the reverse.

## The build-things and their tillskott

Sixteen canonical repos declare a build space. Six already carry a Rust recipe (`impl BuildThing`) and need only the twin removed — the committed toml becomes emitted output, `toml_parity` deleted:

| repo | code recipe today |
|---|---|
| dwarves | `crates/dwarves/src/build_thing.rs` + `build_thing/code.rs` |
| edda | `nornir-build-thing` `plugin.rs` / `laws.rs` |
| gunnar | `gunnar-build-thing/src/recipe.rs` |
| korp | `korp-build-thing/src/recipe.rs` |
| knut | `knut-build/src/lib.rs` |
| holger | recipe lives in `edda/crates/nornir-buildspec-holger/src/lib.rs` |

Ten declare only via toml and need the **tillskott** — a Rust `impl BuildThing` that IS the source, from which the toml is emitted: **Skidbladnir · draupnir · korp-collectors · modgunn · nornir · ordning · skade · tunnr (the kernel) · znippy · znippy-zoomies**. `tunnr` is the special case: its consequence is the Kconfig delta, not a dep closure.

## Tests (inject-assert, LAW 2)

- **the emitter is faithful** — a setting the code declares that the emitted toml drops must go red (count code-declared vs toml-carried).
- **the toml is inert** — edit the committed toml to *disagree* with the code; the build must follow the code, and the edit must have zero effect on the resolved set / artifact (measured, not a log line).
- **`toml_parity` is gone without loss** — its deletion is accompanied by the single-writer emitter test; the guarantee moved, it did not vanish.
- **the consequence is measured per typ** — cargo: wireguard off → the two symbol families absent, with a positive control (`dhcp` present) in the same binary; kernel: the six groups change the bzImage blake3 / size, asserted as a delta, not a round-trip.
- **one evaluator** — a delegated build (`--server`) and an in-process build of the same variant agree on the medium, compared on znippy pin + kernel blake3 + receipt fragment (never ISO sha — its nondeterminism is 39 bytes of FAT timestamps in the ESP).

## Discovery and provenance — search returns a proof, not a find (f5)

Today "search gunnar" is a **filesystem scan**: `dwarves buildthing` (CLI) and the ⚒ pane both go through `dwarves/crates/dwarves/src/build_thing.rs` — `default_root()` finds the constellation root, scans subdirectories for a checkout that *declares a build space* (has a `.nornir/build-variants.toml`), and matches the term. That scan is fragile: on 2026-08-27 `default_root()` picked `/home` (1 repo) over `/home/rickard/git` (34) and answered *"ingen deklaration under /home nämner gunnar"* (fixed: the root now counts members, richest wins). It is a *find*, and a find can be wrong.

The intended shape is a **triangle**, and it is the build-supply-chain application of a pattern Google standardised:

- **producer** — a recipe (`<repo>-build-thing`) that, when built, emits a **SLSA build provenance** (`nornir_build_thing::provenance::BuildProvenance`) plus `build_info` rows (`BuildInfoSink::emit`).
- **the signed record** — the provenance is folded to canonical bytes → SHA-256 digest → **sealed** by `ProvenanceSealer::seal` (`provenance.rs:299`) as a **detached CMS `SignedData` (DER)** over the digest, yielding an `Attestation` (record + canonical bytes + digest + seal). It lands in the warehouse's `build_provenance` table, written by the **server** — the server's pid on the parquet file is the proof, and writing it is what closed "⚒ Build Thing empty four times" on 2026-08-27.
- **the client** — the pane / CLI reads the record and **verifies the seal**, so a discovery answer carries a cryptographic guarantee it is exactly what was published.

The standard is **SLSA** (Supply-chain Levels for Software Artifacts, Google 2021). `BuildProvenance` is a SLSA provenance predicate field-for-field: `builder` = the forge; `buildType`/`invocation` = the variant + `--opt`/`--param`; `materials` = the source + the kernel edge (`tunnr/tunnr-kernel`); `subject` = the artifact's fragment sha / kernel blake3; the signature over all of it = `ProvenanceSealer`. The Google root beneath SLSA is **Certificate Transparency** (RFC 6962) — a signed, append-only Merkle log with inclusion proofs.

**What is live vs. what completes the triangle.** Live today: the signed provenance (CMS seal, server-written) and one evaluator behind both the CLI and the pane — `dwarves buildthing --server` delegates the build to the forge over `dwarf.v1.BuildThing`, proven byte-identical to the in-process build (znippy pin + kernel blake3 + donor pin identical, `cmp -l` = exactly the 39 FAT-timestamp bytes), and `buildthing search <term>` / `info <product>` answer from the forge's declarations, never a local scan. Missing to close it: (1) a **DSSE** envelope if SLSA *interop* is wanted — the CMS seal does the same job over the same digest, only the envelope differs; (2) the **Certificate-Transparency-style append-only log with inclusion proofs**, so discovery returns a *proof* and not merely a record; today search reads the recipe, it does not yet demand an inclusion proof. That step is what turns "find the recipe" into "prove the recipe".

### Signing — CMS today (airgap), DSSE for SLSA interop

The signing seam already exists; the interop envelope is what is being added (2026-08-27).

| part | lives in | what it is |
|---|---|---|
| the primitive | `znippy/znippy-common/src/sign.rs` | detached CMS `SignedData` (RFC 5652), Ed25519 / P-256-ECDSA, airgap, no network (`der`/`ed25519-dalek`/`p256`/`sha2`). `SigAlg`, `signer_from_pkcs8`, `dev::mint_ca` |
| the sealer | `nornir/src/build/seal.rs` (`ZnippySealer`) | a `ProvenanceSealer` wrapping a znippy signer; today emits a CMS `Seal`, travels as airgap `<artifact>.build-provenance.znippy` |
| the seam | `edda/…/nornir-build-thing/src/provenance.rs` | `ProvenanceSealer::seal(digest) -> Seal`, `Attestation` = record + canonical bytes + digest + seal |
| the verifier | `modgunn/src/provenance.rs` | already knows SLSA levels 0–3, sigstore/cosign/rekor/slsa-verifier, and parses Rekor `intoto`/`dsse` incl. `content.envelope.signatures[].keyid` — the DSSE envelope shape is already modelled on the verify side |

So half the code exists: a producer (CMS) and a verifier that already understands DSSE/SLSA. The gap is a **DSSE envelope signing mode** in `znippy-common::sign`, beside CMS, not replacing it — DSSE per spec (PAE `"DSSEv1" SP len(payloadType) SP payloadType SP len(payload) SP payload`, `payloadType = application/vnd.in-toto+json`, `{payload, payloadType, signatures:[{sig, keyid}]}`) signed with the SAME znippy keys, carrying the SLSA-predicate payload. **CMS stays the airgap default; DSSE is the interop option** (`--provenance dsse|cms` / a declared setting on the seal seam). External SLSA tooling (cosign / slsa-verifier) verifies the DSSE form; modgunn verifying our envelope is the internal producer↔verifier agreement test. What only an online cosign run can confirm is left named, not claimed.

## Two ways to name a build thing — coordinate or search

Discovery has **two inputs**, the same two a package manager gives you:

- **the coordinate** — you know the recipe exactly: `<repo>/<variant>` (à la Maven `group:artifact:version`; with a registry, `<repo>/<variant>@<version>`). It resolves DIRECTLY, no ambiguity — and because the coordinate is the registry key, the resolve is *verifiable* (the provenance seam below rides on it). The coordinate grammar already exists on the BUILT-edge path: `--build tunnr/tunnr-kernel` (repo/variant), `--build tunnr-kernel` (the donor variant when exactly one edge names it).
- **the search** — you know only `gunnar`: `buildthing search gunnar` → a fuzzy match over the roster → candidates you pick a coordinate from. This is an index query, the way `cargo search` / Maven Central search is.

Today both go through the local checkout scan (`ConstellationBuilds`); the registry-backed form — the coordinate resolving a published wasm crate `@version` out of the registry, search becoming an index query — is the same staged step as the read-path inversion.

## Can SLSA/DSSE be verified ON the build thing itself? Yes — and it MUST be

The recipe is a **published crate** whose wasm cdylib the forge loads (`gunnar-build-thing`/`korp-build-thing` are `crate-type = ["cdylib","rlib"]`; a build thing that is not a cdylib is refused by name). So the recipe is **itself an artifact that was built** — and therefore it can carry its OWN SLSA build provenance, exactly like the media it produces. There are **two levels of provenance**:

1. **the recipe's own** — the `<repo>-build-thing` wasm module was built from source X by the forge; that build has a SLSA provenance and a DSSE/CMS seal.
2. **the artifact's** — the medium the recipe builds carries the provenance described above.

And verifying the first is not optional, because `plugin_host.rs` already treats a loaded module as hostile: *"the host builds a `wasmtime::Linker` and defines nothing on it, so an import from a registry is untrusted input, and it is treated as such."* Verifying the recipe's DSSE/SLSA attestation is precisely **how that untrusted import earns trust**. So the load path becomes: resolve the coordinate → fetch the wasm crate `@version` → **verify its DSSE envelope / SLSA provenance** (ideally with a Certificate-Transparency inclusion proof) → only THEN instantiate and run it. A recipe whose provenance does not verify is refused before it can declare or build anything.

This is the supply-chain answer to "search gunnar returns a *proof*, not a find": the coordinate resolves a signed, attested recipe, the forge verifies it on load (reusing modgunn's DSSE/in-toto verifier), and only a verified recipe gets to run — and then the medium it builds carries its own verified provenance in turn. Trust flows from the recipe down to the artifact, both sealed, both checkable.

## Build isolation — an attested build goes in a container, and the Rust starts it via draupnir

The dividing line is **not kernel vs Rust** — it is **dev vs attested**:

- **A dev / workstation build** (iterating on oden, artifact thrown away, not shipped, no provenance) may build on the host. Fast, and reproducibility does not matter for it.
- **An attested build** — anything the forge builds for delivery, that gets a provenance and is shipped — is built **in a container**, kernel and Rust alike. The kernel goes through `nordisk-kernel-toolchain`, cargo through `nordisk-cargo-toolchain`; both are **digest-pinned** and run **`--network none`**.

The evidence that containerising only the kernel is not enough is Part A itself: the app's musl binary, built on the **host**, baked its absolute build path (`/home/dwarves/.nornir/workspaces`) into itself, and **the app pin became non-reproducible** — two identical-source builds produced different pins. That is host-built Rust breaking reproducibility exactly as an uncontainerised kernel would. A container fixes the toolchain digest → same compiler every time; `--network none` → hermetic, no supply-chain injection mid-build; a fixed path inside the container → the pin reproduces; and it is the isolation boundary SLSA Build L3 asks for. The containers already exist for both, so this is a **policy**, not new infrastructure: the forge builds everything it attests in its containers; the host is for dev only.

**Who starts the container: the Rust, via draupnir — never the operator or an agent by hand.** The layering is strict:

```
operator / agent  →  dwarves buildthing …          (drives the forge CLI, nothing lower)
                  →  the forge's Rust               (nornir-build-thing)
                  →  DraupnirKernelRunner           (KernelContainerRunner, feature `kernel-build`)
                  →  draupnir                        (backend-oci / youki)
                  →  the OCI runtime                 →  make bzImage / cargo build
```

`DraupnirKernelRunner` is the production `KernelContainerRunner`, running over draupnir's `backend-oci` engine; `kernel.rs` sets `KBUILD_BUILD_USER=nordisk KBUILD_BUILD_HOST=draupnir` inside the container, which is why a real container-built kernel's `strings` reads `version 6.12.104 (nordisk@draupnir)` — that string IS the proof the build went through draupnir, unforgeable on the host. This is the same law as "build through Build Thing, not by hand", one layer down — and it is what makes a `--build kernel` that resolves the pin cache, or a hand-staged bzImage, a defect rather than a shortcut.

> **THE RULE (Rickard, 2026-08-27): NO SHELLING.** An operator or an agent NEVER shells a build — no `podman run`, no `youki`, no hand-run `make`, no `cargo build` staged into an artifact, no `xorriso`. The ONLY thing you invoke is the forge CLI (`dwarves buildthing …`); the forge's Rust drives draupnir, draupnir drives the container. Anything that reaches around the forge to produce an attested artifact by hand is void — it has no provenance, no reproducibility, and no trust. If the forge cannot do it, the fix is to make the forge do it, never to do it yourself in the shell.

## Source map

- the trait + reader + emitter + consequence → `edda/crates/nornir-build-thing` (`plugin.rs`, `variant.rs`, `kernel.rs`, `laws.rs`, `build_dag.rs`)
- the twin to remove → `gunnar/crates/gunnar-build-thing/tests/toml_parity.rs`
- the toml-as-source to invert → `variant.rs` `BuildVariantSet::discover` (reads the file today) and `build_dag.rs` `BesideCheckouts::declaration`
- the two doors → `dwarves` `buildthing` (CLI) · `dwarves-gui` ⚒ pane

See also: [build-thing-model-design.md](build-thing-model-design.md) (the tree, the ABI, the laws A–E — this file refines its "still part" line), [the-two-dags.md](the-two-dags.md), [buildthing-guide.md](buildthing-guide.md).