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

nordisk/holger

.nornir/stinky/00-index.md

holger — the stinky list, 2026-09-20

Five lanes read holger against what the owner said it must be: "en mycket performant, säker produkt, zero copy, apache arrow och produkt som överlever, har job köer", an appliance with no OS, a twin beside it, and a last MVP feature that is holger in the cloud with repo A→B, then holger on-prem with repo C. Everything that does not fit becomes holger Pro.

Each section is measured or labelled. Read the section for the argument; this page is the map and the four things that are true across all five.

§ section branch
01 the data plane — zero copy, Arrow, gatling, job queues, survival stinky/dataplane
02 the twin, growth, and the A→B→C question stinky/twin
03 security, attestation, and the auth bloat question stinky/security
04 code structure — traits, seams, the wasm plugin ABI stinky/structure
05 the suite, monetize, and the MVP line stinky/suite
13 the owner's requirements checked against the code, 2026-09-22 (twin, one ISO, election, WireGuard + mTLS, mock) agent/v1-requirements-2026-09-22

1. THE FOUR THINGS THAT ARE TRUE ACROSS ALL FIVE

1.1 One dependency edge has been deciding what ships, three times

holger-bench-scenarios pins holger-server-lib with default features, and cargo unifies features per GRAPH. So a benchmark crate has silently decided the contents of the production binary three separate times:

what leaked in found how
front 6f02ddf, 2026-09-17 the public web page compiled into binaries built with bench
auth-passkey 2026-09-20 the appliance passkey door existed BY ACCIDENT; default never named it
openssl-sys + auth-oidc + auth-oauth2 2026-09-20 native OpenSSL reaches the systemd binary through reqwest 0.12 → hyper-tls → native-tls

The third is the serious one: holger-server links native OpenSSL through a benchmark crate, and default-tls lands on the production OIDC/JWKS fetch path. The recipe exempts that row from the egress judge, so nothing caught it. holger-server and holger-full link the IDENTICAL 431-crate set — comm -3 is empty — and 21 of those crates arrive through this one edge.

The pattern, not the three bugs: a feature is only honest when a VARIANT names it. sign is named by no variant at all and rides a dep edge into every tier. The fix is a rule, not three patches: every feature in a shipped artifact must be named by the row, and a dev/bench dependency must carry default-features = false.

1.2 A great deal is built and nothing drives it

built driver
attest.rs, 1 458 lines, DSSE + in-toto, digest-keyed no door, no constructor, no consumer
ReplicationService.Replicate, idempotent by content id nothing on a schedule; no twin role — ✅ 2026-10-22 (SA, dc801dff): an executing Replicate on a server with a job queue runs as a resumable Job::Replicate; a dry run stays the inline preview
PromotionEngine::fetch no non-test caller — ✅ 2026-10-22 (SA, c9031dac): it IS the download gate's read (IntakeChain::serve → sealed_bytes, HTTP door exposed/http.rs), proven by the_download_gate_reads_through_the_engine_and_refuses_a_tampered_seal
Pool::delta_since / PoolDelta in Skidbladnir callers are #[cfg(test)] only — ✅ DELETED 2026-10-22 (SA, Skidbladnir 67a3da2): holger never used the pool; its twin follows by manifest + watermark
mirror_into zero callers — ✅ DELETED 2026-10-22 (SA, 6b7a3c12): the upstream mirror is the mirror cycle (Job::MirrorAsset)
wasm plugin host, bounded and CMS-verified wasm-plugins OFF in every tier
metering doors, ~2 300 lines, twice two unmerged branches, one hour apart

This is the house's real debt: not missing code, but code with no seam to the outside. promote/mod.rs:123 still calls modgunn with blob_sha256: "" while the digest-carrying seam sits one layer below with no callers.

1.3 The product does not yet behave like a warehouse under load

Everything moves as a whole Vec<u8>, at every hop: fetch, put, the HTTP body collected to 1 GiB, sha256 on the request thread at 0.61 GB/s, Full responses, and a gRPC "stream" that loads the whole blob first. There is no job queue — promote, replicate, upload and gate all run inline on the RPC, nothing resumes after a restart, and running rows are permanent once the process dies.

And the engine the law names is linked but unused: rotaryengine (gatling), znippy-zoomies, lgz and ldeflate are each linked twice (crates.io and path) (✅ rotaryengine: ONE copy since 2026-10-22, SA fc62ab24 + draupnir f6eaded), while holger-server calls the engine zero times. rayon is absent in all four tiers — the one clean row.

1.4 The MVP is not slim, and slimness is its whole point — DONE 2026-09-21

Struck 2026-09-21 (agent/slim-mvp): marketplace is a feature named by full + server-local; every backend is a fmt-* feature, the MVP rows name fmt-raw + fmt-rust, a RON naming any other format refuses boot as holger-mvp-1. holger-mvp 463 → 442 crates (liblzma-sys gone). Table and reasoning: .nornir/HOLGER-PLAN.md §4 "stinky 1.4 — MVP slimmed 2026-09-21". The holger-remote half of this row was answered separately (?1, C pulls). What stood here:

pub mod marketplace has no feature gate at all: ~2 400 lines of Microsoft fulfilment and tenant provisioning are compiled into holger-mvp, the airgap appliance. All 17 repository backends are unconditional. Meanwhile the thing the owner actually called the last MVP feature — a downstream holger that can reach an upstream one — does not exist: holger-remote returns 0 grep hits.


