Stinky lane 9 — the MVP line redrawn, and feature honesty
Written 2026-09-20 against holger
main542e296(worktree/home/rickard/git/holger-wt-mvpline, branchstinky/mvpline). Tags: MEASURED = a command was run / a line was read and it is so today. READ = a document or an earlier lane says so, not re-proven here. DESIGNED = proposed here, no code beyond what this branch commits.This REVISES
.nornir/stinky/05-suite-monetize-mvp.md(branchstinky/suite, commit5eccdaa). It does not replace it: sections A (the suite / reuse audit) and B (monetize) of that document stand unchanged. What is redrawn is its section C — the MVP line — because the premise it drew under was overturned.
0. THE RULING THAT MOVED THE LINE
Owner, 2026-09-20, verbatim:
"MVP behöver inte vara superslim storleksmässigt, men t.ex. inga egui-grejer och annat — funktionell, men inte science fiction. Kanske inte mimir/modgunn inbyggt t.ex."
Lane 5 drew its line under a different premise, and said so in as many words: "MVP's virtue is slimness" (05-suite-monetize-mvp.md, §C.2 preamble). That sentence is now false. Size is no longer the test. The tests are:
| # | test | what it admits | what it refuses |
| T1 | functional, not science fiction | a capability that works end to end, however large | a capability that is a sketch, however small |
| T2 | no GUI baggage | a screenless capability | egui, and anything whose reason to exist is a screen |
| T3 | maybe not mimir/modgunn built in | — | ⚠ an instruction to INVESTIGATE, not a verdict. §1 answers it. |
| T4 | the last MVP feature is fixed | holger in the cloud with repo A→B, then holger on-prem with repo C | everything that does not fit ⇒ holger Pro |
T1 and T4 pull in opposite directions and that is deliberate. T4 fixes the outer edge: nothing beyond A→B→C is MVP. T1 fixes the inner edge: nothing inside it may be a sketch. A row is MVP when it is inside T4 and passes T1 and T2. A row that is inside T4 but is a sketch is neither — it is work, not a tier. A row that works but is outside T4 is Pro.
The consequence for lane 5: every row it filed as Pro with size as the argument has to be re-argued on function, and most of them move. Every row it filed as MVP because it was small has to survive T1, and three do not.
1. T3 ANSWERED — mimir and modgunn, investigated not assumed
1.1 mimir — "not built in" is already true, at zero cost (MEASURED)
| fact | evidence |
| there is no mimir repo | ls /home/rickard/git | grep -i mimir → empty |
| there is no mimir crate, anywhere | grep -rn 'name = "mimir"' --include=Cargo.toml over /home/rickard/git (target/ excluded) → 0 hits |
| mimir is source inside nornir | nornir/src/mimir*.rs = 11 443 lines (7 files); nornir/src/mimir_lens/ = 11 673; nornir/src/bench_mimir.rs = 6 107. src/mimir* as a glob = 23 116 lines; all mimir-named sources = 29 223 across 32 files |
| mimir is a cargo FEATURE — in dwarves, not nornir | dwarves/crates/dwarves/Cargo.toml:291 mimir = ["coverage"], :292 mimir-fat, module crates/dwarves/src/lib.rs:178; the GUI enables it dwarves-gui/Cargo.toml:539. nornir's own manifest has no mimir feature — only prose at nornir/Cargo.toml:9,692 |
| mimir IS a wire surface | edda/crates/nornir-wire/proto/nornir.proto:771 service Mimir { with 29 rpcs; served by nornir/src/server_grpc.rs:84,2840,2865,6946 |
| holger ↔ mimir | grep -rn mimir holger --include=Cargo.toml --include=*.rs --include=*.proto --include=*.lock → 0 matches. The only occurrence of the string in the whole repo is one design note, .nornir/artifactory-parity-design.md |
VERDICT — mimir: neither, and the cost of "not built in" is ZERO. There is no dep edge, no feature, no proto import, nothing to remove. The ruling's "kanske inte mimir inbyggt" is a description of holger as it already is. It is worth saying out loud what the alternative would have cost, so the question is closed rather than re-asked: linking mimir means linking nornir, and holger has no nornir edge at all (05-suite-monetize-mvp.md §A.1, READ) — the modgunn dep exists partly to break that cycle (server/lib/Cargo.toml:136, MEASURED). mimir is the one T3 candidate with no argument on either side.
⚠ One correction to lane 5, which called mimir "~11 000 lines": that is the top-level mimir*.rs files only. The glob is 23 116, and with the bench 29 223. The number does not change the verdict, it strengthens it — mimir is a large subsystem of another product, not a component holger could "include".
1.2 modgunn — "not built in" would cost the product's core verb (MEASURED)
modgunn is the opposite case: it is linked, and it is load-bearing.
| fact | evidence |
| the ONE edge, non-optional | server/lib/Cargo.toml:137 modgunn = { version = "0.0", path = "../../../modgunn", default-features = false, features = ["scan"] } — no optional = true, and no #[cfg(feature = …)] at any call site; server/lib/src/lib.rs:90 pub mod security; is unconditional |
| what it costs | cargo tree --no-default-features --features scan in modgunn = 30 crates, so 29 added. Of those, ~21 arrive via znippy-zoomies (futures*, rotaryengine, memmap2, zmij) — scan itself is scan = [], a pure gate; serde+serde_json alone would be ~8 |
| where holger calls it | adapter server/lib/src/security.rs:21-26 (pub use modgunn::core::{ArtifactRef, Decision, Finding, Severity, Verdict}); SBOM sbom.rs:71 — holger's hosted CycloneDX bytes literally ARE modgunn::sbom::to_cyclonedx (asserted sbom.rs:576,593); PolicyGate promote/mod.rs:43,66-73,115,122,526; the gRPC promote path grpc/services.rs:1043-1051 (412 on block); the holger-server scan verb server/cli/src/main.rs:126-130,525-528,1381,2162-2196 |
| the UI pane does NOT link it | holger-front has no modgunn dep; modgunn appears in exactly ONE Cargo.toml in the repo. The "Security pane" reaches it through server-lib (holger-build/src/lib.rs:11-12,30,249,259) |
| is there a service to reach instead? | No. modgunn has no .proto, no tonic/axum/hyper dep, no client crate. Its only binary is a CLI with required-features = ["warehouse", "scan"]. modgunn/src/service.rs:1-20 declares a skidbladnir ServiceSpec and says in as many words that modgunn "has no long-running serve loop yet" (DEFAULT_ADDR 127.0.0.1:50071, :26) |
| publishable | modgunn 0.0.6 is on crates.io (no publish = false; the artifact is in the local registry cache). holger pins version = "0.0" beside the path, so a registry-only build resolves |
So "not built in" has exactly three readings, and they cost three different things:
| reading | what it means concretely | cost | verdict |
| (a) not LINKED — made optional | optional = true + scanner = ["dep:modgunn"], #[cfg] on security.rs, PolicyGate, the scan verb and the SBOM re-export |
−29 crates. But sbom.rs:71 re-exports modgunn's CycloneDX writer as holger's own format, so holger would need a second SBOM writer or lose SBOM hosting; and PolicyGate is the promote gate, i.e. T4's own path |
REFUSED on T1. A gateway whose gate is compiled out is not a functional gateway, it is a file server. This is not a size decision, it is the product |
| (b) not linked, reached as a SERVICE | a modgunn daemon holger calls over gRPC | modgunn has no server (service.rs:1-20). Building one is new work in another repo, and then C — the on-prem airgapped holger — must reach it, i.e. a network hop out of the wire |
REFUSED on T1 and on T4. Today it is science fiction: the seam is declared, the loop does not exist |
| (c) not built in = do not take modgunn's HEAVY engines | keep default-features = false, features = ["scan"]; never scip / warehouse / warehouse-sql / rustsec / manifests / framework |
already true, MEASURED at :137 |
THIS IS THE READING THAT HOLDS. It is what the manifest already says, and the comment at :132-136 already says why |
VERDICT — modgunn: MVP, linked, scan only. Recommend the owner rule (c) explicitly so the question stops being re-opened. One genuine stink survives from lane 5 §A.2 and is NOT a size argument: holger takes modgunn's scanner and compiles OUT its provenance-verify (on by modgunn's own default) while re-implementing DSSE verification in attest.rs over znippy-common. One house, two verifiers of one envelope. That is a T1 failure of attest.rs, not of modgunn — see row AttestationGate in §2.
2. ONE TABLE — every capability → MVP / Pro / neither
Read the columns like this. line is the verdict. why (FUNCTION) never argues from size — if a row's old reason was "too big", it has been replaced. moved? says what lane 5 (05-suite-monetize-mvp.md §C.2) filed it as and why it changed. ⚠ marks a row that is MVP and does not exist yet: inside T4, so it must be built, but a claim that MVP is done while it is absent is false.
2.1 Repository backends — the biggest movement in the table
Lane 5 filed all 15 non-raw/non-rust backends as Pro, with the reason "zero of them are needed for A→B→C; each is attack surface on a zero-egress node". The first half is a T4 argument about the DEMO, not about the product — A, B and C are repositories of some format, and nothing says which. The second half is a size/surface argument, which the ruling retires. So all 15 get re-argued on T1, and MEASURED backend by backend (17 crates, server/repository/*, all non-optional in server/lib/Cargo.toml:42-77):
| capability | line | why (FUNCTION) | moved? |
| raw/generic | MVP | the floor; and the ONLY backend implementing coordinate_for_path (server/lib/src/raw.rs:430,434), i.e. the only one a property/quarantine gate can actually address |
no |
znippy-rust (cargo sparse) 1534 LOC, 28 tests, cargo publish PUT |
MVP | full sparse-index protocol incl. a writable api base (lib.rs:530-546); real list, has_archive, archive_files, archive_info — the only backend besides build-thing that overrides all four |
no |
| file-rust 1176 LOC, 19 tests | MVP | the only backend implementing delete_artifact (lib.rs:402) — i.e. the only one retention can write against |
no |
| file-oci 1212 LOC, 18 tests | MVP | docker push works, not only pull: chunked blob upload at lib.rs:590 |
MOVED Pro→MVP — it is a complete, tested implementation of a real protocol. The old reason was surface |
znippy-deb 1261 LOC (real .deb control codec), 15 tests |
MVP | synthesizes dists/{d}/Release + Packages from the typed view (lib.rs:353-377,426-453) — apt installs from it |
MOVED Pro→MVP (T1) |
| znippy-rpm 1151 LOC (real RPM header parser), 13 tests | MVP | repodata/repomd.xml + primary.xml with correct sha256 (lib.rs:268-284,339-366) — dnf verifies it |
MOVED Pro→MVP (T1) |
znippy-helm 1076 LOC (582-line index.rs), 19 tests |
MVP | generates and caches a real index.yaml (lib.rs:180-197) |
MOVED Pro→MVP (T1) |
| znippy-maven 824 LOC, 13 tests | MVP | synthesizes maven-metadata.xml and .sha1/.md5 over the same bytes (lib.rs:446-492) — without that checksum synthesis mvn fails on a bare-jar mirror, and it is there |
MOVED Pro→MVP (T1) |
znippy-python 677 LOC, 11 tests + tests/uv_airflow_journey.rs |
MVP | PEP 503 + PEP 691 served; there is an end-to-end uv journey test. The wart: PEP 691 is reachable only via an explicit /json suffix because the trait surfaces no Accept header (lib.rs:288-292) — a wart, not a stub |
MOVED Pro→MVP (T1) |
| znippy-conda 690 LOC, 6 tests | MVP | real repodata.json from the typed view, 404 on an unknown subdir (lib.rs:297-347); the old empty-dict stub was replaced (lib.rs:48) |
MOVED Pro→MVP (T1) |
| znippy-gem 624 LOC, 6 tests | MVP | real compact index /versions, /info/{name}, /gems/{f} (lib.rs:285-331); the versions md5 column is a literal -, bundler-tolerated and documented (:225-229,250-255) |
MOVED Pro→MVP (T1, with a named shortcut) |
| znippy-npm 536 LOC, 6 tests | MVP | real packument synthesis, scoped names parsed (lib.rs:231-273); no archive_files/archive_info |
MOVED Pro→MVP (T1) |
znippy-package (own .znippy format) 553 LOC |
MVP | there is no third-party client to satisfy; it serves its own format completely | MOVED Pro→MVP (T1) |
znippy-build-thing 449 LOC, 8 tests incl. serves_a_real_build_history.rs |
MVP | the most complete method coverage of any backend (list, has_archive, archive_files, archive_info), traversal-rejecting router (lib.rs:252-318). It serves the provenance log A→B→C is about |
MOVED Pro→MVP (T1) — and it was the worst row to have filed under "attack surface" |
| znippy-docker 418 LOC, 4 tests | MVP ⚠ | docker pull of a pinned reference works, digest echoed correctly (lib.rs:143-153). But it cannot enumerate: no list override, so list_artifacts silently returns empty (traits/src/lib.rs:343-346) |
MOVED Pro→MVP, flagged: an un-enumerable repository is a repository whose contents the audit surface and the storage meter cannot state |
| znippy-go 324 LOC, 4 tests | MVP ⚠ | GOPROXY fetch of a pinned version works (lib.rs:63-114); no @latest, no list |
MOVED Pro→MVP, same flag |
| znippy-nuget 383 LOC, 5 tests | neither | the source says it itself (lib.rs:119-128): the service index advertises only PackageBaseAddress, no RegistrationsBaseUrl, no SearchQueryService, so dotnet restore of a normal project — dependency graphs, version ranges — does not resolve. Exact pins only |
STAYS out, new reason: not "surface", but T1. It is a sketch that compiles |
| znippy-composer 305 LOC, 3 tests | neither | packages.json is a 2-line hand-formatted string (lib.rs:81-88); /p2/{vendor}/{pkg}.json is pure archive passthrough with no metadata builder anywhere in the crate; there is no as_composer() typed view. Unless someone pre-bakes upstream p2/*.json into the archive, every composer install 404s at lib.rs:94 |
STAYS out, new reason: the purest T1 failure in the repo. No todo!() anywhere under server/repository/ — the sketchiness is by omission, which is why nobody grepped it |
| group (virtual aggregate) | MVP | forwards coordinate_for_path (group.rs:56,70); it is how B can be "several repositories seen as one" |
MOVED Pro→MVP (T1) |
| proxy / ProxyBackend cache | Pro | forwards the coordinate methods (proxy.rs:469,473), so it is not a sketch — but it is dead without online, and neither the cloud A→B hop nor the on-prem C needs an upstream. T4, not size |
reason changed (was "egress"/size; now T4) |
Three consequences of the backend sweep that are findings in their own right:
has_archive()lies on 11 archive-backed backends. Onlyznippy-rust(lib.rs:495) andznippy-build-thing(:420) override it; maven, python, npm, gem, deb, rpm, helm, conda, composer, docker, go, nuget and package all hold a znippy reader and inheritfalsefromtraits/src/lib.rs:352-354. Any caller asking "is this repository archive-backed" gets a wrong answer on 11 of 17 — including thearchive_bytesmeter lane 5 §B.4 called one of the two honest ones.- Serve-time quarantine cannot be switched on at all. Zero of the 17 backends implement
coordinate_for_path/maps_paths_to_coordinates; the only implementations in the whole server areraw.rs:430,434plus the two forwarding wrappers. The source says the consequence out loud —exposed/http.rs:1880-1886"on every other backend the gate was decorative" — andlib.rs:2040-2060makes the server refuse to boot whenHOLGER_QUARANTINE_PROPERTYis set. - Four backends cannot enumerate (composer, docker, go, nuget) and it is the same four that overrode nothing optional at all. "Cannot enumerate" and "wrote the bare minimum" coincide exactly, which is a good smell test for the next backend somebody adds.
2.2 Everything else
| capability | line | why (FUNCTION) | moved? |
bearer + mTLS (mini) |
MVP | the identity the promote gate checks | no |
passkey (auth-passkey) |
MVP | the one login an airgap can have; pure-Rust p256, no FFI. Works end to end — 542e296 proved it by measuring its ABSENCE (a 404 Unknown repository on /-/auth/passkey/config) |
no |
| OIDC, OAuth2 | Pro | they need an IdP, and neither the cloud A→B hop nor sealed C has one to reach. T4 | reason changed (was "an IdP is outside the wire" — same answer, now stated as T4 rather than tier) |
sign (CMS provenance verify, znippy-common) |
MVP | verification is the product. But no variant names it (server/cli/Cargo.toml:71) — see §4.1. Its MVP-ness is not in doubt; its honesty is |
NEW ROW — lane 5 never listed it, because nothing declares it |
| RBAC (Reader<Writer<Admin) | MVP | who may promote into B | no |
| audit log (append-only, hash-chained, Arrow IPC) | MVP | the only record of what C received | no |
| audit QUERY surface | MVP ⚠ | ABSENT: trait AuditLog is record+flush (audit/mod.rs:212-218), no AuditService in proto/holger.proto (MEASURED: 0 hits), no route. T4's own question "what did I send C" has no answer |
reason changed: lane 5 argued "60 lines, not skade" — a size argument. The real argument is that T4 is unanswerable without it |
promotion (seal, blake3 CAS, exclusive ref) |
MVP ⚠ | it IS A→B — and it is not functional end to end. PromotionEngine::fetch has no non-test caller (promote/mod.rs:699), so the promoted store is write-only in production; and to_repo is sanitized but never resolved, so a promotion into a repository that does not exist SUCCEEDS and a test asserts it (:812-826) |
MOVED MVP → MVP ⚠. Lane 5 listed the defect in its §C.4 but still scored the row plain MVP. Under T1 the product's core verb is a T1 failure until B is a thing C can pull |
stage ladder + withdraw |
MVP | A→B is the ladder with two rungs; wired via RON promotion: (lib.rs:1275) |
no |
Gate trait as a SEAM + PolicyEngine |
Pro | ⚠ re-argued. PolicyGate is a concrete struct (promote/mod.rs:66) that works: it runs the modgunn scan and refuses (grpc/services.rs:1043-1051, 412 on block). A trait Gate with an engine is a REFACTOR for extensibility, not a capability the last feature lacks |
MOVED MVP→Pro. Lane 5 put it in MVP on the 2026-09-19 ruling "the whole promote→attest→sync→verify chain, gate orchestration included". That ruling is about the CHAIN, which stays MVP. The seam is science fiction in the T1 sense: nothing today has a second gate implementation to plug in. ⚠ owner may overrule; flagged, not decided |
ScannerGate (modgunn CVE/licence) |
MVP | linked, non-optional, fail-closed, with a CLI twin (scan) and a real 412 |
no — and §1.2 confirms modgunn stays |
AttestationGate + DigestGate + AttestationService.Attach |
Pro | attest.rs is 1458 lines and unwired: no RPC, and grep self.promotion finds no fetch or attest on the promote path. It also needs znippy-common 0.9.15, which crates.io does not have (0.9.13 lacks sign) — so the MVP clone would not build alone |
MOVED MVP→Pro. Lane 5 called it "the smallest gate that makes B trustworthy" — a size argument for a capability that does not run. T1 refuses it: 1458 lines of unreachable code is the definition of science fiction |
| SBOM attach/fetch | MVP | works and is literally modgunn's writer (sbom.rs:71, asserted :576,593) |
no |
properties (require_property) |
MVP | the property store works | no |
| quarantine predicate at the fetch boundary | neither | cannot be switched on: no backend addresses a coordinate, and the server refuses to boot with HOLGER_QUARANTINE_PROPERTY set (lib.rs:2040-2060); the source calls the gate "decorative" (exposed/http.rs:1880-1886) |
MOVED MVP→neither. Lane 5 scored it MVP as part of the properties row. MEASURED this lane: it is the most complete T1 failure in the server |
ClosureGate, SetSignerGate, PromoteSet |
neither | absent; a design | no (lane 5 said Pro; moved Pro→neither — Pro is a product you can sell, and none of this exists) |
| per-verdict DSSE statements + SLSA VSA | Pro | VSA is absent everywhere in the house. Lane 5 scored "MVP (VSA only)" on the size of the object | MOVED MVP→Pro (T1: absent) |
holger.repo-metadata/v1 (freshness/sequence/thresholds over repos) |
Pro | the four primitives exist in update-bundle, but nothing applies them to a repository. T1 |
MOVED MVP→Pro — lane 5's reason ("it makes 'C has B' checkable") is a good argument for building it, not for calling it MVP today |
bundle import / verify (secure_update, holger-bundle, holger-verify) |
MVP | C's intake, and the one part of the chain that demonstrably runs: the bench drives the real prepare → approve×2 → finalize → offline verify → atomic import lifecycle (bench-scenarios/Cargo.toml:85-95) |
no |
holger-remote upstream type (C pulls B) |
neither | 0 hits; ReplicationService.Replicate is same-process (replication.rs:168). Lane 5 §C.3's open tension is UNCHANGED by this ruling — it is a product decision (carried bundle vs mirror), not a size one |
no — still the owner's call |
replication (replicate_pairs, same process) |
MVP | it runs, dry-run by default | no |
| federation: nexus/artifactory/azure/aws remotes | Pro | real implementations, but every one of them reaches a third-party registry — outside T4 on both shapes | reason changed (was size/surface) |
rust-remote (crates.io pull-through) |
Pro | needs online; T4 |
reason changed |
holger-agent (offline courier, own store) |
MVP | the medium between B and C with no wire; ships in MVP and ORIGINAL | no |
holger-agent-online, emit-rsync/rclone |
Pro | egress; T4 | no |
| search (944 LOC) | Pro | ⚠ re-argued. It is a working implementation, so T1 admits it — but T4 excludes it: nothing in A→B→C searches | reason changed from "nobody searches a gateway" (an opinion about size/value) to T4 (a boundary) |
| retention / GC (1030 LOC) | Pro | works — but its write side has exactly one backend: delete_artifact is overridden only by file-rust (lib.rs:402), every other backend inherits "immutable store" (traits/src/lib.rs:372-379). A promoted store is immutable by design anyway |
reason changed; and the one-backend fact is new |
| checksum-deploy headers | MVP | binds bytes to a claimed digest on ingest into A; already there | no |
metrics /metrics |
MVP | liveness for C's operator | no |
| marketplace fulfilment (Azure, Entra sign-in, webhook) | Pro, and a #[cfg] |
pub mod marketplace is UNCONDITIONAL (lib.rs:68) but its doors need online, which no MVP row enables — so on the MVP shape it is code that cannot run. That is a T1 failure of the row as shipped, not a size complaint. Making it optional is now demanded by RULE 1 (§4.1), because nothing names it |
reason changed from "the exact opposite of slim" to: it cannot function where it ships, and no variant declares it |
front public page (holger-front) |
Pro | T2. Its reason to exist is a screen | reason changed (lane 5: "already out of MVP; correct") |
egui desktop UI (ui-mini and the three other rungs) |
neither | T2, explicitly. MEASURED: the four ui-* rungs already left this repo on 2026-09-16 for holger-egui, which declares them in its own build thing (holger-recipe/src/variants.rs, the role: client comment block) |
MOVED MVP→neither. Lane 5 scored "MVP: ui-mini only". The ruling names egui by name, and the repo already agrees |
web console (holger-server-ui) + BFF |
Pro | T2 | no |
embedded tier (in-process lib) |
Pro | dwarves/nornir consumers, not the gateway; T4 | no |
| linked handler plugins (bundle-core, rust-toolchain, skidbladnir) | MVP | skidbladnir bundles are a real intake format for C | no |
| wasm plugins | neither | OFF in every tier (server/lib/Cargo.toml:448-462); wasmtime 27, 17 advisories |
no |
UsageService.Read (open meters) |
Pro | two unmerged 2300-line branches, neither on main; T1 |
no |
EntitlementService + metered |
Pro | money is Pro by definition | no |
signed time (peer://) |
Pro | absent in holger; every expiry check trusts the RTC | no |
| mimir | neither | §1.1 — no repo, no crate, zero edges to holger. Nothing to include and nothing to remove | NEW ROW, T3 answered |
| modgunn | MVP | §1.2 — linked, scan only, load-bearing for the promote gate AND holger's SBOM writer. "Not built in" reading (c) is already true |
NEW ROW, T3 answered |
| HA/clustering, S3 backend, multi-tenancy, webhooks/event bus, cosign/Rekor hosting | neither | ❌ in feature-complete-gaps.md; none exists |
no |
| mannequin, bench-scenarios, testmatrix | neither | dev surfaces — and §3.3 makes that structural instead of intended | reason strengthened |
2.3 The headline, and what it costs
MVP is now BIGGER than lane 5 drew it and SHORTER on promises.
- In, that was out: 14 repository backends (every one that a real client can install from), the group backend. Reason: they work, and size is not the test.
- Out, that was in: the egui
ui-minirung (T2, and it already left the repo), thetrait Gateseam, theAttestationGate/AttachRPC, per-verdict DSSE + VSA,holger.repo-metadata/v1— every one of them absent or unreachable (T1). And the quarantine predicate, which the server refuses to boot with. - Neither, i.e. work that must not be called a tier: znippy-nuget, znippy-composer,
holger-remote,PromoteSet. - ⚠ MVP rows that do not exist: the audit query surface, and — the one that matters — promotion resolving
to_repoto a repository something serves.
The measured consequence of the first bullet: mvp-server is 392 crates today with all 17 backends already compiled in (they are non-optional). So admitting them costs nothing — they were always there. What lane 5 proposed (slice 8: make 15 backends #[cfg]-optional and re-measure) is now withdrawn as a slimming exercise, and survives only in the narrow form RULE 1 demands: marketplace must become optional because no variant names it, not because it is big.
3. THE CLEANUP — a benchmark crate stops deciding what ships
3.1 The defect, stated once
bench is in holger's default feature set (server/cli/Cargo.toml:126, MEASURED), and bench = ["dep:bench-scenarios"] (:166) pulls holger-bench-scenarios as an optional NORMAL dependency (:99). A normal dependency's feature choices unify into the binary's graph. So a benchmark crate's manifest has written itself into the production binary three times:
| # | what leaked | how | fixed |
| 1 | front — holger.rs's public web page — into every bench-enabled binary, including ones built --no-default-features without front |
bench-scenarios took server-lib with default features, and front was in that default set |
6f02ddf 2026-09-17 — default-features = false + the default set MINUS front |
| 2 | auth-passkey — the appliance's only login door |
after fix 1, bench-scenarios' explicit list still named auth-passkey, and cargo unified it into holger-server, whose own default did not name it. The door existed BY ACCIDENT; the commit message of the fix says so: "holger-bench-scenarios pins server-lib with auth-passkey, and cargo unifies features per GRAPH" |
542e296 2026-09-20 — named on the mvp-server / mvp-server-musl rows and added to cli default |
| 3 | native OpenSSL, plus auth-oidc + auth-oauth2 |
bench-scenarios/Cargo.toml:76 reqwest = { version = "0.12", features = [...], optional = true } — features ADDED, defaults never DISABLED, so reqwest's default-tls stays on ⇒ hyper-tls ⇒ native-tls ⇒ openssl-sys, in a binary whose own reqwest is pinned rustls-only (server/lib/Cargo.toml:121). And :62's server-lib feature list still carries auth-oidc/auth-oauth2, copied from server-lib's default set during fix 1 — no line of bench-scenarios/src uses either (served.rs:120-175 fills no oidc-cfg'd field) |
this lane, commit fa78200 |
⚠ Correction to the brief. The brief said bench-scenarios/Cargo.toml:76 "lacks default-features = false" on the server-lib edge. MEASURED: :62 (the server-lib edge) already carries it — that was fix 1, with a 16-line comment explaining exactly this hazard. The line that lacks it is :76, reqwest. The comment at :49-56 proves the author understood the mechanism and then missed the crate one screen below.
3.2 MEASURED — what each step removes
Resolved with cargo tree -p holger --edges normal --prefix none, deduplicated by crate name, in /home/rickard/git/holger-wt-mvpline.
| graph | crates | delta |
holger-server (default), as main 542e296 ships it |
431 | baseline |
+ default-features = false on bench-scenarios' reqwest |
421 | −10: openssl, openssl-sys, openssl-macros, openssl-probe, native-tls, tokio-native-tls, hyper-tls, foreign-types, foreign-types-shared, encoding_rs |
+ drop online/auth-oidc/auth-oauth2 from bench-scenarios' server-lib edge |
414 | −7: nornir-auth-oidc, nornir-auth-oauth2, oauth2, jsonwebtoken, simple_asn1, pem, serde_path_to_error |
| + bench-scenarios becomes a dev-dependency (§3.3) | 410 | −4: holger-bench-scenarios, zip, zopfli, bumpalo |
⚠ auth-passkey must STAY on the bench edge, and it took a compile on loki to learn it. The first pass dropped it with online/auth-oidc/auth-oauth2. MEASURED, cargo check -p holger-bench-scenarios --features served,… on loki:
error[E0560]: struct `GatewaySettings` has no field named `passkeys`
error: could not compile `holger-bench-scenarios` (lib) due to 1 previous error
bench-scenarios/src/served.rs:153 sets passkeys: None, and that field is #[cfg(feature = "auth-passkey")] — which the manifest comment at :57-61 already said in as many words, and which the first pass removed anyway. Restored (commit cd5c1f0); the crate count is unchanged at 414, because auth-passkey is named by cli's default (:126) since 542e296 and by the two MVP rows, so it costs the shipped graph nothing.
This is the rule of §3.3 applied to the bench itself: a benchmark asks for what it needs IN ITS OWN MANIFEST, and nothing more. auth-passkey it needs; online, auth-oidc, auth-oauth2 it never touched.
GREEN, MEASURED on loki at 9d4bc1f, all seven bench features at once — cargo check -p holger-bench-scenarios --features served,nexus,artifactory,promote-ledger,retention-planner,audit-append,secure-update → Finished dev profile in 48.10s, 0 errors. The benches still compile, and the shipped binary is 17 crates lighter. That is the whole bargain of §3.3: the fix must not kill the benches, and the benches must not decide what ships.
21 crates arrive in the shipped server through that one edge, and the whole 21 is exactly the difference between the brief's claim and the measurement. For comparison, the rows that never enable bench: mvp-server = 392 crates, mini = 385, full = 421. Every one of those greps ^(reqwest|ureq|openssl) to 0 for mvp-server and mini — the airgap tiers were never touched. Binary size, MEASURED earlier and not re-measured here (READ): holger-server default 39 251 408 B, appliance holger-mvp 32 875 720 B.
3.3 THE OWNER'S FIX — holger-bench-scenarios is a DEV DEPENDENCY
Owner, 2026-09-20: "den skall vara dev-beroende, dvs aldrig in i slutbinären!!"
This is stronger than the feature fix and it is the right one, because it closes the class instead of the instance. A normal dependency can unify into a shipped graph no matter how carefully its features are spelled; a dev-dependency cannot — cargo never resolves dev-dependencies for a lib/bin build.
Where the move applies (MEASURED, every declaration in the repo):
| declaration | kind today | shipped? | ruling |
server/cli/Cargo.toml:99 bench-scenarios, optional, features = ["served"], behind bench (in default, :126) |
normal | YES — holger-server, holger-full, server-local |
MOVE to [dev-dependencies], drop the bench feature, §3.4 |
agent/cli/Cargo.toml:70 holger-bench-scenarios, features = ["nexus","artifactory"] |
already a dev-dependency — the [dev-dependencies] section opens at :46 and [dependencies.nornir-testmatrix] at :75 closes it |
no | ⚠ correction to the brief: nothing to do here. agent/cli was already right |
xtask/Cargo.toml:102 holger-bench-scenarios, six features |
normal | no — xtask declares no variant row (holger-recipe/src/variants.rs rows() = 13 rows, none is xtask) and is never installed |
MOVE it too. MEASURED: no file under xtask/src/ mentions it; the ONLY consumer is xtask/examples/nornir-bench.rs:11,15,16,17,73, and cargo resolves dev-dependencies for [[example]] targets. So the move is free and cargo run --example nornir-bench keeps working. Not urgent (xtask ships nothing), but it makes the rule in §4.2 total instead of carved-out |
LANDED — the baseline was captured on stinky/bench a5ef6e6 (merged into this branch), and the move is commit 7a618e0. What it did:
server/cli'sbench-scenariosmoved to[dev-dependencies]; thebenchfeature is deleted from[features], fromdefaultand fromfull.- ⚠
auth-oidcandauth-oauth2had to be NAMED in the same commit. The bench crate's server-lib pin was the only declaration of either in the whole graph, so droppingbenchwould have deleted two authentication doors fromholger-serversilently, with nothing red to say so — the passkey accident again, the same edge pointing the other way. They are now passthrough features ofserver/cliand named indefault. MEASURED correction to the warning as received:auth-bearerandauth-mtlswere NOT at risk — they arrive throughmini(server/lib/Cargo.tomlmini = ["auth-bearer", "auth-mtls"]), not through the bench edge. Two doors, not three. - The
holger-dsverb moved verbatim tobench-scenarios/examples/holger_ds.rs([[example]] required-features = ["served"];clapadded as a dev-dependency so the library half grows no argument parser). server/repository/znippy-rust/tests/bench_nexus_container.shdeleted — 237 lines of bash (curlper REST call,awkon/proc,sleeppolling,trap cleanup EXIT), referenced by nothing, superseded bynexus.rs+artifactory.rs+served.rs. Pure Rust, no shelling.
MEASURED after the move, cargo tree -p holger --edges normal:
| row | before | after |
holger-server (default) |
431 | 417 |
holger-full |
421 | 417 |
mvp-server |
392 | 392 (never enabled bench) |
mini |
385 | 385 |
GREEN on loki at 7a618e0, four checks, all exit 0: cargo check -p holger (the default binary), cargo check -p holger-bench-scenarios --features served --example holger_ds (the rehomed verb), cargo check -p xtask --example nornir-bench (the bench driver, RULE 3), and cargo check -p holger --no-default-features --features mvp-server,appliance,auth-passkey (the appliance row). Zero errors.
Net −14, not the −21 of §3.2's ladder, because 7 of those crates (nornir-auth-oidc, nornir-auth-oauth2, oauth2, jsonwebtoken, simple_asn1, pem, serde_path_to_error) are now kept by declaration instead of by accident. That is the point: the binary barely changed, the declaration became true, and no benchmark crate can move it again. The benches stay runnable, and under this design they do: bench-scenarios keeps its own normal dependency on server-lib with its own explicit feature list, because bench-scenarios is not a shipped member. A bench that needs a feature the server does not ship asks for it in its own manifest, which is the whole point.
3.4 What bench was FOR, and where it goes (do not silently drop it)
bench is not a fixture switch. It gates a real, user-facing CLI verb:
server/cli/src/main.rs:124-125—#[cfg(feature = "bench")] HolgerDs(HolgerDsArgs), documented at:118-123as "Artifact-delivery benchmark (holger-ds): build a synthetic crate corpus sized from--scaleand the free disk on--data, serve it over Holger's native HTTP door, and bombard it with a parallel GET pool".- Its knobs:
--scale,--preset micro|small|large(:540-561),--data,--target <url>(bench an EXTERNAL Nexus/Artifactory over the real protocol),--eco crate|pip(:564-600). - Dispatch at
:1379, implementation:2204-2213, import:45-48.
So holger-server holger-ds … exists today and somebody may be typing it. It cannot survive as a subcommand of the shipped binary — that is exactly the thing the ruling forbids. Three places it could go:
| option | shape | verdict |
[[bench]] / [[test]] target in server/cli |
criterion-style harness | no — it takes --target <url>, --data <dir>, presets; it is an operator tool with a CLI, not a harness |
a [[bin]] name = "holger-ds" in bench-scenarios itself |
the driver moves next to the code it drives; bench-scenarios declares no variant row, so the build thing can never ship it; server/cli moves the dep to [dev-dependencies] and deletes the bench feature and the HolgerDs arm |
YES — recommended. One crate owns the benchmark: its scenarios, its reqwest, its TLS choice, its binary. Nothing it names can reach a variant |
| delete the verb | — | no. Naming it and moving it is the difference between a cleanup and a regression |
Consequences to write down, because they are visible:
full's declared feature list losesbench(holger-recipe/src/variants.rs— thefullrow names&["full", "front"];bencharrives through thefullCLI feature atserver/cli/Cargo.toml:171).holger-fullshrinks by the same 21 crates.- cli
defaultlosesbench(:126). - The invocation changes from
holger-server holger-ds --scale Ntoholger-ds --scale N. That is a release note, not a silent edit. holger-recipe/src/rules.rs's doc comment forAIRGAP_TIERS(:95-102) saysserver-localis excluded because "its default feature set carriesonline+bench". After this move that sentence is half wrong —onlinestill,benchno longer. §4.3 replaces the mechanism anyway.
4. THE RULES THAT PREVENT THE FOURTH OCCURRENCE
Three fixes so far, three commits, zero tests. The pattern that produced all three is the same: a feature reached a shipped artifact without any variant naming it. So the rule is the inverse, and it is mechanical.
4.1 RULE 1 — every feature in a shipped artifact must be NAMED by its variant
A feature is named when its enablement chain starts at something the variant declared: a feature in the row's features list, or the package default set when the row has no_default_features = false. A feature is a rider when its chain starts at a bare dependency edge — a features = [...] on some [dependencies] line that no row ever asked for.
cargo shows the distinction directly. MEASURED, mvp-server row:
$ cargo tree -p holger --no-default-features \
--features mvp-server,appliance,auth-passkey -e features -i holger-server-lib
holger-server-lib v0.6.10 (…/server/lib)
├── holger-server-lib feature "auth-bearer"
│ └── holger-server-lib feature "mini"
│ └── holger feature "mini"
│ └── holger feature "mvp-server" (command-line) ← NAMED
├── holger-server-lib feature "auth-mtls"
│ └── holger-server-lib feature "mini" (*) ← NAMED
├── holger-server-lib feature "auth-passkey"
│ └── holger feature "auth-passkey" (command-line) ← NAMED
├── holger-server-lib feature "mini" (*) ← NAMED
├── holger-server-lib feature "sign"
│ └── holger v0.6.10 (…/server/cli) ← RIDER: the parent is a CRATE, not a feature
│ ├── … (command-line)
└── holger-server-lib feature "znippy-common"
└── holger-server-lib feature "sign" (*) ← RIDER, transitively
sign is named by NO variant in any row — it rides in on server/cli/Cargo.toml:71 server-lib = { …, default-features = false, features = ["sign"] }, and the comment on :69-70 even says so out loud: "sign (CMS provenance verify) rides in on every tier." It pulls znippy-common with it. Every one of holger's four server rows ships CMS provenance verification and not one of them declares it. MEASURED, all four rows:
| row | features the row declares | server-lib features actually resolved | RIDERS |
embedded |
["embedded"], no-default |
embedded, auth-bearer, auth-mtls, sign, znippy-common |
sign, znippy-common |
mini |
["mini","appliance"], no-default |
mini, appliance, auth-bearer, auth-mtls, sign, znippy-common |
sign, znippy-common |
mvp-server / -musl |
["mvp-server","appliance","auth-passkey"], no-default |
mvp-server, appliance, mini, secure_update, auth-bearer, auth-mtls, auth-passkey, sign, znippy-common |
sign, znippy-common |
full |
["full","front"], no-default |
full, front, federation, skidbladnir, online, mini, auth-bearer, auth-mtls, auth-oidc, auth-oauth2, auth-passkey, sign, znippy-common (13) |
sign, znippy-common |
server-local |
[] — the row declares NOTHING; defaults ON |
mini, auth-bearer, auth-mtls, auth-passkey, front, online, sign, znippy-common (8, MEASURED on this branch) |
sign, znippy-common — and before fa78200, auth-oidc, auth-oauth2 as well, from the bench edge. Every other feature is named by a MANIFEST, never by the row: server-local is the row that decides nothing and ships everything |
What the test would have said about the three past leaks
| leak | the rider it would have named | the sentence |
1 — front (before 6f02ddf) |
front on full and server-local |
variant 'full' ships holger-server-lib feature "front", which no variant declares; it is enabled by the dependency edge bench-scenarios → holger-server-lib (default features). Either name it on the row or stop the edge from enabling it. |
2 — auth-passkey (before 542e296) |
auth-passkey on server-local |
variant 'server-local' ships holger-server-lib feature "auth-passkey", which no variant declares; enabled by bench-scenarios → holger-server-lib features=[…,"auth-passkey"]. — the exact defect, three days before the 404 on holger.rs was traced by hand |
| 3 — OpenSSL / oidc / oauth2 (before this branch) | auth-oidc, auth-oauth2 on server-local; plus RULE 1's crate-level twin |
variant 'server-local' ships holger-server-lib features "auth-oidc","auth-oauth2", which no variant declares. The OpenSSL half is not a feature of holger — it is a feature of reqwest, so RULE 1 catches it only in its crate-level form: the resolved closure of a shipped row contains openssl-sys, and no row names a feature that implies a native-TLS backend |
sign, today |
sign, znippy-common, all five rows |
still RED. It is the oldest instance and nobody has ever been told |
The test, sketched (DESIGNED)
Pure Rust, no shelling — the existing guard (build-space/src/lib.rs:660-666) runs Command::new(env!("CARGO")), which is the one place holger's own law is bent, and it skips silently when cargo is absent (auth_ship_small.rs:117), so a cargo-less CI passes it vacuously. This test reads the manifests with toml (a dev-dependency of holger-recipe) and computes the closure itself, so it is total, offline, and works in the wasm sandbox the build thing already targets.
// holger-recipe/tests/feature_honesty.rs
//
// RULE 1: every feature present in a shipped artifact must be NAMED by its variant.
//
// "Named" = the feature's enablement chain begins at something the ROW declared:
// * a feature in `BuildVariant::features`, or
// * the member package's `default` set, when `no_default_features == false`.
// "Rider" = the chain begins at a bare [dependencies] edge's `features = [...]`.
// A rider is a feature the BUILD DECLARATION does not know it is shipping, and
// every one of holger's three feature leaks was a rider.
use std::collections::{BTreeMap, BTreeSet};
/// A package's feature table + its dependency edges, parsed from Cargo.toml.
struct Manifest {
/// feature name -> the `["dep:x", "other/feat", "sibling-feature"]` list
features: BTreeMap<String, Vec<String>>,
/// package name -> (default_features, explicit features, kind)
deps: BTreeMap<String, DepEdge>,
}
struct DepEdge {
default_features: bool,
features: Vec<String>,
optional: bool,
kind: DepKind, // Normal | Dev | Build <- RULE 2 reads this
}
/// The whole workspace, keyed by package name. Parsed ONCE, no cargo, no process.
fn workspace() -> BTreeMap<String, Manifest> { /* toml::from_str over members */ }
/// Features of `target_pkg` reachable from `roots` of `entry_pkg`, following
/// ONLY the feature table and `dep/feature` implications — i.e. what the ROW asked for.
fn declared_closure(
ws: &BTreeMap<String, Manifest>,
entry: &str,
roots: &[String],
target: &str,
) -> BTreeSet<String> { /* … */ }
/// Everything cargo will actually enable on `target_pkg`: the declared closure
/// UNION every non-optional (or feature-activated) dependency edge's own
/// `features = [...]` and `default` set, transitively. `kind == Dev` edges are
/// SKIPPED — that is precisely why the §3.3 move is the structural fix.
fn resolved_closure(
ws: &BTreeMap<String, Manifest>,
entry: &str,
roots: &[String],
target: &str,
) -> BTreeSet<String> { /* … */ }
/// Riders the owner has seen and accepted, each with a reason. EMPTY is the goal.
/// A row here is a decision on the record, not a silence.
const ACCEPTED_RIDERS: &[(&str, &str, &str)] = &[
// (variant, feature, reason) — nothing is accepted today; `sign` is RED on purpose.
];
#[test]
fn every_shipped_feature_is_named_by_its_variant() {
let ws = workspace();
let set = holger_recipe::declared();
let mut complaints = Vec::new();
for v in set.variants.iter().filter(|v| v.ships()) {
let entry = v.member_package(); // e.g. "holger"
let mut roots = v.features.clone();
if !v.no_default_features {
roots.extend(ws[entry].features["default"].clone());
}
// Judge EVERY package in the closure, not just server-lib: `sign` lives in
// server-lib, but the OpenSSL leak lived in `reqwest`'s feature table.
for target in closure_packages(&ws, entry, &roots) {
let named = declared_closure(&ws, entry, &roots, &target);
let actual = resolved_closure(&ws, entry, &roots, &target);
for rider in actual.difference(&named) {
if ACCEPTED_RIDERS.iter().any(|(rv, rf, _)| *rv == v.name && rf == rider) {
continue;
}
complaints.push(format!(
"variant '{}' ships {target} feature \"{rider}\", which NO variant \
declares. It is enabled by a dependency edge, not by the build \
declaration. Either name it on the row, or make the edge stop \
enabling it (`default-features = false`, or a dev-dependency).",
v.name
));
}
}
}
assert!(complaints.is_empty(), "feature riders:\n {}", complaints.join("\n "));
}
This test is RED on main today (sign, znippy-common, five rows). That is the correct starting state: the first commit that makes it green is the commit that decides whether sign belongs on the rows or on nobody's. Owner decision needed — see §5.
4.2 RULE 2 — no shipped binary may normal-depend on a bench/test/fixture crate
RULE 1 catches a leak once it exists. RULE 2 forbids the edge that makes the class possible, which is the owner's ruling generalised.
/// Crates whose reason to exist is a test, a benchmark or a fixture. A SHIPPED
/// member may depend on these only as [dev-dependencies]: cargo never resolves
/// a dev-dependency for a lib/bin build, so such an edge cannot unify a single
/// feature into a shipped graph. This is the rule the owner set on 2026-09-20,
/// in as many words: "den skall vara dev-beroende, dvs aldrig in i slutbinären".
const NOT_IN_A_SHIPPED_BINARY: &[&str] = &[
"holger-bench-scenarios",
"nornir-testmatrix",
"testcontainers",
"criterion",
"holger-marketplace-mock",
"holger-mannequin",
];
#[test]
fn no_shipped_binary_normal_depends_on_a_bench_or_test_crate() {
let ws = workspace();
let set = holger_recipe::declared();
// Every package a shipped row can reach through NORMAL edges only.
let shipped: BTreeSet<String> = set.variants.iter()
.filter(|v| v.ships())
.flat_map(|v| normal_closure_packages(&ws, v.member_package()))
.collect();
let mut complaints = Vec::new();
for pkg in &shipped {
for (dep, edge) in &ws[pkg].deps {
if edge.kind == DepKind::Normal && NOT_IN_A_SHIPPED_BINARY.contains(&dep.as_str()) {
complaints.push(format!(
"'{pkg}' is reachable from a shipped variant and declares '{dep}' as a \
NORMAL dependency{}. A bench/test crate's manifest must never be able \
to unify features into a shipped graph — move it to [dev-dependencies].",
if edge.optional { " (optional — which does NOT help: a default feature turns it on)" } else { "" }
));
}
}
}
assert!(complaints.is_empty(), "{}", complaints.join("\n"));
}
RED on main today, and it finds MORE than the owner named (MEASURED):
| offender | line | note |
server/cli → holger-bench-scenarios |
Cargo.toml:99, optional, behind bench which is in default :126 |
the leak of record — §3.3 |
server/cli → nornir-testmatrix |
Cargo.toml:106, optional = true |
the same shape, one screen down. "Optional" is not protection: bench was optional too |
server/lib → nornir-testmatrix |
Cargo.toml:199, optional = true |
the library half |
agent/cli → nornir-testmatrix |
Cargo.toml:75 [dependencies.nornir-testmatrix] |
normal, optional |
xtask → holger-bench-scenarios |
Cargo.toml:102 |
not shipped, but free to move (§3.3) |
agent/cli → holger-bench-scenarios |
Cargo.toml:70 |
already a dev-dependency — correct today. Named here so nobody "fixes" it |
The nornir-testmatrix rows are the fourth occurrence waiting to happen: an optional normal dep of a shipped binary, whose feature list can unify at any time. That is the whole value of writing the rule instead of the fix.
4.3 RULE 3 — the bench drivers must BUILD, or the numbers are unreproducible
Cheap, and it would have caught a failure nobody saw for weeks. MEASURED by the baseline lane: cargo build --release -p xtask --example nornir-bench failed at HEAD with 10× E0063 — nornir-bench 0.1.8 added a field and holger builds that struct by literal at ten sites. Nobody had built it, because an --example of a publish = false xtask is built by nothing in the normal flow.
The reason nothing caught it is worth writing down on its own: holger's only CI workflow is cargo audit. MEASURED — .forgejo/workflows/security.yml is the single workflow in the repo, and its own comment says "no cargo build, no cargo test, and no cargo deny" (:12-13). .github/ contains one Markdown file. So the house has no gate that compiles anything, which is the same reason the three feature leaks of §3.1 each had to be found by hand, after the fact, on a deployed binary.
/// Every driver that produces a number the house PUBLISHES must compile. An
/// `--example` on a `publish = false` crate is built by nothing in the normal
/// flow, so its rot is silent until someone tries to reproduce a benchmark.
const BENCH_DRIVERS: &[(&str, &str, &[&str])] = &[
("xtask", "nornir-bench", &[]),
("holger-bench-scenarios", "holger_ds", &["served"]),
("holger-bench-scenarios", "secure_update_bench", &["secure-update"]),
];
One line per row in whatever gate exists:
cargo build -p xtask --example nornir-bench
cargo build -p holger-bench-scenarios --example holger_ds --features served
4.4 The egress judge — no row may be exempted SILENTLY
holger_recipe::rules::judge_artifact ends like this (rules.rs:399-413, MEASURED):
if links_no_http_client(variant) { /* airgap: refuse any forbidden client */ }
if variant == FEDERATING_TIER { /* full: refuse the ABSENCE of a client */ }
Verdict::ok() // ← every other row. No statement. No record. No reason.
AIRGAP_TIERS is ["embedded", "mini", "mvp-server"] (:102) and FEDERATING_TIER is "full" (:106). server-local — holger-server, the binary the systemd unit stages — matches neither, so it falls to the bare Verdict::ok(). That is the row OpenSSL got into. The judge did not fail to notice; it was never asked. The doc comment at :95-102 explains the exemption in prose ("server-local is deliberately NOT here … its VERBS are local while its LINK is not") — prose is not a verdict, and prose does not appear in a build log.
DESIGNED — make the posture a TOTAL function of the declared variant set.
/// What a variant is ALLOWED to reach. Every declared variant must map to exactly
/// one of these — the mapping is TOTAL and a test proves it, so a new row cannot
/// be born unjudged.
pub enum EgressPosture {
/// Provably unable to phone home: no outbound HTTP client in the closure.
NoEgress,
/// Federates by design: the ABSENCE of a client is itself a refusal.
Federates,
/// Reaches the network, on the record. `reason` is printed in every verdict,
/// and `allowed` is the exhaustive list of clients this row may link — so a
/// row that gains a NEW client (the OpenSSL path: `native-tls`) is refused
/// even though the row was already allowed to have `reqwest`.
Egress { reason: &'static str, allowed: &'static [&'static str] },
}
pub fn posture(variant: &str) -> Option<EgressPosture> { /* declared per row */ }
#[test]
fn every_declared_variant_has_a_posture() {
for v in crate::declared().variants {
assert!(posture(&v.name).is_some(),
"variant '{}' declares no egress posture. A row that has never been \
judged is not a row that passed.", v.name);
}
}
and judge_artifact loses its fallthrough:
match posture(variant) {
None => Verdict::refuse(format!(
"'{variant}' has no declared egress posture — an UNJUDGED artifact is \
not an approved artifact")),
Some(NoEgress) => { /* as today */ }
Some(Federates) => { /* as today */ }
Some(Egress { reason, allowed }) => {
let unexpected: Vec<_> = facts.linked_crates.iter()
.filter(|c| FORBIDDEN_OUTBOUND_CLIENTS.contains(&c.as_str()))
.filter(|c| !allowed.contains(&c.as_str()))
.collect();
if unexpected.is_empty() {
Verdict::ok_with(format!("'{variant}' reaches the network: {reason}"))
} else {
Verdict::refuse(format!(
"'{variant}' may reach the network ({reason}) but links \
UNDECLARED client(s): {unexpected:?}"))
}
}
}
Two more things the judge should learn, both measured by this lane:
FORBIDDEN_OUTBOUND_CLIENTS(rules.rs:90-91) does not contain a TLS backend. The OpenSSL leak broughtnative-tls,tokio-native-tls,hyper-tls,openssl-sys— and the judge would have said ok to all four, because none of them is an HTTP client. Add a second list,FORBIDDEN_TLS_BACKENDS = ["native-tls", "openssl-sys", "openssl", "hyper-tls", "tokio-native-tls", "schannel", "security-framework"], applied to every row includingfull: holger's declared TLS story is rustls everywhere (server/lib/Cargo.toml:121,agent/lib/Cargo.toml:35), and a native backend in any row is a drift, not a tier difference.judge_artifactrefuses an empty closure by name already (:371-375, "an UNMEASURED zero-egress claim is not a zero-egress claim") — which is the right instinct, applied to one field.posture()extends the same instinct to the whole row set.
5. WHAT NEEDS THE OWNER
| # | question | why it cannot be answered here | cost of each answer |
| 1 | sign — name it on the rows, or take it off the dep edge? |
It is a capability decision, not a cleanup: every tier ships CMS provenance verify today and nobody chose that | (a) name sign on all five rows: zero binary change, the declaration becomes true. (b) drop it from server/cli/Cargo.toml:71 and name it only where wanted: znippy-common leaves the tiers that do not name it, and repository-znippy-package/sign goes with it. Recommend (a) — it is verification, and the airgap argument wants it |
| 2 | Does bench die, or move to a holger-ds binary in bench-scenarios? (§3.4) |
Somebody may be typing holger-server holger-ds |
moving it is a release note; deleting it is a regression. Recommend the move |
| 3 | trait Gate seam — MVP or Pro? |
The 2026-09-19 ruling put gate orchestration in MVP. This lane reads that as the CHAIN, and files the seam as Pro on T1. That reading may be wrong | if MVP, it is a refactor of a working gate with no second implementation to justify it; if Pro, PolicyGate stays a struct and MVP ships a gate that works |
| 4 | Lane 5 §C.3 is still open and this ruling did not touch it: does C pull from B (a holger-remote backend, a mirror) or receive a carried bundle (zero egress as sold)? |
It decides what "holger on-prem with repo C" means, and the house's own documents have taken both sides | unchanged from lane 5. Under pull, the egress judge must name tonic's client half explicitly (§4.4's Egress{allowed} is the shape for it); under carry, the cloud UI's word is bundled, never sent |
| 5 | modgunn: ratify reading (c) — default-features = false, features = ["scan"], never the heavy engines, linked not serviced |
§1.2 shows (a) guts the promote gate and (b) is science fiction today | ratifying costs nothing and closes a question that has been re-asked three times |
| 6 | znippy-nuget and znippy-composer: finish them, or delete them? | They compile into every binary and cannot serve their ecosystems' normal client | finishing nuget = RegistrationsBaseUrl + SearchQueryService; finishing composer = a typed view + a p2/*.json builder. Deleting them removes a claim holger cannot back |
| 7 | Serve-time quarantine: is it a capability or a removal? | It cannot be enabled at all, and the boot check refuses it by name | making it real = coordinate_for_path on 17 backends. Removing it = deleting the boot check and the config field |
6. SEQUENCE (DESIGNED — the order that keeps every step landable)
- ⏸ Wait for the benchmark-baseline lane on loki to confirm capture. Nothing below changes
mainuntil then. (The reqwest fix isfa78200, on this branch only.) bench-scenarios→[dev-dependencies]inserver/cliandxtask; delete thebenchfeature fromserver/cli(:166,:126,fullat:171); move theholger-dsverb to a[[bin]]inbench-scenarios. Re-run the benches, compare to the baseline. MEASURED expectation:holger-server431 → 410 crates.- RULE 2 as a test (§4.2). It goes green on step 1 for
bench-scenariosand stays RED on the threenornir-testmatrixedges — land it with those three named in aPENDINGlist that has a date, not an exemption that has none. - RULE 1 as a test (§4.1). RED on
signuntil owner question 1 is answered. posture()+ the total-mapping test +FORBIDDEN_TLS_BACKENDS(§4.4). Plus RULE 3 (§4.3), two lines.judge_artifactloses its fallthrough.marketplacebecomes#[cfg(feature = "marketplace")], off on every row that does not name it — demanded by RULE 1, not by size.seal(&Repository, …): a promotion into a repository that does not exist becomesNOT_FOUND; flip the expectation ofpromote/mod.rs:812-826. ThenHolger::fetchserves the promoted store. This is the ⚠ that makes A→B real.AuditService.Query(ident?, repo?, action?, since)overArrowIpcAuditLog::read_dir, ADMIN-gated. This is the ⚠ that makes "what does C have" answerable.- Owner question 4; only then the
holger-remotework or the courier work. - Pro slice 0: merge ONE of the two metering branches (lane 5 §B.5).
7. THE FIVE WORST FINDINGS OF THIS LANE
- A benchmark crate has decided the production binary's contents three times, and the third time it was native OpenSSL in a binary whose declared TLS story is rustls everywhere. The recipe's egress judge could not have caught it twice over:
server-localfalls throughjudge_artifactto a bareVerdict::ok()(rules.rs:413), andFORBIDDEN_OUTBOUND_CLIENTS(:90-91) lists no TLS backend. MEASURED. signis shipped by every holger server row and declared by none — the oldest rider,server/cli/Cargo.toml:71, whose own comment says "rides in on every tier". The build declaration has never been true. MEASURED.- Serve-time quarantine cannot be turned on at all: zero of 17 backends implement
coordinate_for_path, the source calls the gate "decorative" (exposed/http.rs:1880-1886), and the server refuses to boot if you configure it (lib.rs:2040-2060). It was scored MVP. MEASURED. znippy-composeris a sketch with no marker: a 2-line hand-writtenpackages.json(lib.rs:81-88) and no metadata builder anywhere in the crate, compiled non-optionally into every binary. There is notodo!()underserver/repository/at all, so grep would never have found it — andznippy-nugetsays in its own comments thatdotnet restoredoes not resolve (lib.rs:119-128). MEASURED.- The line moved a long way and the previous line was drawn on the wrong axis. 14 backends were filed Pro for being surface; they are complete, tested protocol implementations that were already in the binary. Meanwhile five rows were filed MVP for being small and do not run. Both errors come from the same habit: scoring a capability by what it costs instead of by whether it works.
Written on branch stinky/mvpline. Nothing in /home/rickard/git/holger was checked out, branched or committed; all work is in the worktree /home/rickard/git/holger-wt-mvpline. No git stash, no git add -A, no repo-wide cargo fmt; explicit pathspecs only.