Holger — reasoning (the whole)
Class:
reasoning. The why behind holger's overall shape, the trade-offs, and the alternatives we rejected. The descriptive what is indesign.md, which by rule mentions none of this. Function-level rationale that is large enough to stand alone lives beside this file:registry-reasoning.md, and the design-for-review notespromotion-design.md,static-dynamic-profile-design.md,auth-rbac-design.md,artifactory-parity-design.md,end-to-end-conformance-testing.md,holger-evolution.md,holger-tiers.md.
Why an airgap-first artifact server at all
The goal was never "another Nexus". It was auditability + airgap compliance as the primary requirement, not a bolt-on. In a bank/defence/hospital zone the questions are "is this the exact artifact that passed the gate, where did it come from, can I prove it offline six months from now?" — questions a copy-based, DB-backed, online-first server answers only by trusting live server state. Holger makes the release an immutable, content-addressed, self-describing object (one signed .znippy) so those properties fall out of the substrate instead of being features to bolt on. Everything below serves that.
Why one trait, many backends (not per-ecosystem servers)
Thirteen ecosystems could each be their own service. We chose one seam, RepositoryBackendTrait, because the hot request path must never grow an ecosystem match: dispatch stays Arc<dyn RepositoryBackendTrait> through FastRoutes, and adding Rust/Maven/npm/… is a new crate plus one registry arm, with zero change to the engine. The deeper argument, and the rejected "blanket generic" and "enum of backends" shapes, are in registry-reasoning.md.
Why config is a graph in RON (not a flat list / YAML)
A repo isn't a flat entry — it references a storage location, an exposed endpoint, and (for proxy) upstream repos. RON expresses typed enums + named node refs naturally, so ExposedEndpoint ← Repository → StorageEndpoint is a directed graph wire_holger() resolves in passes. YAML/JSON would force stringly-typed refs and lose the enum typing that makes a bad config a parse error. Putting auth policy in the same RON (not a mutable KV store) is deliberate: policy is then git-tracked, code-reviewed, diffable, and — in the sealed static flavor — immutable, exactly the bytes you reviewed. A mutable policy store would defeat audit and the airgap immutability model (redb is reserved for runtime state — audit, promotion lineage — not policy). Detail: auth-rbac-design.md.
Why the three-variant split is a crate boundary, not #[cfg]
The static/drift server must be provably unable to phone home. A runtime flag or a #[cfg] inside a shared crate cannot guarantee that: Cargo features are additive and unify, so dynamic/egress code hidden behind a feature in a shared crate could unify on in a workspace build and leak into the "static" binary. Only a crate boundary makes "no reqwest, no remote upstream, no writable backend is linked" a property you audit from cargo tree. So the split is: traits (shared) → a lean core-serve (read + serve, no concrete writable backends) → a dynamic lib (writable/proxy/import/online-OIDC) → two binary crates. Features tune within a side; they never draw the static/dynamic line. The mini/static-holger binary and the core-serve extraction are not built yet — the full server is currently one un-split crate that is capability-complete but not stripped. The full design-for-review is static-dynamic-profile-design.md; the tier reality-check is holger-tiers.md.
Why "airgap ≠ offline". Airgap pulls apart into two independent axes: egress (does the binary make outbound calls?) and serving (does it accept inbound clients?). An airgapped red-zone box still serves developers inside the zone — so airgap = serves inbound, zero outbound, which is exactly the cargo tree property, not disconnection. This is why "auth split by egress" (offline mTLS/JWKS in static; online /userinfo in dynamic) is the right cut, rather than "mTLS vs OIDC".
Why separate agent and server (not one writable server)
The airgap requires the producer to be offline-from-the-secure-side. Splitting the internet-side agent (builds archives, has egress) from the secure-side server (serves, no egress needed) means the secure zone never needs connectivity and the write happens outside the wall. The honest consequence: the OT-side server should stay read-only. The building blocks for an internal write path exist (wired mTLS/OIDC, fail-closed RBAC on both write paths, audit-on-by-default) but every gate is opt-in and off by default, so we do not recommend opening one yet — push immutable .znippy in instead. Making the gates mandatory (default-deny) plus a production air-gapped PKI are the prerequisites. Detail: design.md §9b history and auth-rbac-design.md.
Why gRPC for holger-native and native-HTTP for ecosystems
Two doors, deliberately. The holger-native surface (agent, UI, SDK) wants strong typing + streaming → gRPC only, no REST. But stock cargo/pip/docker must work unmodified → each backend also speaks its ecosystem's real HTTP registry protocol. One backend serves both doors, so there is no second code path to keep in sync.
Why proto codegen shells out to nothing (pure-Rust protox)
build.rs used to call tonic_build::compile_protos, which shells out to protoc at every build. On checkouts without protoc on PATH (the nornir server user) the build script failed and took down everything path-dep'd on holger-server-lib. The fix follows the nordisk "no shelling — pure Rust" law: a cargo xtask gen-proto step compiles the proto with pure-Rust protox into a committed holger.v1.rs, and normal builds just include! it — no subprocess, no protoc. Detail folded into testing-guide.md; the shipped record is proto-codegen-xtask.md.
The road here (briefly)
Read handlers were brought to real-protocol depth via znippy's typed-view plugin contract (npm/gem/conda parse real metadata for authoritative coords). The write path got W1 (pooled OIDC client + TTL token cache) and W2 v1 (RBAC global gate, fail-closed, off-until-configured). A security-hardening pass closed JSON-injection, path-traversal, SSRF and write-race holes and made audit resilient to a torn segment. The remote cache became pluggable (RemoteUpstream: Nexus/Artifactory/Rust). Still ahead and tracked in PLAN.md: the static/dynamic crate split, offline-OIDC, promotion (promotion-design.md), and container conformance (end-to-end-conformance-testing.md).