2. THE DECISION THAT BLOCKS THE LAST MVP FEATURE

Both halves of holger's own design document are written, and they contradict each other. This is the owner's to settle, and nothing downstream of it should be built first.

carried bundle holger-remote pull
zero-egress guarantee intact and true the guard measures the wrong thing — the tonic client is exempt and is already in every tier
does upstream know what C has? no yes — AuditEvent{action: Download} grouped by ident
what the UI may say bundled sent — never accepted
prerequisite audited export under C's principal C needs an identity and a route upstream

⚠ Either way, the audit log has no query surface (AuditLog = record + flush, no RPC, no route). The ruling "the upstream already knows, because it served the fetch" cannot be honoured until that exists.

3. THE QUESTIONS THE OWNER ASKED, ANSWERED

3.1 "Har holger blivit bloated av passkey?"

No. Measured in bytes, not argued: holger-mvp 32 475 776 B without auth-passkey, 32 875 720 B with — +399 944 B, +1.2 %, four crates, all pure Rust (nornir-auth-webauthn, ciborium, ciborium-io, ciborium-ll). reqwest, ureq, openssl-sys, webauthn-rs and wasmtime are all absent from the appliance. The door 404s by name when unconfigured and a bad rp_id refuses BOOT. Two planes are respected: the appliance's direct airgap login and the console's BFF.

The bloat is elsewhere — see §1.1. holger-server default is 39 251 408 B, 6.8 MiB above the appliance.

3.2 "Kan gunnar och holger samarbeta, ax till limpa?"

Yes, and the premise needs one correction: gunnar does not use DSSE at all (SLSA fields in an sshsig envelope). So the shared answer is not gunnar's envelope — it is znippy's, which both should adopt. Home: a new edda crate depending on znippy-common/sign and nornir-build-thing, with gunnar and holger both depending on it and neither on the other.

The house's embarrassment, measured: four WebAuthn verifiers (edda's pure one, plus garmr → gunnar-ui-server → holger-ui-server hand-ported) and three key identity vocabularies, so a customer needs three trust stores today — cert fingerprint, SSH fingerprint, raw ed25519 key.

3.3 "Twin som gunnars — och kan mönstret bära A→B→C?"

One engine, two drivers. replicate_pairs is a pure function over two backends and should serve both. Above it they are different types: the twin copies the whole data root — secrets and KEK included — ungated, measured by a watermark; C copies one promoted repo through AttestationGate=Block and can only ever be sent. A single Peer{direction} would force a gate onto the twin (no longer a restore source) or the KEK onto C (a boundary breach).

Both PULL. gunnar proved pull, the primary dials nobody, and the C-knows ruling only holds if C's fetches land as Download rows under C's own ident — a push produces Upload rows written by the driver. What private-holger-ops/estate.toml:364-365 currently owes is a PUSH from the primary: wrong direction, and it makes the ruling false.

⚠ And today replicate_pairs cannot make a twin: it moves artifacts by coordinate only — no evidence store, no audit segments, no properties, no config, no bundle store. Promoted, such a twin serves artifacts with no provenance. That is gunnar's un-metered-twin scar in a new coat.

3.4 "En holger-plugin för monetize?"

Not a plugin — a native seam. The wasm ABI is a package-handler ABI: no principal on the wire, no digest in WireArtifactId, no audit or clock access, one implicit ungrantable capability, and an exact-match u32 version whose manifest decoder rejects unknown fields, so it cannot even negotiate. Of the three plausible plugins, today's ABI carries only "another format handler".

What monetize actually needs is the gRPC door that is already written twice and merged never. And a cap over a quota is a gate, which needs the trait holger does not have.

3.5 "Är koden trait-baserad?"

More than expected — RepositoryBackendTrait has 12 verbs and 21 implementations — except where it decides the product. There is no Gate trait: policy is a struct configured by ENVIRONMENT VARIABLES on a box with no shell, Ok(None) means pass-through, and judgement is per artifact by name@version with no set-scope. gunnar has the seam (gunnar-policy/src/lib.rs:173) and the law that goes with it — GUN-007: an unreadable gate config refuses everything, because an unreadable policy defaulting quietly once turned branch protection off.

✅ 2026-10-22 (SA, 95043a69): decided by the code — the stub is DELETED, not grown into a trait. The one factory that opens bytes is Repository::backend_from_config_with, and Repository::storage_places answers where they are; a storage endpoint is a name the wiring checks. What stood: Storage is worse: backend_from_config returns Ok(()). There is no storage seam at all; 17 backends each open their own bytes.

4. WHAT TO DO FIRST

  1. Settle §2. Building before it builds the wrong product with a green guard.
  2. Close the bench edge (§1.1) and add the rule that a shipped feature must be named by its variant.
  3. A Gate trait over a PromotionContext carrying the SET with digests required — it deletes the env parsing, so it is net-negative surface and fits MVP.
  4. Fix the promotion that lies: a promotion to a non-existent repo SUCCEEDS, and a test asserts it (promote/mod.rs:812-826).
  5. An audit query surface, without which §2 cannot be honoured either way.
  6. Feature-gate marketplace — one cfg shaped like front, −2 400 lines from the airgap appliance.
  7. Then performance: HotArchive for the writable znippy repo, a streaming contract instead of Vec<u8>, and gatling where the server does the work.