Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

.nornir/stinky/05-suite-monetize-mvp.md

Stinky lane 5 — the suite, monetize, and the MVP line

Written 2026-09-20 against holger main 542e296, monetize 84f0a8a, monetize-impl b5ab0e9, gunnar 1d0fd62, znippy/edda/modgunn as checked out. Every finding is tagged MEASURED (ran a command / read the line and it is so today), READ (a doc or comment says so; not re-proven here), or DESIGNED (proposed here, no code). No live machine touched, no credential read, no key value anywhere in this file.

The owner's brief this lane answers, 2026-09-20, near verbatim: holger/gunnar/dwarves/mimir/modgunn can become a suite — standalone first, reuse where it helps; the MVP's LAST feature is holger in the cloud with repo A→B and then holger on-prem with repo C; what does not fit becomes holger Pro; monetize exists as an open-source resource-allocation plugin plus vetra/monetize-impl for the paid part (UpCloud + Stripe); and holger needs a plugin for monetize.


A. THE SUITE — where reuse is real and where it would be a coupling mistake

A.0 Vocabulary, fixed before anything else (LAW)

Two words are spelled "suite" and they are not the same word (workspace_nordisk/nornir-workspace.toml:48-59, MEASURED):

word meaning used in this doc as
PRODUCT a member of the nordisk software suite: gunnar · holger · dwarves · nornir · korp · modgunn · skade. A PRODUCT IS NEVER A LIBRARY. "product"
library znippy, znippy-zoomies, facett, Skidbladnir, draupnir, edda, knut — made for reuse, have build-things, have NO product "library"
SUITE (flavour) the suites = [...] list on one [[build.variant]] row: MVP or ORIGINAL. holger is the ONLY product with two. "MVP" / "ORIGINAL", never bare "suite"

mimir is in the owner's list and NOT in the law's product list. MEASURED: find /home/rickard/git -maxdepth 3 -iname '*mimir*' returns no repo and no crate. Mímir is (a) 11 443 lines of compiled Rust INSIDE nornir — nornir/src/mimir.rs, mimir_analyze.rs, mimir_calibrate.rs, mimir_clones.rs, mimir_subclone.rs, mimir_slices.rs, mimir_dup.rs and 23 lens modules under nornir/src/mimir_lens/, exported at nornir/src/lib.rs:269-299, served by a gRPC service (nornir/Cargo.toml:692 MimirSvcTrait); (b) the 72-table Iceberg warehouse those read (nornir/.nornir/mimir-datastructure-manual.md:5,9,30-41); (c) ~25 design docs under workspace_nordisk/.nornir/mimir-* of which the trust lineage (mimir-agent-trust-lineage-design-2026-07-24.md:5 "No code shipped") and "mimir v2 the purgatorium" (workspace_nordisk/mimir-v2.md, code actually lives in edda/crates/nornir-purgatory) are ideas. Its consumer is dwarves (dwarves/crates/dwarves/Cargo.toml:291 mimir = ["coverage"], GUI at dwarves-gui/Cargo.toml:539).

Honest answer: mimir today is a CAPABILITY OF NORNIR — a subsystem plus an idea, not a product and not a library. If the owner wants it in the suite as a peer, it needs a repo, a crate name, a recipe and a row; today nornir is the product and mimir is what nornir's warehouse knows. This doc treats "mimir" as "nornir's code-intelligence warehouse" everywhere below.

A.1 The dependency graph as it IS (MEASURED, Cargo.toml lines)

