Cross-repo from_variant — the design, and why it is not a small edge
Status: FILED AS A DECISION, not implemented. 2026-09-06. Scope: crates/nornir-build-thing (edda), read against dwarves, gunnar, tunnr/tunnr-lockfix, korp.
Three seats derived the same missing capability independently on 2026-09-05/06: tunnr stages /app from a PATH because a variant reference cannot cross repos; korp landed a per-binary variant witness because a path is a string and nothing opened the file at the end of it; the seven korp media had to MOVE REPOS (korp 86bf1a9) because a medium is composed from the artifact its from_variant names and that name resolves inside one repo's set only.
This document answers the four questions and then says why the row is a decision rather than an edge.
1. WHY it cannot cross repos today — and which KIND of limit it is
Not one limit. Three, at three different layers, and only the third is hard.
L1 — the FIELD has no repo half (a vocabulary limit; trivially fixable)
| declaration | field | file:line |
| iso | pub from_variant: Option<String> |
src/iso.rs:163 |
| vm | pub from_variant: Option<String> |
src/vm.rs:144 |
| container layer | pub from_variant: Option<String> |
src/container.rs:149 |
| package | pub from_variant: Vec<String> |
src/package.rs:237 |
A bare handle. Compare KernelDonor { repo, kernel } (src/installer.rs:153-158) and EdgeDecl { repo, variant, typ } (src/tree.rs:257-264), both of which DO carry the repo half.
L2 — the ROSTER the handle is resolved against is this repo's, at four sites
(a scope limit; mechanically fixable, with one real consequence)
variant.rs:2684 validate_package self.get(dep).is_none() -> refuse
variant.rs:2760 validate_container (via variant_deps) -> refuse
variant.rs:2802 validate_vm self.get(dep).is_none() -> refuse
variant.rs:2826 validate_iso self.get(dep).is_none() -> refuse
e.g. variant.rs:2830 — "iso '{}': masters cargo variant '{}' which is not declared (known variants: {})".
These are SHAPE-ONLY validators: no disk, no resolver, called on every load. That is why the refusal is loud and early today. A coordinate-shaped from_variant cannot be judged here at all — the donor's declaration is not in hand — so the check necessarily moves to a _in-style validator that takes a resolver (validate_edges_with, variant.rs:3756). Fail-fast degrades unless every door that loads a recipe also calls the resolving validator. That is a real cost, and it is exactly the failure mode gunnar hit in gunnars_todo §11.
L3 — the HAND-OFF has no cross-process form (the hard limit)
stage_variant_artifacts (src/image_build.rs:408) is where "the declaration names a cargo variant" becomes "the image build finds its payload". It takes ONE set and ONE repo_root, resolves the handle at image_build.rs:419:
let v = set.require(vname).with_context(|| {
format!("{label}: chains cargo variant '{vname}', which this repo does not declare")
})?;
and then builds it in-process (image_build.rs:445-449, provenance::build_variant_with_info / chained_variant_in_container) and takes the artifact path off BuildOutcome::artifacts (provenance.rs:509-510) — what cargo actually wrote, never guessed.
That last sentence is the blocker. The path exists only inside the process that ran the build. There is no published location, and no pin, at which a cargo variant's artifact can be found by anyone else. The two kinds that already have one are precisely the two that already cross a boundary:
| kind | published at | pinned by |
| kernel | <repo>/.kernel-out/<name>/{bzImage,kernel.config} (kernel.rs:3156) |
<name>.kernel-pin.json — KernelPin (kernel.rs:3064) |
| iso | <repo>/.iso-out/<name>.iso (image_build.rs:752-762) |
the compose's own attestation |
| exe / lib | nothing | nothing |
Verdict on the kind of limit: it is a PUBLICATION limit, not a path-resolution limit, not a trust limit, and not merely unimplemented. Path resolution is already solved (dwarves::buildthing::resolve_build_thing_from, buildthing.rs:494; BuildVariantSet::validate_edges_in, variant.rs:3815). Trust is already solvable (§3). What is missing is that a cargo variant's built artifact is not an addressable, verifiable thing outside the run that made it.
2. Does kernel_donor generalise? — YES in three of four parts, NO in the fourth
KernelDonor (installer.rs:129-158) is documented as "the rule every cross-repo seam here uses". Its four parts:
| part | generalised already? | evidence |
| the declaration — a NAME PAIR, not a path | YES, and it is already done | EdgeDecl { repo, variant, typ } (tree.rs:257) is the explicit generalisation, live on six kinds (variant.rs:596, iso.rs:229, vm.rs:150, container.rs:285, installer.rs:387, kernel.rs:810). Its doc says so: "The general form of an installer's / iso's kernel_donor" (variant.rs:592-594) |
| resolution — donor checkout beside the consumer | YES | validate_edges_with(own_repo, resolve) (variant.rs:3756) + validate_edges_in(repo_root) (variant.rs:3815); dwarves supplies the real ladder at buildthing.rs:666 resolve_donor_kernel_decision |
| the plan — build the donor first, transitively, cycle-refused | YES | Coordinate { repo, variant } (build_dag.rs:83) and BuildDag::derive, driven from Edge for ANY typ (dwarves/src/build_steps.rs:349) |
| the hand-off — read the donor's BUILT artifact and prove it is the chosen one | NO | resolve_built_kernel (kernel.rs:3199) works only because of .kernel-out/ + KernelPin. Every non-kernel branch of the runner is s.edge.typ == ArtifactKind::Kernel-gated (dwarves/src/build_steps.rs:473, :906, :910): a non-kernel step is BUILT and then nothing stages it |
So: kernel_donor generalises as a declaration and as a plan — and those halves are already generalised and shipping. It does NOT generalise as a hand-off, and the hand-off is the whole of what from_variant means. from_variant is not "name another artifact"; it is "the payload IS the bytes that artifact is". The edge vocabulary answers the first and is silent on the second.
Corollary, and it makes the row smaller than it looks in one direction and bigger in another: cross-repo from_variant should not be a new field at all. It is edges.<feature> of typ exe plus a publication rule. The declaration work is zero; the publication work is the row.
3. THE DESIGN
3a. Resolution — where the other repo is found
Reuse, do not invent. The constellation-layout rule, already spelled twice: <parent of repo_root>/<repo> (variant.rs:3815-3826), with dwarves' richer ladder (buildthing.rs:494 resolve_build_thing_from, which also accepts a repo/variant coordinate) as the door's resolver. edda stays dep-light and takes a &dyn Fn(&str) -> Result<PathBuf> exactly as validate_edges_with does; edda never resolves a checkout on its own.
The consumer never builds the donor. This is already a DECIDED rule for the kernel edge (installer.rs:147-152: "A donor is a CONSUMER of the built kernel, never a second builder"), and it must hold here for the same reasons plus one more: stage_variant_artifacts today would otherwise run cargo inside somebody else's checkout, which cargo_container.rs:575 already forbids ("get to edit another repo's checkout"). The donor's variant is built by dwarves buildthing <donor> --variant <v> — as a --build DAG step (build_dag.rs, already generic over typ) or by hand.
3b. Publication — THE NEW THING, and the only new thing
A cargo variant must land somewhere addressable, with a pin, exactly as a kernel does. Proposed, mirroring kernel_output_dir:
<repo_root>/.build-out/<variant>/
<artifact basename> the bytes cargo wrote
<variant>.build-pin.json BuildPin
BuildPin is not new work either — provenance::BuildOutcome already carries the artifacts and an Attestation; the pin is that attestation plus the artifact's sha256, written to a conventional path. Without this directory there is nothing for a consumer to open, and every alternative (an env var, a staged path, a warehouse lookup) is the workaround being cured.
The ledger seam generalises for free: KernelLedger (kernel.rs:3217) is already a trait so dwarves can serve a pinned artifact from the build ledger before falling back to the donor's local out-dir. ArtifactLedger is the same shape.
3c. Provenance — how a consumer knows the artifact IS the declared variant
Do not invent a second witness format. edda already owns one: contract::Receipt in linker section .nordisk.buildthing (contract.rs:44, contract.rs:189), carrying atom — "The atom this artifact is, e.g. korp-server" (contract.rs:195-196) — plus the build-thing Coordinate { krate, version, hash }, read back without executing the artifact by receipt::read_receipt (receipt.rs:101).
So the consumer's check is three lines: open the pinned artifact, read_receipt, refuse unless receipt.atom == <the declared variant> and receipt.build_thing.krate == "<donor>-build-thing".
Two measured facts make this the right choice rather than a preference:
- korp already built the parallel mechanism, because it could not reach this one.
korp/src/variant.rs(origin/main, commit820660f) embedsKORP-BIN-VARIANT=<name>;padded to 64 bytes via aconst fnwitness, and its own doc (korp/src/variant.rs:43-50) says the format belongs in edda once if it ever grows a second field. It exists because for weeks the RDP appliance shippedkorp-ui(the GPU build) under a path spelledkorp-ui-thin. - The reader korp's doc names DOES NOT EXIST.
korp/src/variant.rs:43-50points attunnr-build-thing::staged;tunnrhas nostagedmodule and no occurrence ofKORP-BIN-VARIANTon any ref (git grep -l KORP-BIN-VARIANT $(git for-each-ref …)→ empty). The witness is written and read by nobody. A provenance mechanism whose reader is prose is the same defect as the path it replaced, one layer up.
Honest limit, stated: the receipt is a plaintext self-declaration in a section, not a signature. It is proof against the failure that actually happened (the wrong sibling artifact staged under a right-looking name) and not against a hostile editor. That is the correct strength for a build seam between checkouts the same operator owns, and the pin's sha256 covers the accidental half.
Second honest limit: embed_build_receipt!() is deployed unevenly — dwarves emits it (dwarves/src/main.rs:8398), gunnar and korp only READ it in tests (gunnar/crates/gunnar-recipe/tests/cli_matrix.rs:303, korp/crates/korp-build-thing/tests/cli_matrix.rs:202). A donor variant that carries no receipt cannot be a cross-repo from_variant donor. That is a precondition to enforce at declaration time, not a warning at stage time.
3d. Schema — and what an OLD reader does with it
RECIPE_SCHEMA = 4 (schema.rs:131), first reader 0.1.14 (schema.rs:178-179). If the capability is spelled as edges (§2's corollary) then no new vocabulary is added at all and this section is about a hole that already exists:
edgeshas no arm inrequired_by(schema.rs:263-354).grep -n edges src/schema.rs→ nothing. So a recipe declaringedgesemitsschema = 1(the floor), andIsoBuild/InstallerBuild/ContainerBuildare notdeny_unknown_fields(schema.rs:59-67) — an older reader silently drops the whole edge table. That is R3's own named disease, live today:tunnr-lockfix/.nornir/build-variants.toml:459,473,487,501declares four[build.kernel.edges.toolchain]tables under no schema header at all.
The fix is a required_by arm naming each edge ("[build.<kind>.edges.<feature>] on '<variant>'"), RECIPE_SCHEMA = 5, and FIRST_READER += (5, "0.1.15").
It is not a tonight edit. FIRST_READER may not name a version that does not exist — the module says so in its own words about the 0.1.11 gap (schema.rs:147-155: "Naming 0.1.11 here would make the refusal message a lie") — so the arm forces nornir-build-thing 0.1.14 → 0.1.15, a crate ~20 manifests across the constellation depend on. And it converts a silent drop into a loud whole-file refusal for every stale installed forge, which is the 2026-09-05 gunnar outage replayed on tunnr (schema.rs:100-120). Correct by doctrine; a release event in practice; Rickard's call, not a lane's.
3e. Failure — it must refuse loudly, never silently produce nothing
Five refusals, each naming a different repair. Modelled one-for-one on resolve_built_kernel's four (kernel.rs:3186-3198), which is the proven shape:
| state | refusal names |
| donor checkout absent | the coordinate, the path looked at, and the layout rule — not "no such variant" |
| donor declares no build space | wrong checkout, not a missing build (variant.rs:3775's wording) |
| donor declares no such variant | the coordinate + the donor's declared names |
| donor's variant is declared but NOT PUBLISHED | the exact line that publishes it: dwarves buildthing <donor> --variant <v> |
| published artifact's receipt disagrees | expected atom vs. the atom in .nordisk.buildthing, and the pin sha vs. the file's — this is the tunnr /app defect, caught |
Two rules govern all five:
- Refuse before building anything. The cheap gate above the expensive one, for the reason
dwarves/src/assemble_cell.rs:490-500measures: 18 m 29 s of cargo before a missingadmin_keywas mentioned. - A missing donor is never a skip and never an empty payload. Today's
iso.rs:458-462("needs akernel, astagingtree, or afrom_variantto be bootable") is the local version of the property; the cross-repo version must be at least as loud, because the failure is now REMOTE and the operator has no reason to look in the other checkout unless told to.
4. VERDICT — filed, not implemented
The row is one publication convention (§3b) away from being small, and that convention is a build-system-wide decision: where a cargo variant's artifact lands, what pins it, whether the ledger or the out-dir is authoritative, and which repos must start emitting a receipt before they may be donors. Landing half of it — a coordinate-shaped from_variant whose validator moves off the load path (§1/L2) and whose hand-off has nowhere to read from (§1/L3) — would trade a loud workaround for a quiet gap in the build system of five repos. The three seats' workarounds are honest and they hold; nothing is on fire.
What is genuinely small and separable, in dependency order:
- The
required_byarm foredges+ the 0.1.15 bump (§3d) — needed by anything that spells a cross-repo edge, and it repairs a live silent drop intunnr-lockfix. Blocked only on the version bump being sanctioned. korp's witness →contract::Receipt(§3c), and a real reader on tunnr's side. Today the guard korp documents does not exist on any tunnr ref..build-out/<variant>/+BuildPinwritten bybuild_variant_with_info(§3b). Additive, no schema, no declaration change, useful on its own.
With 1–3 landed, cross-repo from_variant is edges.<feature> typ = "exe" plus a stage_variant_artifacts that takes (set, root) per dep instead of one pair — which IS a small edge, then.