No cycle among the seven products. The hub is edda/crates/* + libraries. Nothing depends on holger except dwarves, optionally; holger depends on no sibling PRODUCT except modgunn.

edge fact where
holger → modgunn default-features = false, features = ["scan"] — the CVE/licence verdict for PolicyGate; NOT provenance-verify holger/server/lib/Cargo.toml:137 (rationale :132-136)
holger → znippy-common optional = true, only under sign; used for CMS verify of znippy archives before serve server/lib/Cargo.toml:181, repository.rs:512,555,583
holger → Skidbladnir optional, federation ⇒ full only; plus handler-skidbladnir plugin pair (in every tier) server/lib/Cargo.toml:208,429-432, plugin/handler-skidbladnir
holger → edda leaves nornir-hash, jera, nornir-service, nornir-auth-policy, nornir-auth-webauthn (opt), korp-installer 0.1.7 in xtask server/lib/Cargo.toml:152,217,254,270,282; xtask/Cargo.toml:60
holger → skade NONE — two comments about avoiding skade's arrow-59 fork xtask/Cargo.toml:94, server/lib/Cargo.toml:192
holger → nornir (product) NONE —
gunnar ↔ holger NEITHER direction; prose only ("Mirrors holger server/cli/Cargo.toml:150") gunnar/crates/gunnar-server/Cargo.toml:38
dwarves → holger optional holger = ["dep:holger-server-lib"], in-process artifact store; rehearse, embed-holger dwarves/crates/dwarves/Cargo.toml:356,411,536, src/artifact_store.rs:202-216
modgunn → skade (opt), edda leaves, Skidbladnir, znippy-common (opt); no product modgunn/Cargo.toml:131,182,207
nornir → draupnir, Skidbladnir, ~20 edda leaves nornir/Cargo.toml:561,615,590-668

A.2 The standards ARE in the house, and holger's use of them is HALF-TRUE, not none

The brief says "holger uses none of them". MEASURED, it is more nuanced and the nuance is the finding:

standard implemented holger status
DSSE v1, PAE byte-exact znippy/znippy-common/src/sign.rs:1190 dsse_pae, :1347 dsse_sign, :1375 dsse_verify, feature sign (znippy-common/Cargo.toml:44) REFERENCED AND UNWIRED. holger/server/lib/src/attest.rs (1458 lines, commit fa9f5e6 2026-09-19 23:56) delegates wholly to znippy_common::sign::dsse_verify (attest.rs:810-835) and holds AttestStore, Statement, AttestTrust, AttestPolicy. Consumers: security.rs:194,227,676, sbom.rs:45,67,179,183. Nothing on the promote path calls it (grep self.promotion = 4 hits, none a fetch or attest).
M-of-N threshold sign.rs:1045 sign_multi, :1090 verify_threshold (refuses M=0; opt-out confined to :1109) Used by holger-verify (update-bundle) for the bundle path only. Not on promotion.
in-toto Statement v1 + SLSA Provenance v1 edda/crates/nornir-build-thing/src/provenance.rs:297 to_slsa_statement, :361 canonical bytes, seams :421,428 attest.rs:95-116 declares the same _type/predicate constants. Not depended on: server/lib has NO nornir-build-thing dep (only holger-recipe/holger-build-thing do).
verifier, default ON modgunn/src/provenance.rs:700 verify_sealed_provenance; modgunn/Cargo.toml:18 default = ["scan","provenance-verify"] holger pins modgunn with scan ONLY (:137) — so holger deliberately compiles OUT the verifier that is on by default for everyone else. The comment at modgunn/Cargo.toml:54-64 still says "OFF by default" and contradicts :18 (stale).

The reuse trap nobody has measured: the DSSE/M-of-N API is unreachable from crates.io — local znippy-common 0.9.15 vs published 0.9.13 which lacks sign (holger/Cargo.toml:113 patches it in; znippy-common/Cargo.toml:3-6 calls the result "19 holger crates unpublishable fork riders"). Every "reuse znippy's DSSE" sentence in promote-sync-attest-design.md is therefore a sentence about a PATH dep, which is exactly the shape A.4 forbids for a standalone clone.

The seventh-envelope rule holds today. MEASURED the five bespoke containers: KORPSIG1 (edda/crates/korp-installer/src/rollback.rs:105), holger.update-bundle/v1 (holger/update-bundle/src/schema.rs:4, ed25519 over exact bytes), gunnar-sshsig-v2 (gunnar/crates/gunnar-grpc/src/auth.rs:164), monetize's in-object EntitlementFact.signature (monetize/crates/monetize-product/src/lib.rs:453), Skidbladnir .sig.json (Skidbladnir/src/archive.rs:428). attest.rs:26-40 inventories all five and adds none. The unmerged holger metering branches add a SIXTH VERIFIER of an existing envelope (monetize's, via monetize_embed::signing::verify_fact, holger-wt-metering/server/lib/src/metered.rs:110-112) — that is reuse, not a new envelope, and it is the right shape.

A.3 The reuse audit, pair by pair

Rule the owner set: standalone first. So every row says how the standalone case survives. The house answer is almost always a crate in edda (exists to avoid circular deps, nornir-workspace.toml:18), and almost never product→product.

holger ↔ gunnar — two content-addressed stores with the same organs and ZERO shared code

MEASURED: no Cargo edge either way (A.1). Both have: CAS (gunnar gunnar-store, gunnar-core/src/doc.rs:6; holger attest.rs digest-keyed + promote/mod.rs:576 blake3 blob tree), gates (gunnar gunnar-policy/src/lib.rs:173 trait Gate; holger PolicyGate concrete struct promote/mod.rs:66), attestation (gunnar gunnar-core/src/{provenance,key_attestation,transparency,seal}.rs; holger attest.rs), twin (gunnar gunnar-cli/src/control/twin.rs; holger one-medium holger-recipe/src/packaging.rs:258), metering (gunnar gunnar-core/src/entitlement.rs; holger two unmerged branches, B.4), console (gunnar gunnar-server/src/dashboard*; holger holger-front + holger-ui), a clock (gunnar gunnar-clock peer://; holger none, SystemTime).

could be shared how, standalone-safe must NOT
trait Gate { name, scope, check } -> Verdict, Err = refuse, all gates run, refusal names the gate, unreadable policy refuses everything (GUN-007) lift gunnar-policy's trait + engine (no git types) into edda/crates/nornir-gate; gunnar and holger both depend on the edda crate; each ships its own PushContext/PromotionContext holger depending on gunnar-policy (drags gix); copy-paste (the design doc says "copy this trait" — copying is the debt, lifting is the fix)
EntitlementFact verify + the open-build-speaks-no-money guard already a published crate: monetize-embed (publish = true, monetize/crates/monetize-embed/Cargo.toml:19) — this IS the right shape and both consume it a holger-side canonical form (metered.rs says so: "holger writes no second canonical form")
signed time for an airgapped box gunnar-clock peer:// (gunnar/crates/gunnar-clock/src/peer.rs:12-20,68) → move to edda as nornir-clock; holger's expiry checks (update-bundle/src/policy.rs:93-97) are claims about now and today trust the RTC holger inventing a second time protocol
audit log holger's Arrow-IPC hash-chained log (server/lib/src/audit/arrow_ipc.rs) vs gunnar's gunnar-core/src/audit.rs — two append-only logs, two verifiers merging them now; but a shared query surface (C.4) should be designed once
twin the RULE (twin = same build, same config) is shared; the mechanism is not holger copying gunnar's keyless twin row — holger's one-medium answer (variants.rs:264-268) is the stronger one and gunnar should copy holger
holger ↔ dwarves — dwarves builds, holger stores; today it is an env var and a path patch

MEASURED: dwarves/crates/dwarves/src/artifact_store.rs:202 DWARVES_HOLGER_DIR, configured() :214-216 returns None unless feature+env; it links holger-server-lib IN-PROCESS (Cargo.toml:355 "never a twin CAS"). There is no publish verb and no network hop — a forge on dwarf cannot push into a holger on another host; it can only write into a holger directory it can see. The dwarves→holger patch is flagged LOCAL-DEV-ONLY (dwarves/Cargo.toml:23-29).

could be shared how must NOT
provenance: dwarves emits SLSA v1 (nornir-build-thing/src/provenance.rs:297); holger's AttestStore accepts DSSE-wrapped in-toto — the producer and the consumer already speak the same statement and nobody has connected them AttestationService.Attach(repo, artifact, envelope) (DESIGNED in promote-sync-attest-design.md §3.4, not in proto/holger.proto); dwarves' DsseSealer seam (provenance.rs:428) is the hook dwarves linking holger-server-lib to attest (it already links it to store — that is the coupling to unwind, not extend)
the build declaration nornir-build-thing (edda) — already shared, both consume it —
a holger gRPC client crate for dwarves to push over the wire holger-client in holger, thin, published; dwarves depends on the registry version in-process linking as the only door
holger ↔ modgunn — the one real product→product edge, and it is scoped right

MEASURED server/lib/Cargo.toml:137 features = ["scan"]. The verdict is content-addressed by (blob_sha256, policy_version, advisory_db_rev) (READ, promotion-design.md §4). Standalone survives because modgunn 0.0.6 is on crates.io (modgunn/Cargo.toml:3, no publish = false) and version = "0.0" is named beside the path. Stink: holger takes the scanner and refuses the verifier (provenance-verify off) while attest.rs re-implements verification over the same znippy DSSE — one house, two verifiers of one envelope. Either holger turns on provenance-verify and drops its own verify_envelope, or modgunn's verifier moves to edda and both call it.

holger ↔ nornir/skade — nothing today, and the one thing that belongs there is the audit QUERY

⟶ BUILT 2026-09-21 — see .nornir/HOLGER-PLAN.md §4 "the audit query seam — 2026-09-21", which carries the measured can/cannot table for AuditService.Query, the holger.audit.export.v1 layout, the AuditService.ListSegments door, the end-to-end reader proof, and the eight things a lakehouse still cannot ask. The ruling below stands and was implemented as written: holger grew NO query engine and NO edge to skade or nornir — it published the layout and a way to enumerate it, and a reader pulls.

MEASURED: no holger→skade edge; feature-complete-gaps.md:320-325 already rules "a servable who pulled X query belongs in skade". C.4 needs exactly that query. The standalone answer: holger writes Arrow IPC already (audit/arrow_ipc.rs) — skade reads Arrow IPC natively. So the query surface can be holger-audit-query over skade as an ORIGINAL/Pro capability, while MVP's C answer (C.4) needs only a fold over read_dir grouped by ident — no skade, no iceberg, no arrow-59 fork in an airgap binary.

holger ↔ mimir — an idea only

Nothing to share until mimir is a thing (A.0). The one honest link: mimir's vuln_findings are modgunn's, and holger's ScannerGate reads modgunn's advisory feed; if both read one warehouse the verdict is computed once. DESIGNED, far.

A.4 The constraint that kills lazy answers (MEASURED)


B. MONETIZE — two halves, and holger's plugin

B.1 The split as it really is (MEASURED)

OPEN — /home/rickard/git/monetize CLOSED — /home/rickard/git/monetize-impl
remote ssh://git.gunnar.rs:2222/nordisk/monetize (NOT GitHub, though Cargo.toml:24 claims github.com/vetra-automation/monetize) git@github.com:vetra-automation/monetize-impl.git, single remote — confirmed never on gunnar.rs
last commit 84f0a8a 2026-09-19 b5ab0e9 2026-09-19
crates monetize (core: orders, ledger, reserve→provision→capture), -pay, -cloud, -product, -proto, -embed, -server, -ui-server, -ui-web, products/gunnar, products/holger monetize-pay-impl (klarna/stripe/swish), monetize-cloud-impl (upcloud/scaleway + kvm rehearsal estate), monetize-signup-impl, monetize-tax-impl, monetize-server-vetra (the binary)
published ONLY monetize-product + monetize-embed (publish = true, crates/monetize-product/Cargo.toml:21, monetize-embed/Cargo.toml:19); rest publish = false nothing; depends on the open half by RELATIVE PATH ../monetize/crates/* (Cargo.toml:28-33)
money words yes, everywhere — monetize IS the money engine (transaction.rs, invoice/, tax.rs). Stripe appears only in prose (monetize-pay/Cargo.toml:19) Stripe at crates/monetize-pay-impl/src/stripe/mod.rs (HMAC webhook :82, env names :220-223, live/test detection reconcile.rs:84-88), feature stripe (Cargo.toml:20)
UpCloud trait CloudProvider (monetize-cloud/src/lib.rs:155), ApplianceGrow (monetize-product/src/lib.rs:908) crates/monetize-cloud-impl/src/upcloud/{mod,api,grow,price}.rs — grow.rs is the OUTSIDE actor of the data-set-growth flow (detach→resize→re-attach, journaled)
seams PaymentProvider monetize-pay/src/lib.rs:274, SignupSink signup.rs:173, TaxReport monetize/src/tax.rs:474, Product monetize-product/src/lib.rs:598; registration = VendorFactory { name, config_envs, build: fn } (monetize-pay/src/factory.rs:74-84), no trait objects loaded dynamically, no wasm, no dylib implements all four via factory::register; configured-but-not-linked refused by name at boot (monetize-server/src/registry.rs:388-398)

Correction to the brief's framing: the open half is NOT "a resource-allocation plugin with no money in it". The open half is the whole money engine minus the vendors; the product plugins (products/gunnar, products/holger) are the resource-allocation half and are what a product build links via monetize-embed. What carries no money is the product's open build, policed by open_build_speaks_no_money.rs in gunnar (gunnar/crates/gunnar-server/tests/) and copied to both holger branches. "unlimited" has no spelling in monetize (monetize-server/src/oversell.rs:38); on the open appliance it is the ABSENCE of an Allowance (gunnar/crates/gunnar-lfs/src/allowance.rs:64-65).

The live-charge fact, verified as far as a file lets you: reconcile.rs:84-88 key_mode() distinguishes live/test by key prefix at runtime; the vetra front's key is rk_live_, livemode true (memory, not re-read here). No key value is in either tree; the only key-shaped literal is the fake sk_test_not_a_real_key (stripe/tests.rs:40).

B.2 The gunnar scars, each verified

scar verdict evidence
-open answers Entitlement.Set Unimplemented ⇒ paid signups get no quota TRUE, by ABSENCE: the service is #[cfg(feature = "metered")], not stubbed (gunnar/crates/gunnar-grpc/src/service/mod.rs:37-41, 687-701); tonic answers UNIMPLEMENTED; monetize's gunnar plugin reads that as MeteredDoor::Absent (monetize/products/gunnar/src/lib.rs:839-846) the metered ISO is metered_twin() cloning the open installer row with metered_env(monetize_pubkey) (gunnar-recipe/src/recipe.rs:1163-1181, applied :733-735)
--monetize-pubkey must reach the appliance TRUE: gunnar-common/src/cli.rs:507, deliberately not cfg-gated so an open build refuses it by name (:509-537); a metered build started without it logs "every namespace is Free" (gunnar-server/src/serve.rs:1071) boot param korp.env.GUNNAR_MONETIZE_PUBKEY (recipe.rs:1108)
monetize principal must be enrolled (server admin) after re-bootstrap TRUE, documented on the monetize side only: monetize-impl/crates/monetize-signup-impl/src/appliance.rs:26-32; Entitlement.Set requires Access::Server/Admin (gunnar-grpc/src/service/entitlement.rs:165-167). Nothing in gunnar checks a principal named monetize — it is a grant, not a name
ONE unattested key per principal; the console takes it TRUE in shape: gunnar-core/src/key_attestation.rs:147-155 — NobodyToAsk only when the target holds no key; after the first key a holder exists and every later key needs that holder's sshsig the consequence (paid customers cannot add their own SSH key) is a console defect, not a core one
the metered appliance's TWIN is un-metered TRUE: gunnar-twin row gunnar-recipe/src/variants.rs:730, features [], is twin_installer(&open) — the keyless/open row (:725-728); gunnar/.nornir/twin-transaction-log.md:368,384,414 records "T1d entitlements back: UNRUN — the keyless medium refuses Entitlement.Set" promoted, it stops enforcing. holger's answer is already stronger: ONE ISO row for server and twin (holger-recipe/src/variants.rs:264-268, packaging.rs:258) — so holger can never have this scar unless someone adds a second row

B.3 What holger already has toward monetize — two branches doing the same thing (MEASURED)

Stink: two 2 300-line branches with different proto package names and RPC names for the same two doors, four days old, both unmerged, and the design doc that would pick one does not exist. Whichever the owner merges, the other is a git branch -D; the longer both live the more each drifts from main's auth-passkey and front changes (both branches predate b8e13a0).

B.4 DESIGNED — what holger meters, and what it refuses to meter

meter honest unit per repo? policeable on the box? airgapped?
stored bytes (artifact_bytes / archive_bytes) bytes at rest, uncompressed, as ArchiveInfo reports yes yes — a put that would cross the cap is refused before the write; deterministic, offline yes — the cap is a signed fact on disk, the check is local
artifacts (count) entries as ListArtifacts enumerates yes, but 4 backends cannot enumerate (traits/src/lib.rs:352 default empty) — REFUSE the meter for those formats, never report 0 yes yes
repositories count of ron_repo_type rows server-wide yes — refuse Admin.CreateRepository above the cap; but holger's repos are RON config, created at boot, not by RPC — so a cap here is a boot refusal, which bricks the box yes
downloads / bytes served / requests Prometheus counters, no repo label no no — served bytes can be replayed by any reader; and the cap would be on a number the box cannot attribute —
promotions AuditAction::Promote rows yes (repo on the row) yes, but pointless: promotion is the product's reason to exist; capping it sells a gateway that stops gating —
gates evaluated per verdict yes no — a gate is a verifier, its cost is the analyser's, outside holger (design §3) —

Rules that follow (DESIGNED, keep the branch's instinct):

  1. Sell only what the box can refuse. artifact_bytes and artifacts are the two honest meters. repos is honest but its cap is a boot refusal — sell it only as a plan shape (which repos the fact names), never as a runtime cap. Server-wide meters are Measured (shown, tested, never capped) — METERED_CAP_UNPOLICEABLE at the sale, as the branch does.
  2. Airgap = a signed fact with an paid_until, and the box keeps counting. A holger inside a wire cannot phone home, so the plugin cannot pull usage. The fact travels IN with the bundle (holger.update-bundle/v1 already carries payloads + evidence; an EntitlementFact is one more evidence member), and usage travels OUT in the audit segments the courier carries back (ArrowIpcAuditLog::read_dir). Past paid_until the box does what 6b of the design already proposes for expired metadata: read-only, never brick — config field, no default. A meter that cannot be read by the seller for a quarter is still a meter; the invoice is late, not wrong.
  3. The open build shows it and tests it. UsageService.Read is mounted in every build (both branches agree); the guard open_build_speaks_no_money.rs proves no plan vocabulary is in the open string table.

B.5 DESIGNED — the holger plugin for monetize, concretely

There are two things that could be called "holger's monetize plugin" and only one of them is holger's:

what where it lives ABI
(a) the PRODUCT plugin impl Product for HolgerProduct — monetize's side, reads holger's RPCs exists: monetize/products/holger tonic client over the vendored proto; VendorFactory-style fn registration; native, no wasm
(b) the metered DOORS UsageService (open) + EntitlementService (metered only), verifying monetize's Ed25519 fact via monetize-embed exists twice, unmerged (B.3) gRPC services on HolgerGrpc; metered cargo feature; the paid vocabulary in its own proto package so it never enters the open string table

It must be a native seam, not a wasm plugin — three measured reasons.

  1. wasm-plugins is OFF in every tier of both suites (server/lib/Cargo.toml:448-462; wasmtime 27, 17 advisories, 4 sandbox escapes). Holger's plugin ABI (plugin/abi/src/lib.rs:152) is a package handler ABI — format logic over host-owned bytes. It has no host imports for "refuse this put", "read this counter", "verify this signature against this key". Adding them is a new ABI, not a plugin.
  2. monetize itself has no dynamic loading anywhere — "no dylib, no wasm, no libloading" (monetize-pay/src/factory.rs:20-22) — and its registration contract is three fields and a function pointer. The gunnar plugin is native. A wasm holger plugin would be the odd one out in its own family.
  3. The metered half must be ABSENT from the open build, not disabled — gunnar's rule, adopted by both holger branches. A cargo feature achieves absence; a wasm file present-or-not on disk does not (the host would still carry the vocabulary to load it).

Concrete proposal (patch sketch, DESIGNED):

holger/server/lib/Cargo.toml
  [features] metered = ["dep:monetize-embed", "dep:monetize-product"]   # registry deps, both published
  monetize-embed   = { version = "0.x", optional = true }   # NOT path — standalone clone rule (A.4)
  monetize-product = { version = "0.x", optional = true }

proto/holger.proto               UsageService.Read(repository) -> readings[{meter, value, scope, source}]   # every build
proto/holger-metered.proto       EntitlementService.{Set,Get,List}                                            # package holger.metered.v1, `metered` only

holger-recipe/src/variants.rs    mvp-server-metered = mvp-server row + "metered" feature, SAME argv otherwise (gunnar's metered_twin shape)
                                 holger-appliance-iso stays ONE row (server + twin) — the metered twin is the metered build by construction
xtask installer                  --monetize-pubkey <hex|path>  → boot param korp.env.HOLGER_MONETIZE_PUBKEY; open build refuses the flag BY NAME
server/lib/tests/open_build_speaks_no_money.rs   three guards (clap surface, string table, live output) — already written on both branches

Pick ONE branch (recommendation: feat/metering-doors — its Set order shape→policeable→signature is the one that carries the refusal-at-the-sale, and its proto package is named for the product state not the RPC), rebase it over 542e296, and land it as slice 0 of Pro (C.2) — it is Pro, not MVP, because MVP is the airgap posture and the fact-in-bundle path (B.4 rule 2) is what makes it airgap-safe, and that path is not written on either branch.


C. THE MVP LINE

C.1 What is settled (READ, do not re-argue)

C.2 ONE TABLE — every capability → MVP / Pro / neither

MVP's virtue is slimness. A row is MVP only if the last feature (A→B in the cloud, C on-prem receiving B) does not work without it. "Compiled in today" is not an argument — 17 backends and the marketplace are compiled into holger-mvp today and most of them should not be.

capability today (MEASURED) line reason
raw/generic repository non-optional, both MVP Henrik's MVP needs only the raw door (SUITES.md:241-244); A, B and C can all be raw
rust (crates.io-shaped) repository non-optional MVP the house's own artifacts; ClosureGate is computable only here (design §6c)
maven3, pip, npm, nuget, go, gem, deb, rpm, helm, docker/oci, conda, composer, group non-optional, in holger-mvp Pro zero of them are needed for A→B→C; each is attack surface on a zero-egress node; making them optional is a server-lib change SUITES.md already names as "real hardening"
bearer + mTLS auth mini MVP the only identity the chain needs (design §6a)
passkey (auth-passkey) named on mvp rows since 2026-09-20 MVP the one login an airgap can have; pure Rust p256
OIDC, OAuth2 full only Pro need an IdP; an IdP is outside the wire
RBAC (Reader<Writer<Admin) always compiled, config-gated MVP who may promote into B / read A is the gate's identity model
audit log (append-only, hash-chained) always compiled MVP it is the ONLY record of "what C has" (C.4); without it the last feature has no proof
audit QUERY surface ABSENT (no RPC, no route; feature-complete-gaps.md:320-325) MVP the last feature's UI question "what did I send C" is a fold over AuditEvent{ident, action: Download, repo, artifact, status, bytes} — 60 lines over read_dir, not skade
promotion (seal, blake3 CAS, exclusive ref) always compiled, ungated CLI verb MVP it IS A→B
stage ladder + withdraw wired via RON promotion: (lib.rs:1275) MVP A→B is the ladder with two rungs
to_repo resolved to a REAL repository (served) ABSENT: sanitized only; fetch has no non-test caller; promotion to ../../pwn succeeds (promote/mod.rs:812-826) MVP B must be something C can pull. A promoted store nothing serves is not a repository B
Gate trait + PolicyEngine (all gates run, Err = refuse, GUN-007) ABSENT as a seam (PolicyGate concrete, promote/mod.rs:66) MVP ruled 2026-09-19; MVP's slim version = the compiled-in verifier gates only
ScannerGate (modgunn CVE/licence) PolicyGate::from_env, fail-closed MVP already there, already scan-only
AttestationGate + DigestGate + AttestationService.Attach attest.rs present, unwired; no RPC MVP "verify, never execute" is the whole airgap argument; this is the smallest gate that makes B trustworthy
ClosureGate, SetSignerGate, PromoteSet (N artifacts atomic) ABSENT Pro valuable, but A→B of one artifact at a time proves the line; a set verb can ship later without changing the wire of the single one
per-verdict DSSE statements + SLSA VSA ABSENT (VSA absent everywhere in the house) MVP (VSA only) C verifies ONE object; the per-verdict N×M statements are Pro (design §6c storage cost)
holger.repo-metadata/v1 (freshness, sequence, thresholds over update-bundle) update-bundle has all four primitives; not applied to repos MVP it is what makes "C has B" a checkable claim rather than a directory listing
bundle import / verify (secure_update, holger-bundle, holger-verify) MVP today MVP MVP's transport is the carried bundle — this is C's intake
holger-remote upstream type (C pulls B) ABSENT: 0 hits; ReplicationService.Replicate same-process only OPEN — C.3 this row is where the owner decides what MVP means
replication (replicate_pairs, same process) always compiled MVP it is the verb §5 unifies promotion into; dry-run default
nexus/artifactory/azure/aws remotes (federation) full only Pro upstream harbours are the cloud's business, not the gateway's
rust-remote (crates.io pull-through) needs online Pro egress
ProxyBackend cache code in both, dead without online Pro egress
holger-agent (offline courier, own store) MVP + ORIGINAL MVP the medium between B and C when there is no wire
holger-agent-online, emit-rsync/rclone ORIGINAL Pro egress
SBOM attach/fetch always compiled MVP the CycloneDX predicate is one of the three the design requires
properties (+ quarantine predicate) always compiled MVP PropertyGate is the existing require_property; quarantine is a fetch-boundary refusal C wants
search always compiled Pro nobody searches a gateway; C's operator lists
retention/GC always compiled Pro a sealed promoted store is immutable by design; GC is a full-registry concern
checksum-deploy headers always compiled MVP ingest into A must bind bytes to a digest the caller claims — cheap, already there
metrics /metrics always compiled MVP operators of C need liveness; it is also the usage meters' source
marketplace fulfilment (Azure, Entra sign-in, webhook) pub mod marketplace UNCONDITIONAL (lib.rs:68), doors need online Pro — and a #[cfg] an appliance behind a wire compiling Azure Marketplace fulfilment is the exact opposite of slim; today it is in holger-mvp
front public page full + server-local only Pro already out of MVP; correct
appliance / installer / ISO / one-medium twin / signed boot MVP only MVP it is how C exists on-prem
embedded tier (in-process lib) ORIGINAL Pro dwarves/nornir consumers, not the gateway
wasm plugins OFF everywhere neither blocked on wasmtime; and gates will never be wasm (design §3)
linked handler plugins (bundle-core, rust-toolchain, skidbladnir) every tier MVP skidbladnir bundles are a real intake format; the other two are tiny
UsageService.Read (open meters) two unmerged branches Pro (slice 0) the open build should show storage (law), but the last feature does not need it
EntitlementService + metered feature two unmerged branches Pro money is Pro by definition; the fact-in-bundle airgap path is unwritten
signed time (peer://) ABSENT in holger Pro, flagged every expiry check in MVP trusts the RTC (design §6b.4); an owner may rule this MVP once C is real
three UIs (egui desktop, web console, BFF) other repos MVP: ui-mini only the last feature needs the UI to say sent (C.4); the console and BFF are cloud-side, Pro
mannequin, bench, testmatrix dev surfaces neither not shipped
HA/clustering, S3 backend, multi-tenancy, webhooks/event bus, cosign/Rekor hosting, long-tail formats ❌ in feature-complete-gaps.md Pro / neither none touches the line

Headline: MVP = raw + rust, bearer/mTLS/passkey, RBAC, audit + QUERY, promote into a REAL served B with verifier gates and a VSA, signed repo metadata, bundle import, agent, appliance. Fifteen format backends, the marketplace, federation, OIDC, search, retention, metering, the web console are Pro. Three of MVP's rows are ABSENT today (served B, gate seam, audit query) and one is an open decision.

C.3 THE OPEN TENSION — do not read past this row

MEASURED: build-space/tests/auth_ship_small.rs:86-136 walks the REAL resolved tree (cargo tree -e normal, invoked via Command::new(env!("CARGO")) — a test process, and the one place holger's own law about shelling is bent) and fails if mini, embedded or holger-verify link reqwest/ureq/curl, with the non-vacuous anchor that full MUST. :293 proves the MVP appliance row links the passkey door and no HTTP client. The guard skips silently when cargo is absent (:117) — a cargo-less CI passes it vacuously.

And the last feature says C, on-prem, receives B. There is no holger-remote upstream type (0 hits), and the two ways C can receive are:

option what MVP means afterwards consequence
(1) C's fetch is NOT an MVP posture. C receives B as a carried bundle (holger-bundle on the cloud side, holger-verify + bundle import on C). "holger on-prem with repo C" = a holger whose C is filled by import, and the audit row that proves "what C has" is written by the courier's import, not by a fetch B served. zero-egress guarantee stays exactly what it says: no HTTP client in the binary. The upstream does NOT know what C has (it never served a fetch) — C.4's ruling "the upstream knows because it served the fetch" is FALSE under (1); the truth lives in C's audit log and travels back on media. The UI on the cloud side can only say bundled, not sent.
(2) C PULLS from B over the wire. A holger-remote backend (a RepositoryBackendTrait impl speaking holger's own gRPC, mTLS, per-credential) so replicate_pairs((remote B, local C)) is the sync — design §5's unification. zero-egress means something NARROWER than it says: "no outbound HTTP to the internet"; the binary links a gRPC client (tonic already links hyper — the guard's forbidden list is reqwest/ureq/curl, and tonic's client half is not on it, so the guard would stay green while the posture changed). The upstream DOES know what C has: AuditEvent{ident: C, action: Download, repo: B, ...} grouped by ident. The UI can say sent. But a C that reaches upstream is a C with a route out of the wire, and the customer buying "airgap" is buying (1).

Both are coherent. They are different products. (1) is the Secure Update Gateway as sold; (2) is a mirror. The design doc chose (1) in words (§6a.2 "MVP transport is the carried bundle") and (2) in architecture (§5 "sync down, two holgers — the same function"). The brief's own C.4 ruling ("the upstream knows because IT SERVED THE FETCH") presumes (2). This is the owner's decision and this doc does not make it. What it does say: under (2) the egress guard must be rewritten to name tonic's client half explicitly as allowed to one configured peer or it is measuring the wrong thing; under (1) the "what C has" surface is C's audit log carried back, and the cloud UI must say bundled.

C.4 What MVP is MISSING to reach the last feature (MEASURED, each)

  1. No holger-remote upstream. repository.rs:281-511 dispatches 21 string kinds; none is a holger. ReplicationService.Replicate is same-process (replication.rs:168). Needed under (2) only; under (1) the courier is the remote.
  2. PromotionEngine::fetch has no non-test caller (promote/mod.rs:699; callers at :776…:1437, all #[cfg(test)]). The promoted store is write-only in production. Slice 3 of the design (make Holger::fetch consult it) is the smallest fix; §5's real fix (B is a real backend, the seal becomes a WriteOnce wrapper on put) is the right one.
  3. to_repo is sanitized, never resolved (promote/mod.rs:294,537-545; grpc/services.rs:920 uses it only as ACL scope). A promotion into a repository that does not exist SUCCEEDS — the test crafted_to_repo_cannot_escape_prod_root (:812-826) asserts exactly that for "../../../../outside/pwn". Only the optional ladder refuses an undeclared rung (stages.rs:235-243, boot check lib.rs:520+); with no promotion: block "any hop is permitted" (lib.rs:1280). Fix: seal takes &Repository, not &str; the gRPC handler resolves through FastRoutes and answers NOT_FOUND by name.
  4. The audit log has NO query surface. trait AuditLog is record + flush (audit/mod.rs:212-218); reads exist only offline (arrow_ipc.rs:299,377,539); proto/holger.proto has no Audit service; no /-/audit route. "What does C have" — under (2) — is AuditEvent{ts_nanos, ident, action: Download, repo, artifact, status, bytes} grouped by ident, filtered status == 200, over read_dir. Under (1) it is the same fold over C's own segments after the courier brings them back. Either way: AuditService.Query(ident?, repo?, action?, since) → rows, ADMIN-gated, MVP. The UI must say sent (a Download row with status 200 and the byte count equal to the artifact's size), never accepted: accepted is C's verify verdict, which the upstream does not have under either option, and the two differ exactly when C refused what B served.
  5. The gate seam is a struct (PolicyGate, promote/mod.rs:66,122) — slice 4.
  6. attest.rs is unwired to promote — no AttestationService, no AttestationGate — slice 7/8. And its DSSE verify needs znippy-common 0.9.15 published first (A.2) or the MVP clone does not build alone.
  7. MVP has no signed time; every expires_at check is SystemTime. Flagged, not blocking (design §6b.4).

C.5 Sequence (DESIGNED — the order that keeps every step landable)

  1. Owner rules C.3 (1) or (2). Nothing below changes except step 5.
  2. Publish znippy-common 0.9.15 via dwarves crate-release; drop the patch at holger/Cargo.toml:113. (Standalone clone rule.)
  3. seal(&Repository, …); promotion into an undeclared repo is NOT_FOUND; flip the :812 test's expectation. (C.4.3)
  4. Holger::fetch serves the promoted store (slice 3); then B becomes a real backend with a WriteOnce wrapper (§5). (C.4.2)
  5. AuditService.Query over read_dir, grouped by ident. (C.4.4)
  6. Under (2): holger-remote backend + rewrite the egress guard to name the one allowed peer client. Under (1): nothing — bundle import writes the audit row; the cloud UI word is bundled.
  7. trait Gate lifted to edda/crates/nornir-gate; holger's ScannerGate, DigestGate, PropertyGate, AttestationGate. (slices 4, 7, 8; VSA only)
  8. holger.repo-metadata/v1 over update-bundle. (slice 9)
  9. Make the 15 non-MVP backends and the marketplace #[cfg]-optional; mvp-server names raw+rust. Measure the binary before and after.
  10. Pro slice 0: merge ONE of the two metering branches (B.5).

D. The five worst findings, ranked

  1. A promotion to a repository that does not exist succeeds — and a test asserts it (promote/mod.rs:812-826); to_repo is a directory name, not a repository, and the promoted store has no reader outside tests (:699). The product's core verb writes into a side tree nothing serves. MEASURED.
  2. The last MVP feature is undefined until C.3 is ruled, and the house's own documents have already taken both sides (design §5 vs §6a.2; the brief's "upstream served the fetch" presumes pull). Building before ruling builds the wrong product with a green egress guard. MEASURED + READ.
  3. "What does C have" has no surface: the audit log is write-only over the wire (audit/mod.rs:212), no Audit RPC, no route. Under either C.3 option the answer is a fold nobody has written. MEASURED.
  4. Two unmerged 2 300-line branches implement holger's monetize doors twice, one hour apart, different proto packages and RPC names (f0ce959 vs f6ff7ec), and main has neither — so monetize-product-holger refuses push_entitlement for a true reason. MEASURED.
  5. The MVP binary is not slim: 17 format backends non-optional and pub mod marketplace unconditional (lib.rs:68) — Azure Marketplace fulfilment compiles into an airgap appliance; and holger refuses modgunn's default-on verifier (scan only) while re-implementing DSSE verification over a znippy-common that crates.io does not have (0.9.13 lacks sign). MEASURED.

Runner-up: holger-errcode 0.1.2 was published today from a commit (bb2b22e) that is not on main; the working tree says 0.1.1. The registry moved and the trunk did not.