holger — AI context (full API & source reference)
Read ai-context.md first. This is not normal prompt context — it is a lookup reference for exact public-API signatures, selected private APIs, complete rustdoc and
file:linesources. Do not load it for every task.
Snapshot
| Field | Value |
| Repository | holger |
| Source revision | a843b3260e016fc17d113ab42dba20344c5e832e |
| Working tree | Dirty |
| Reproducible snapshot | Non-reproducible |
| Generation mode | development |
| Generator version | kvasir 0.2.1 |
| Maintained metadata | .nornir/ai-context.toml |
| Generated at | 2026-08-13T21:41:29+02:00 (source: commit) |
⚠ Dirty working tree — this snapshot is NOT reproducible. It reflects uncommitted local changes, not commit
a843b3260e01.
Navigation
Architecture, runtime wiring, security invariants, failure semantics, the trust lifecycle, the state inventory and the task map all live in ai-context.md:
- Runtime wiring map
- Non-negotiable invariants
- Failure semantics
- Task-to-file map
- Trust and lifecycle
- State and persistence
Crate and module map
| Category | Count | Included items |
| Cargo workspace packages | 42 | see the workspace dependency graph |
| Detached Rust packages | 0 | — |
| Packages with indexed API surface | 42 | 42 workspace + 0 detached |
| Binary targets | 19 | [[bin]] + src/main.rs + src/bin/* |
| Library targets | 39 | packages with src/lib.rs |
The indexed count (42) is the 42 workspace members that expose a src/ tree plus 0 detached package(s) (own [workspace], indexed for their API but outside the dependency graph). Detached packages are in the API index; they are not in the workspace dependency graph.
| crate | description | target | modules | items |
holger-agent-cli |
Holger agent CLI: airgap and transfer artifacts between registries | bin | 1 | 4 |
holger-agent-lib |
[agent-transfer] operations.rs: transfer/push/pull with bounded concurrency and partial-success tallies. | lib | 3 | 27 |
holger-airgap |
[airgap] Pure-Rust plan/argv; one shell-out to distribution/airgap/populate. | lib | 6 | 20 |
holger-artifactory-connector |
Holger connector for JFrog Artifactory (REST + AQL, basic/bearer/mTLS auth) | lib | 2 | 5 |
holger-bench-scenarios |
[benchmarks] No [[bench]] targets exist; real path is nornir bench run holger / xtask examples. |
lib | 10 | 63 |
holger-build |
Holger's MVP/dev/micro build-thing: the 3 tiers × 3 roles (agent · UI · server) matrix as a VIEW over holger's declared build-space, a comparison matrix a UI can render, and the invertible feature↔crate dependency view (the Galois connection: features_pulling / drop_crate). A build-thing CONSUMER, like knut-build — it path-deps nornir-build-thing for the reusable core and reuses holger-build-space's declared BuildVariantSet rather than re-declaring it. |
lib | 13 | 56 |
holger-build-space |
[dev-tooling] build-variants.toml + tier link-audits (auth_ship_small: webauthn/openssl must NOT link). | lib | 1 | 7 |
holger-embed |
The <guest>-embed adapter for holger: hosts holger's registry-browser panel (facett-holger) inside ANOTHER app as a native Box<dyn Facet> (backend (2) — native, no wasm), fed either in-process through holger's functional read API or from a caller-supplied model (the thin-gRPC insertion point). The reusable generalization of nornir's bespoke in-process holger tab. |
lib | 1 | 3 |
holger-errcode |
holger's FROZEN, append-only holger-<subsystem>-<n> application error codes |
lib | 3 | 12 |
holger-file-oci-repository |
OCI registry backend for holger, disk-backed and WRITABLE — full Distribution Spec v2 push and pull (content-addressed sha256), and holger's target for the modern helm push oci://… flow that replaces the bespoke ChartMuseum API. |
lib | 2 | 2 |
holger-handler-bundle-core |
The sealed-.znippy bundle repository machinery shared by holger's airgap package handlers | lib | 4 | 11 |
holger-handler-rust-toolchain |
holger package handler for airgapped Rust-toolchain bundles — the whole logic, shared by the native backend and the wasm module | lib | 1 | 6 |
holger-handler-rust-toolchain-wasm |
The Rust-toolchain bundle handler compiled to wasm32 — ABI shim only, all logic in holger-handler-rust-toolchain | lib | 0 | 0 |
holger-handler-skidbladnir |
holger package handler for Skidbladnir airgap bundles — the whole logic, shared by the native backend and the wasm module | lib | 1 | 5 |
holger-handler-skidbladnir-wasm |
The Skidbladnir airgap-bundle handler compiled to wasm32 — ABI shim only, all logic in holger-handler-skidbladnir | lib | 0 | 0 |
holger-mannequin |
[test-infra] Mock PKI+OIDC+Nexus in one process. Test/dev only — never production. | lib | 26 | 109 |
holger-nexus-connector |
Sonatype Nexus connector for the holger agent — a ConnectorTrait Source/Target that lists repositories and assets and moves artifact bytes over Nexus's REST API (basic / bearer / mTLS), so an existing Nexus can be mirrored into holger or fed from it. | lib | 2 | 6 |
holger-plugin-abi |
holger package-handler plugin ABI: the wire contract a handler speaks whether it is linked in or loaded from a .wasm | lib | 18 | 100 |
holger-plugin-host |
Loads holger package handlers — linked or from a directory of .wasm — and wraps each in one RepositoryBackendTrait | lib | 17 | 67 |
holger-rust-file-repository |
Rust / Cargo repository backend for holger, disk-backed and WRITABLE — speaks the same Cargo sparse protocol as holger-rust-znippy-repository but over a live loose-file tree, so it can accept cargo publish. |
lib | 2 | 6 |
holger-rust-znippy-repository |
[storage-backends] Only backend that consumes the FormatGate reader pin and overrides has_archive; only one with a writable dev-mode archive. | lib | 2 | 10 |
holger |
[server-core] The holger-server binary; tier compile_error! guard lives here. default-features=false on server-lib (the airgap keystone). | bin | 2 | 25 |
holger-server-lib |
[server-core] Wiring engine, 8 gRPC services, HTTP door, auth, audit, promote, retention, replication, raw. Central high-coupling crate. | lib | 116 | 522 |
holger-server-ui |
[ui] facett/egui; thin RemoteHolger or in-process LocalHolger; gui/mini/mikro tiers guarded by tier_ladder.rs. | lib | 44 | 215 |
holger-traits |
[contracts] RepositoryBackendTrait, ConnectorTrait, HolgerObject, ArtifactId/Format, znippy format-version pin. A znippy API change breaks the whole workspace via [patch.crates-io]. | lib | 10 | 71 |
holger-update-bundle |
Holger Secure Update Gateway — network-free signed update-bundle format (holger.update-bundle/v1), two-person approval + release-signature trust model, and offline verifier. No outbound networking by construction. | lib | 37 | 124 |
holger-world-connector |
[connectors] External registries (crates.io/PyPI); SSRF host allowlist; list_assets bails (lockfile-driven). | lib | 7 | 21 |
holger-znippy-build-thing-repository |
The znippy-build-thing repository backend for holger — serves a forge's BUILD HISTORY out of a .znippy archive: one build-provenance attestation per built artifact, addressed by the artifact's own sha256, plus the derived recipe→artifacts lookup a CVE sweep reads. Read-only, pure path mapping, no shared types with the writer. | lib | 2 | 13 |
holger-znippy-composer-repository |
Composer / PHP (Packagist) repository backend for holger — passthrough of a Composer metadata repository held in a read-only znippy archive; unlike the deb/rpm siblings it synthesizes no index, so the archive must already carry one. | lib | 2 | 6 |
holger-znippy-conda-repository |
Conda / Anaconda repository backend for holger — serves a real per-subdir repodata.json plus the packages, with name/version/build/subdir read from each package's parsed info/index.json rather than guessed from the filename. | lib | 2 | 6 |
holger-znippy-deb-repository |
Debian / APT repository backend for holger — synthesizes the native dists/{dist}/Release and Packages index (real SHA-256, control fields parsed out of the .deb via ar + control.tar) from packages inside a read-only znippy archive. | lib | 4 | 10 |
holger-znippy-docker-repository |
Docker / OCI registry backend for holger, READ-ONLY — serves the Registry HTTP API V2 straight out of a znippy archive whose entries already sit in on-wire layout. Pure passthrough; see holger-file-oci-repository for the writable push target. | lib | 2 | 6 |
holger-znippy-gem-repository |
RubyGems repository backend for holger — serves .gem bytes plus a REAL compact-index (versions, info/{name}) synthesized from a read-only znippy archive, which is what bundler resolves against. |
lib | 2 | 6 |
holger-znippy-go-repository |
Go module repository backend for holger — serves the GOPROXY protocol (@v/list, .info, .mod, .zip) out of a read-only znippy archive laid out like the on-disk module-proxy cache. | lib | 2 | 6 |
holger-znippy-helm-repository |
Helm chart repository backend for holger — serves a Helm repo's native index.yaml and chart .tgz files from a read-only znippy archive, synthesizing index.yaml from the charts present when the archive ships none. | lib | 3 | 13 |
holger-znippy-maven-repository |
Maven / Java (maven3) repository backend for holger — serves JARs and POMs out of a read-only znippy archive and synthesizes the native maven-metadata.xml version listing, so a bare partial mirror still resolves. | lib | 2 | 6 |
holger-znippy-npm-repository |
[storage-backends] packument is view-only — generic-reader ingestion 404s the packument (breaks npm install). |
lib | 2 | 8 |
holger-znippy-nuget-repository |
NuGet / .NET repository backend for holger — serves the NuGet V3 flat-container protocol (service index, versions, .nupkg) synthesized from packages inside a read-only znippy archive. | lib | 2 | 7 |
holger-znippy-package-repository |
[storage-backends] Holger-native znippy surface; opt-in CMS provenance verify behind sign. |
lib | 3 | 9 |
holger-znippy-python-repository |
PyPI / pip repository backend for holger — serves a real PEP 503 simple index (plus PEP 691 JSON and #sha256= digests pip/uv verify) synthesized from wheels and sdists inside a read-only znippy archive. This is holger's PyPI crate: config calls it pip, code calls it ArtifactFormat::Pip, the directory is znippy-python. |
lib | 2 | 6 |
holger-znippy-rpm-repository |
RPM / yum / dnf repository backend for holger — synthesizes the native rpm-md index (repomd.xml + primary.xml, epoch and read from the RPM binary header) from .rpm files inside a read-only znippy archive. | lib | 4 | 9 |
xtask |
[dev-tooling] gen-proto (protox+tonic-build, committed output), demo-wasm/deploy-pages. | bin | 4 | 8 |
Workspace dependency graph
Garmr workspace crates only (external deps excluded); edges deduplicated, kind + feature-gate marked.
| Crate | Depends on (workspace) | Kind | Feature-gated |
holger-agent-cli |
holger-agent-lib |
runtime | no |
holger-agent-cli |
holger-bench-scenarios |
dev-only | no |
holger-agent-cli |
holger-mannequin |
dev-only | no |
holger-agent-cli |
holger-nexus-connector |
dev-only | no |
holger-agent-cli |
holger-rust-znippy-repository |
dev-only | no |
holger-agent-cli |
holger-traits |
runtime | no |
holger-agent-cli |
holger-znippy-maven-repository |
dev-only | no |
holger-agent-cli |
holger-znippy-python-repository |
dev-only | no |
holger-agent-lib |
holger-artifactory-connector |
optional | yes |
holger-agent-lib |
holger-nexus-connector |
optional | yes |
holger-agent-lib |
holger-traits |
runtime | no |
holger-agent-lib |
holger-world-connector |
optional | yes |
holger-artifactory-connector |
holger-traits |
runtime | no |
holger-bench-scenarios |
holger-agent-lib |
optional | yes |
holger-bench-scenarios |
holger-rust-znippy-repository |
runtime | no |
holger-bench-scenarios |
holger-server-lib |
optional | yes |
holger-bench-scenarios |
holger-traits |
runtime | no |
holger-bench-scenarios |
holger-update-bundle |
optional | yes |
holger-bench-scenarios |
holger-znippy-maven-repository |
runtime | no |
holger-bench-scenarios |
holger-znippy-python-repository |
runtime | no |
holger-build |
holger-build-space |
runtime | no |
holger-embed |
holger-server-lib |
runtime | no |
holger-file-oci-repository |
holger-traits |
runtime | no |
holger-handler-bundle-core |
holger-plugin-abi |
runtime | no |
holger-handler-rust-toolchain |
holger-handler-bundle-core |
runtime | no |
holger-handler-rust-toolchain |
holger-plugin-abi |
runtime | no |
holger-handler-rust-toolchain-wasm |
holger-handler-rust-toolchain |
runtime | no |
holger-handler-rust-toolchain-wasm |
holger-plugin-abi |
runtime | no |
holger-handler-rust-toolchain-wasm |
holger-plugin-host |
dev-only | no |
holger-handler-rust-toolchain-wasm |
holger-traits |
dev-only | no |
holger-handler-skidbladnir |
holger-handler-bundle-core |
runtime | no |
holger-handler-skidbladnir |
holger-plugin-abi |
runtime | no |
holger-handler-skidbladnir-wasm |
holger-handler-skidbladnir |
runtime | no |
holger-handler-skidbladnir-wasm |
holger-plugin-abi |
runtime | no |
holger-handler-skidbladnir-wasm |
holger-plugin-host |
dev-only | no |
holger-handler-skidbladnir-wasm |
holger-traits |
dev-only | no |
holger-mannequin |
holger-errcode |
runtime | no |
holger-nexus-connector |
holger-traits |
runtime | no |
holger-plugin-host |
holger-handler-rust-toolchain |
dev-only | no |
holger-plugin-host |
holger-handler-skidbladnir |
dev-only | no |
holger-plugin-host |
holger-plugin-abi |
runtime | no |
holger-plugin-host |
holger-server-lib |
dev-only | no |
holger-plugin-host |
holger-traits |
runtime | no |
holger-rust-file-repository |
holger-traits |
runtime | no |
holger-rust-znippy-repository |
holger-traits |
runtime | no |
holger-rust-znippy-repository |
holger-znippy-maven-repository |
dev-only | no |
holger-rust-znippy-repository |
holger-znippy-python-repository |
dev-only | no |
holger |
holger-airgap |
runtime | no |
holger |
holger-bench-scenarios |
optional | yes |
holger |
holger-server-lib |
runtime | no |
holger |
holger-update-bundle |
optional | yes |
holger-server-lib |
holger-errcode |
runtime | no |
holger-server-lib |
holger-file-oci-repository |
runtime | no |
holger-server-lib |
holger-handler-rust-toolchain |
runtime | no |
holger-server-lib |
holger-handler-skidbladnir |
runtime | no |
holger-server-lib |
holger-plugin-host |
runtime | no |
holger-server-lib |
holger-rust-file-repository |
runtime | no |
holger-server-lib |
holger-rust-znippy-repository |
runtime | no |
holger-server-lib |
holger-traits |
runtime | no |
holger-server-lib |
holger-znippy-build-thing-repository |
runtime | no |
holger-server-lib |
holger-znippy-composer-repository |
runtime | no |
holger-server-lib |
holger-znippy-conda-repository |
runtime | no |
holger-server-lib |
holger-znippy-deb-repository |
runtime | no |
holger-server-lib |
holger-znippy-docker-repository |
runtime | no |
holger-server-lib |
holger-znippy-gem-repository |
runtime | no |
holger-server-lib |
holger-znippy-go-repository |
runtime | no |
holger-server-lib |
holger-znippy-helm-repository |
runtime | no |
holger-server-lib |
holger-znippy-maven-repository |
runtime | no |
holger-server-lib |
holger-znippy-npm-repository |
runtime | no |
holger-server-lib |
holger-znippy-nuget-repository |
runtime | no |
holger-server-lib |
holger-znippy-package-repository |
runtime | no |
holger-server-lib |
holger-znippy-python-repository |
runtime | no |
holger-server-lib |
holger-znippy-rpm-repository |
runtime | no |
holger-server-ui |
holger-agent-lib |
optional | yes |
holger-server-ui |
holger-airgap |
runtime | no |
holger-server-ui |
holger-bench-scenarios |
optional | yes |
holger-server-ui |
holger-rust-znippy-repository |
optional | yes |
holger-server-ui |
holger-server-lib |
optional | yes |
holger-server-ui |
holger-traits |
runtime | no |
holger-server-ui |
holger-world-connector |
optional | yes |
holger-update-bundle |
holger-errcode |
runtime | no |
holger-world-connector |
holger-traits |
runtime | no |
holger-znippy-build-thing-repository |
holger-traits |
runtime | no |
holger-znippy-composer-repository |
holger-traits |
runtime | no |
holger-znippy-conda-repository |
holger-traits |
runtime | no |
holger-znippy-deb-repository |
holger-traits |
runtime | no |
holger-znippy-docker-repository |
holger-traits |
runtime | no |
holger-znippy-gem-repository |
holger-traits |
runtime | no |
holger-znippy-go-repository |
holger-traits |
runtime | no |
holger-znippy-helm-repository |
holger-traits |
runtime | no |
holger-znippy-maven-repository |
holger-traits |
runtime | no |
holger-znippy-npm-repository |
holger-traits |
runtime | no |
holger-znippy-nuget-repository |
holger-traits |
runtime | no |
holger-znippy-package-repository |
holger-traits |
runtime | no |
holger-znippy-python-repository |
holger-traits |
runtime | no |
holger-znippy-rpm-repository |
holger-traits |
runtime | no |
xtask |
holger-bench-scenarios |
dev-only | no |
xtask |
holger-build |
runtime | no |
xtask |
holger-build-space |
runtime | no |
Central external dependency hotspots
Only architecturally meaningful dependencies. Used by is derived from Cargo metadata.
| Dependency | Used by | Architectural effects | Security/reliability implications | Feature/runtime impact |
tokio |
holger-agent-cli, holger-agent-lib, holger-artifactory-connector, holger-bench-scenarios, holger-mannequin, holger-nexus-connector, holger, holger-server-lib, holger-server-ui, holger-world-connector, xtask |
multi-thread async runtime for gRPC + HTTP; UI uses private current-thread runtimes + block_on | — | always on server-side; backend fetch/put run blocking I/O directly on async workers (no spawn_blocking) |
tonic |
holger-server-lib, holger-server-ui |
gRPC transport (HTTP/2); the only network API surface plus the HTTP door | — | all tiers; message size caps = ron_max_body_bytes |
rustls |
holger-server-lib |
TLS + mTLS (client_ca ⇒ handshake-level mutual auth); no OpenSSL | — | auth_ship_small link-audit asserts openssl NEVER links into any tier |
skidbladnir |
holger, holger-server-lib |
federation harbour clients; also the identity/verify seam the CLI links | — | only via full/federation; *-remote repo types fail closed on mini/embedded |
ron |
holger-mannequin, holger-server-lib, holger-update-bundle |
the config format — a typed graph of repos/endpoints/storage/auth/audit | — | always; parse failure aborts startup |
axum |
holger-agent-cli, holger-mannequin |
HTTP server for both the ingest endpoints and the read/admin API. | The request-auth and body-limit surface lives on these routers. | Runtime; request routing. |
reqwest |
holger-agent-cli, holger-agent-lib, holger-artifactory-connector, holger-bench-scenarios, holger-mannequin, holger-nexus-connector, holger-rust-znippy-repository, holger-server-lib, holger-world-connector |
Async outbound HTTP: LLM provider calls, threat-intel feed refresh, and webhook notification delivery. | Every call routes through the egress chokepoint; redirects are refused so a 3xx can't re-POST a prompt. No client-side LLM timeout is set. | Runtime; TLS egress surface. |
ureq |
holger-bench-scenarios, holger-server-lib |
Blocking HTTP client used by the separate garmr-ui desktop console to reach the read API. | Attaches the API bearer when set; a UI-side, not daemon, egress surface. | Separate binary (desktop UI). |
ed25519-dalek |
holger-update-bundle |
Signatures for every governed signed artifact: audit checkpoints, backup manifests, and release/airgap bundle manifests. | The out-of-band trust root for audit + backup/bundle verification; fail-closed on a bad key. | Security; signing/trust root. |
blake3 |
holger-agent-lib, holger, holger-server-lib, holger-update-bundle, holger-znippy-conda-repository, xtask |
Repository-wide hashing: the audit hash-chain, content-addressing for cold archives, stable log-template/query fingerprints, registry + dataset content identities, and backup/bundle digests. | Tamper-evidence for the audit chain and integrity for content-addressed stores; a hash change breaks the chain and every content id. | Runtime + Security. |
eframe |
holger-mannequin, holger-server-ui |
Native desktop console + the WASM topology map. | Separate binaries; not linked into the daemon. | Separate binaries. |
egui |
holger-mannequin, holger-server-ui |
Immediate-mode UI for the desktop console and map. | Separate binaries. | Separate binaries. |
High-coupling crates
| Crate | Incoming workspace edges | Outgoing workspace edges | Why changes are high impact |
holger-traits |
28 | 0 | widely depended on — edits ripple across the workspace |
holger-server-lib |
5 | 22 | widely depended on — edits ripple across the workspace |
holger-bench-scenarios |
4 | 7 | orchestrator — composes many crates |
holger-agent-cli |
0 | 8 | orchestrator — composes many crates |
holger-plugin-host |
3 | 5 | moderately central |
holger-agent-lib |
3 | 4 | moderately central |
holger-rust-znippy-repository |
4 | 3 | moderately central |
holger-server-ui |
0 | 7 | orchestrator — composes many crates |
holger-plugin-abi |
6 | 0 | widely depended on — edits ripple across the workspace |
holger-handler-rust-toolchain |
3 | 2 | moderately central |
holger-handler-skidbladnir |
3 | 2 | moderately central |
holger-znippy-maven-repository |
4 | 1 | moderately central |
holger-znippy-python-repository |
4 | 1 | moderately central |
holger-handler-rust-toolchain-wasm |
0 | 4 | moderately central |
holger-handler-skidbladnir-wasm |
0 | 4 | moderately central |
holger |
0 | 4 | moderately central |
holger-errcode |
3 | 0 | moderately central |
holger-handler-bundle-core |
2 | 1 | moderately central |
holger-nexus-connector |
2 | 1 | moderately central |
holger-update-bundle |
2 | 1 | moderately central |
holger-world-connector |
2 | 1 | moderately central |
xtask |
0 | 3 | moderately central |
holger-airgap |
2 | 0 | moderately central |
holger-artifactory-connector |
1 | 1 | moderately central |
holger-build |
1 | 1 | moderately central |
holger-build-space |
2 | 0 | moderately central |
holger-file-oci-repository |
1 | 1 | moderately central |
holger-mannequin |
1 | 1 | moderately central |
holger-rust-file-repository |
1 | 1 | moderately central |
holger-znippy-build-thing-repository |
1 | 1 | moderately central |
holger-znippy-composer-repository |
1 | 1 | moderately central |
holger-znippy-conda-repository |
1 | 1 | moderately central |
holger-znippy-deb-repository |
1 | 1 | moderately central |
holger-znippy-docker-repository |
1 | 1 | moderately central |
holger-znippy-gem-repository |
1 | 1 | moderately central |
holger-znippy-go-repository |
1 | 1 | moderately central |
holger-znippy-helm-repository |
1 | 1 | moderately central |
holger-znippy-npm-repository |
1 | 1 | moderately central |
holger-znippy-nuget-repository |
1 | 1 | moderately central |
holger-znippy-package-repository |
1 | 1 | moderately central |
holger-znippy-rpm-repository |
1 | 1 | moderately central |
holger-embed |
0 | 1 | moderately central |
Dependency validation:
- duplicate workspace edges after dedup: 0 (must be 0)
- external dependency present in workspace graph: false (must be false)
- workspace crates: 42, workspace edges: 99, external deps: 95
- dependency kind retained per edge; feature-gated edges marked
API reference
Every exported item (plus security-relevant private items), grouped by change-relevance, then crate → module. Each entry: signature, file:line, and its rustdoc.
Core contracts
(no crates in this group)
Runtime and CLI
(no crates in this group)
Ingest
(no crates in this group)
Storage, search, query
(no crates in this group)
Detection, policy, catalog, baselines
(no crates in this group)
Agent, LLM, tools
(no crates in this group)
Governance, audit, actions
(no crates in this group)
Retention, backup, HA
(no crates in this group)
API and UI
(no crates in this group)
Development tooling
Crate holger-agent-cli
Holger agent CLI: airgap and transfer artifacts between registries
Module holger_agent_cli
enum Source { World, Nexus, Artifactory, Holger, Znippy, TarZstd, Directory } · src/main.rs:11
enum Target { Znippy, TarZstd, Directory, Nexus, Artifactory, Holger } · src/main.rs:33
enum AuthMode { Oidc, Mtls } · src/main.rs:52
enum LockfileKind { Cargo, RequirementsTxt, UvLock } · src/main.rs:61
Package ecosystem lockfile format.
Crate holger-agent-lib
[agent-transfer] operations.rs: transfer/push/pull with bounded concurrency and partial-success tallies.
Agent crate: the airgap courier that moves artifacts across the network boundary. This is the public facade — the actual work lives in [operations], and every endpoint is a [ConnectorTrait] (Nexus, Artifactory, crates.io/PyPI world; holger/znippy/directory live server-side).
Flow: a Source connector's assets are pulled through operations into a carriable sink — a .znippy archive, a .tar.zst, or a loose directory — then, on the secure side, pushed from that sink back into a Target connector. Download/pack and read/upload are the two halves; the archive in between is the thing that crosses the airgap.
Gotcha: an upload failure is data, not an error — one bad asset is counted and skipped so a whole push does not abort, whereas a source read/decode error short-circuits. So a successful return can still hide failed assets; check the tallies. Untrusted .tar.zst input is per-entry size-capped against decompression bombs.
Module holger_agent_lib::operations
Agent transfer engine: the concrete Source→Target moves behind the CLI, one step above the
ConnectorTraitseam. Pull side downloads a connector's assets and lands them as a.znippyarchive (with ecosystempkg_typetags so the compressor runs the right plugin and the archive is natively browsable), a plain.tar.zst, or a loose directory. Push side reverses it: a directory,.znippy, or.tar.zstbecomes a stream ofconnector.upload_assetcalls.
pkg_typeis derived, never literal — [pkg_type_from_format] delegates to [traits::ArtifactFormat] (znippy's own registry), and [pkg_type_for_file] is a deliberately CURATED extension table that tags only unambiguous packaging (.crate,.jar,.whl, …) and leaves ambiguous ones (.tar.gzsdist vs npm tgz) untyped for an explicit--formatoverride; a drift-guard test pins it as a subset of znippy.Uploads fan out through a bounded in-flight pool (
znippy_zoomies::gatling,UPLOAD_CONCURRENCY): a per-asset upload failure is counted, not fatal, while a source-read/decode error short-circuits the whole push. The one gotcha: archive members are UNTRUSTED airgap input —read_capped/MAX_TAR_ENTRY_BYTESbound per-entry expansion (decompression bombs) andis_safe_member_path/safe_joinreject absolute or..paths, since a connector only trims a leading/and a stray..could otherwise write outside the repo (or drop an RCE file on the host).
fn pkg_type_from_format(format: &str) -> i8 · src/operations.rs:91
Map a Nexus/Artifactory/CLI format string → znippy pkg_type discriminant.
znippy is the single source of truth for these numbers: this delegates to [ArtifactFormat], which derives them from znippy's own handler registry. Unknown formats map to 0 (raw / unknown).
pub(crate) fn is_safe_member_path(rel: &str) -> bool · src/operations.rs:131
true iff rel is a safe relative member path to PUSH to a connector: no absolute/root/prefix and no .. component. Archive member paths come from an untrusted airgap-carried .znippy/.tar.zst, and connector.upload_asset only trims a leading /, so a .. member could place an artifact outside the intended repo path on the target. Mirrors the safe_join component policy; shared by both push paths so an offending member is skipped+counted, never uploaded.
async fn feed_repo_to_sender(connector: Arc<dyn ConnectorTrait>, assets: Vec<RemoteAsset>, sender: crossbeam_channel::Sender<ArchiveEntry>, pkg_type: i8, repo_name: String,) -> Result<(usize, usize)> · src/operations.rs:140
Feed assets from one repo into an already-open compressor sender. Returns (downloaded, failed) counts.
async fn dump_all_repos_to_znippy(connector: Arc<dyn ConnectorTrait>, repos: Vec<traits::RemoteRepository>, output: PathBuf, sequential: bool,) -> Result<()> · src/operations.rs:179
Dump all repos from a connector into one znippy archive.
Parallel mode (default): all repos downloaded concurrently — tokio tasks share cloned senders. The compressor's bounded channel provides backpressure so memory stays bounded. Sequential mode: one repo at a time — lower peak memory and network usage.
async fn transfer_to_znippy(connector: &dyn ConnectorTrait, assets: Vec<RemoteAsset>, output: PathBuf,) -> Result<()> · src/operations.rs:248
Transfer assets from a connector to a znippy archive (single repo, no pkg_type tagging).
async fn pack_directory_to_znippy(directory: PathBuf, output: PathBuf, format: Option<& str>,) -> Result<()> · src/operations.rs:333
Build a .znippy archive from every file under directory (recursively), preserving each file's path relative to directory as its archive member path.
This is the offline inverse of [push_znippy_to_connector]: stage a tree of real artifacts (.crate, .jar+.pom, .whl, …) and pack it into one immutable, content-addressed archive carriable across an airgap. Each member is tagged with its ecosystem pkg_type (by extension, or forced by format when given) so the znippy compressor runs that ecosystem's plugin and writes the typed index columns — the archive is then natively browsable by holger (as_maven()/as_python()/…), not just a raw file bag. A format override (e.g. "python") tags every file with that ecosystem, which is how ambiguous extensions (.tar.gz sdists) get indexed.
async fn transfer_to_tar_zstd(connector: &dyn ConnectorTrait, assets: Vec<RemoteAsset>, output: PathBuf,) -> Result<()> · src/operations.rs:402
Transfer assets from a connector to a zstd-compressed tar archive (.tar.zst)
async fn transfer_to_directory(connector: &dyn ConnectorTrait, assets: Vec<RemoteAsset>, output: PathBuf,) -> Result<()> · src/operations.rs:450
Transfer assets from a connector to a directory
async fn push_directory_to_connector(connector: &dyn ConnectorTrait, directory: PathBuf, repository: &str,) -> Result<()> · src/operations.rs:497
Push assets from a directory to a connector
async fn push_znippy_to_connector(connector: &dyn ConnectorTrait, archive: PathBuf, repository: &str,) -> Result<()> · src/operations.rs:542
Push assets from a znippy archive to a connector
async fn push_tar_zstd_to_connector(connector: &dyn ConnectorTrait, archive: PathBuf, repository: &str,) -> Result<()> · src/operations.rs:589
Push assets from a tar.zst archive to a connector
async fn unpack_znippy_to_directory(archive: PathBuf, output: PathBuf) -> Result<()> · src/operations.rs:674
Unpack a znippy archive to a directory — the inverse of [pack_directory_to_znippy]. Each member is extracted and written under output at its (path-safe) relative path. Members with hostile paths (absolute or containing ..) are skipped, never written outside output.
async fn unpack_tar_zstd_to_directory(archive: PathBuf, output: PathBuf) -> Result<()> · src/operations.rs:722
Unpack a .tar.zst archive to a directory. Each regular entry is extracted (bounded by [MAX_TAR_ENTRY_BYTES] — the stream is untrusted airgap input) and written under output at its (path-safe) relative path.
async fn pack_directory_to_tar_zstd(directory: PathBuf, output: PathBuf) -> Result<()> · src/operations.rs:771
Pack a directory into a .tar.zst archive — the inverse of [unpack_tar_zstd_to_directory], and the on-disk twin of [transfer_to_tar_zstd] (which packs from a connector). Every regular file under directory is appended at its path relative to directory.
async fn convert_znippy_to_tar_zstd(input: PathBuf, output: PathBuf) -> Result<()> · src/operations.rs:819
Convert a znippy archive to a .tar.zst archive by staging through a temp directory (unpack → repack). The direct archive↔archive conversions compose the directory primitives rather than duplicating format logic.
async fn convert_tar_zstd_to_znippy(input: PathBuf, output: PathBuf, format: Option<& str>,) -> Result<()> · src/operations.rs:827
Convert a .tar.zst archive to a znippy archive by staging through a temp directory (unpack → repack). format optionally forces the znippy pkg_type.
Module holger_agent_lib::strict
Strict transfer mode + provenance receipts.
The existing transfer functions in [
crate::operations] intentionally treat a per-asset failure as a tally and returnOk(())with failures counted (the documented airgap-dump compatibility contract). This module adds an opt-in strict mode alongside that behavior — it does not change the existing functions, so best-effort semantics are preserved by construction.In [
TransferMode::Strict], any failed download, hostile asset path, size mismatch, or missing asset makes the whole operation fail (a non-zero result), a partial archive never appears under the final filename (temp + atomic rename), and a [TransferReceipt] — suitable for embedding in a signed update bundle — records every asset's dual digest with credentials stripped from all endpoints.
const DEFAULT_MAX_ASSET_BYTES: u64 · src/strict.rs:26
8 GiB per-asset cap (matches the connector download cap).
enum TransferMode { BestEffort, Strict } · src/strict.rs:30
Transfer failure policy.
struct TransferOptions { mode: TransferMode, max_asset_bytes: u64, source_type: String, source_endpoint: String, receipt_path: Option<PathBuf> } · src/strict.rs:38
Options for a strict-capable transfer.
struct TransferFailure { asset_path: String, reason: String } · src/strict.rs:66
A safe, credential-free description of one failed asset.
struct AssetRecord { source_path: String, size: u64, sha256: String, blake3: String, source_uri: Option<String> } · src/strict.rs:73
Per-asset provenance record.
struct TransferReport { requested_assets: usize, successful_assets: usize, failed_assets: usize, failures: Vec<TransferFailure>, bytes_transferred: u64, complete: bool } · src/strict.rs:84
Structured outcome of a transfer.
struct TransferReceipt { operation_id: String, mode: String, started_at: DateTime<Utc>, completed_at: DateTime<Utc>, source_type: String, source_endpoint: String, source_repository: String, destination_type: String, requested_assets: usize, successful_assets: usize, failed_assets: usize, bytes_transferred: u64, assets: Vec<AssetRecord>, failures: Vec<TransferFailure>, final_archive_digest: Option<String>, complete: bool } · src/strict.rs:95
A provenance receipt, suitable for embedding as bundle evidence.
fn strip_credentials(url: &str) -> String · src/strict.rs:134
Remove user:pass@ userinfo from the authority of a URL, if present.
async fn transfer_repo_to_znippy_with_options(connector: &dyn ConnectorTrait, repository: &str, output: &Path, opts: &TransferOptions,) -> Result<TransferReport> · src/strict.rs:170
Transfer every asset in repository from connector into a .znippy archive at output, honoring [TransferOptions::mode]. Lists assets from the connector, then delegates to [transfer_assets_to_znippy_with_options].
async fn transfer_assets_to_znippy_with_options(connector: &dyn ConnectorTrait, repository: &str, assets: &[RemoteAsset], output: &Path, opts: &TransferOptions,) -> Result<TransferReport> · src/strict.rs:186
Transfer a pre-fetched assets list from connector into a .znippy archive at output, honoring [TransferOptions::mode]. Returns a structured report. In strict mode an incomplete transfer returns Err (after writing the receipt when possible) and leaves no archive at output. Used by the CLI, where the asset list comes from a lockfile (world) or a listing (nexus/artifactory/holger).
Module holger_agent_lib::strict::TransferOptions
fn new(mode: TransferMode, source_type: impl Into<String>, source_endpoint: impl Into<String>,) -> Self · src/strict.rs:49
Crate holger-airgap
[airgap] Pure-Rust plan/argv; one shell-out to distribution/airgap/populate.
A typed Rust front-end to distribution/airgap/populate (the documented POSIX-sh build→carry→push driver). The UI Tools#5 pane and the holger-server airgap CLI verb both drive this crate, which:
- models the populate interface ([
Stack], [Scale], [Target], [Auth], [PopulateArgs]) as types; - plans an invocation in pure Rust — [
PopulateArgs::validate], [PopulateArgs::stacks] (scale→stack expansion), [PopulateArgs::argv], [PopulateArgs::plan] — all unit-tested, no shell, no I/O; - executes by delegating to the driver script (the inherently-external build/push half —
docker pull,pip download, the agent's gRPC push) via [execute]. The pure-Rust core is the testable seam; only the actual carry-the-bytes work shells out, to the one sanctioned driver.
All three targets — holger / nexus / artifactory — are wired (the agent gained an --to artifactory path via agent/connector/artifactory, and lib/push.sh routes it). Artifactory pushes need ARTIFACTORY_URL in the driver's environment.
Module holger_airgap
enum Stack { Airflow, Cargo, Npm, Maven, Docker, All } · src/lib.rs:32
Which software stack(s) to populate. All expands to every stack; otherwise a single ecosystem. With no explicit stack the [Scale] preset decides.
enum Scale { Micro, Small, Large } · src/lib.rs:73
Dataset scale preset. Drives both the per-stack closure size and (when no explicit [Stack] is given) which stacks run — mirroring stacks.toml.
enum Target { Holger, Nexus, Artifactory } · src/lib.rs:118
Where to populate. All three targets push through the agent: Holger (default), Nexus, and Artifactory (routed by lib/push.sh via the agent/connector/artifactory path; needs ARTIFACTORY_URL in the driver's environment). [PopulateArgs::validate] accepts every variant (guarded by the validate_allows_artifactory_now_that_agent_is_wired test).
enum Auth { Mtls, Oidc } · src/lib.rs:149
Auth the push half uses against the target.
const DEFAULT_ENDPOINT: & str · src/lib.rs:175
The dev-pair default gRPC control door.
const DEFAULT_GATEWAY: & str · src/lib.rs:178
The dev-pair default HTTP/native + OCI gateway (holger-server dev-pair --http 127.0.0.1:18444, matching stacks.toml).
struct PopulateArgs { stack: Option<Stack>, scale: Scale, target: Target, endpoint: String, gateway: String, auth: Auth, repository: Option<String>, staging: Option<PathBuf>, build_only: bool, push_only: bool, dry_run: bool, client_cert: Option<PathBuf>, client_key: Option<PathBuf>, token_url: Option<String>, client_id: Option<String> } · src/lib.rs:185
A fully-specified populate run — the typed mirror of the populate CLI. Built by the UI Tools#5 pane / the CLI verb from the connected server's live endpoint + session auth, then validated, planned, and (optionally) [execute]d.
fn driver_in(repo_root: &Path) -> Option<PathBuf> · src/lib.rs:367
The default driver location: distribution/airgap/populate resolved against a repo root. None if it isn't there (e.g. an installed binary with no repo).
struct Report { log: String, exit_code: i32, success: bool } · src/lib.rs:374
One stack's outcome line from a populate run.
fn execute(args: &PopulateArgs, driver: &Path) -> Result<Report> · src/lib.rs:390
Execute the populate run by delegating to the driver script. The inherently-external build/push (docker/pip/agent-gRPC) lives in the driver; this captures its stdout+stderr and exit status into a [Report]. Validates first, so a not-yet-wired target / bad combo fails fast with a clear message.
NOTE: this is the one place holger shells out, and only to the documented driver (README wiring point #1). The pure-Rust planning surface above is the testable core; this is the thin execution seam.
Module holger_airgap::Auth
fn as_arg(self) -> &'static str · src/lib.rs:155
Module holger_airgap::PopulateArgs
fn validate(&self) -> Result<()> · src/lib.rs:242
Reject impossible combinations before any work:
--build-onlyand--push-onlytogether;- mTLS without a cert+key, or OIDC without a token-url+client-id (only when actually pushing —
build-onlyanddry-runneed no creds).
All three targets (holger / nexus / artifactory) are now wired in the agent + lib/push.sh; artifactory pushes need ARTIFACTORY_URL in the driver's environment.
fn stacks(&self) -> Vec<Stack> · src/lib.rs:266
The concrete stacks this run touches: the explicit stack (with All expanded), else the scale preset's stacks.
fn argv(&self, driver: &Path) -> Vec<String> · src/lib.rs:278
Build the argv for invoking the populate driver at driver. Mirrors the flags documented in distribution/airgap/README.md exactly.
fn plan(&self) -> String · src/lib.rs:324
A human-readable, multi-line plan of what would run — the dry-run surface the UI Tools#5 pane shows before the operator commits. Pure; no I/O.
fn state_json(&self) -> serde_json::Value · src/lib.rs:350
Observable state for headless/robot tests.
Module holger_airgap::Scale
fn as_arg(self) -> &'static str · src/lib.rs:80
fn preset_stacks(self) -> &'static [Stack] · src/lib.rs:90
The stacks a bare --scale expands to (the [scales.*] table in stacks.toml): micro = airflow; small = airflow+cargo+npm; large = all.
Module holger_airgap::Stack
fn as_arg(self) -> &'static str · src/lib.rs:42
Module holger_airgap::Target
fn as_arg(self) -> &'static str · src/lib.rs:125
Crate holger-artifactory-connector
Holger connector for JFrog Artifactory (REST + AQL, basic/bearer/mTLS auth)
JFrog Artifactory connector for the holger agent — the Artifactory twin of connector_nexus, implementing the same [ConnectorTrait] so the agent's migrate / suck / push operations work against Artifactory unchanged.
API surface used (Artifactory REST + AQL):
- list repositories —
GET /artifactory/api/repositories({key, type, packageType, url}). - list assets — AQL
POST /artifactory/api/search/aql,items.find({"repo":"<repo>"}).include("repo","path","name","size"). Builds each asset's download URL from the base +repo/path/name. - download —
GET /artifactory/<repo>/<path>/<name>. - upload (deploy) —
PUT /artifactory/<repo>/<path>.
Auth mirrors the Nexus connector: HTTP Basic, OIDC/access-token Bearer, mTLS, or None. The same SSRF / credential-exfiltration guard applies on download (only same-origin URLs get credentials).
Module holger_artifactory_connector
enum ConnectorAuth { None, Basic, OidcToken, Mtls } · src/lib.rs:47
How the connector authenticates with Artifactory.
struct ArtifactoryConnector { url: String, auth: ConnectorAuth } · src/lib.rs:59
A holger-agent connector for a JFrog Artifactory instance.
Module holger_artifactory_connector::ArtifactoryConnector
fn new(url: String, username: Option<String>, password: Option<String>) -> Result<Self> · src/lib.rs:68
Build a connector with optional Basic credentials.
fn with_oidc_token(url: String, token: String) -> Result<Self> · src/lib.rs:84
Build a connector with a Bearer access token (Artifactory access token / OIDC bearer).
fn with_mtls(url: String, cert_pem: &[u8], key_pem: &[u8]) -> Result<Self> · src/lib.rs:99
Build a connector with an mTLS client cert.
Crate holger-bench-scenarios
[benchmarks] No [[bench]] targets exist; real path is nornir bench run holger / xtask examples.
Shared bench scenarios for the holger constellation.
Two consumers call into this lib:
repository-znippy-rust/tests/...— correctness-flavoured integration tests (#[test]), exercise the same code paths with assertions.holger/xtask/examples/nornir-bench.rs—Bencherimpls thatnornir bench run holgerdrives, results land in the icebergbench_runstable at<workspace>/.nornir/warehouse.
Keep this crate nornir-free. Persistence is nornir's job; the contract here is "build a workload, return numbers". See workspace_holger/WORKSPACE.md (Bench pattern section).
Three artifact types: Rust crates, Java jars, Python wheels.
- Location — under the home directory, never
/tmp(it is tmpfs/RAM here, ~31 GiB) and never the T9 external drive. Override withHOLGER_BENCH_DIR; default$HOME/holger-bench-data. - Size — driven by a target byte budget (
Scale::target_bytes), so ahugerun builds ~100 GiB of served data per type for real throughput stress.quick/fullkeep the old entry-count workloads. - Freshness — fixtures are keyed by
(FIXTURE_VERSION, real?, size)and a sidecar.count; a param/version change regenerates, otherwise an existing archive is reused (you do NOT want to rewrite 100 GiB every run). - Real vs synthetic — heavy implies real. On the heavy tiers (
full/huge) the fixtures default to real packages downloaded once into the cache (crates.io / Maven Central / PyPI), then replicated (re-id'd) to reach the byte budget — real bytes/compression at scale without pulling 100 GiB of distinct artifacts off public registries. The light tiers (quick/sane) stay fast synthetic (make_fake_*).HOLGER_BENCH_REAL=0forces synthetic even when heavy;=1forces real even on the light tiers.
Module holger_bench_scenarios
fn boot_timeout_secs(default: u64) -> u64 · src/lib.rs:105
Container boot/readiness timeout (seconds) for the JVM competitor servers (Nexus, Artifactory). JVMs are slow to start on a loaded bench host, so the per-server default is overridable with HOLGER_BENCH_BOOT_SECS.
fn ensure_container_host() · src/lib.rs:128
Make a container runtime discoverable to testcontainers before the first image.start().
testcontainers talks to a Docker-compatible socket. When DOCKER_HOST is unset it defaults to unix:///var/run/docker.sock, which does not exist on a rootless-Podman box (the socket lives at $XDG_RUNTIME_DIR/podman/podman.sock). That mismatch is the first reason the Nexus/Artifactory containers "couldn't be brought up": the bench skipped with an opaque container start failed before any image was even pulled.
This probes — only when DOCKER_HOST is unset and no docker socket is present — for the rootless Podman API socket and exports DOCKER_HOST so the existing Podman service answers testcontainers. A pre-set DOCKER_HOST (or a real docker socket) is always respected. No-op on success of either; safe to call repeatedly. Pure Rust, no shelling.
fn bench_root() -> PathBuf · src/lib.rs:166
Root dir for all bench fixtures — home directory, never /tmp (tmpfs) and never the T9 external drive. HOLGER_BENCH_DIR overrides; default $HOME/holger-bench-data.
fn real_enabled(scale: &Scale) -> bool · src/lib.rs:183
Whether to seed from real downloaded packages (crates.io / Maven Central / PyPI), fetched once into the cache then replicated to size.
Heavy implies real. On the heavy tiers (full/huge) real artifacts are the default — "heavy" means real jars/crates/wheels without having to say so. HOLGER_BENCH_REAL=0 is the explicit opt-out (force synthetic even when heavy); HOLGER_BENCH_REAL=1 forces real even on the light tiers.
struct Scale { entries: usize, bench_secs: u64, threads_mt: usize, target_bytes: Option<u64> } · src/lib.rs:194
Tunable workload knobs. entries-driven (quick/full) or target_bytes- driven (huge). Both produce metrics under the same names, so regressions are comparable as long as the scale stays constant within a series.
fn cores() -> usize · src/lib.rs:206
Available hardware parallelism — the thread count every *_mt scenario and Scale::{sane,quick,full,huge} scales to. Public so consumers (e.g. the holger_multicore_sanity matrix cell) report the same number the workload ran on.
fn total_ram_bytes() -> Option<u64> · src/lib.rs:214
Total system RAM in bytes, read purely in Rust from /proc/meminfo (MemTotal:) — never by shelling free. Returns None off Linux or if the file is missing/unparseable, so callers can fall back conservatively.
struct FairResources { cores: usize, container_mem_bytes: u64, heap_mb: u64, direct_mb: u64 } · src/lib.rs:248
Fair-resourcing knobs for the JVM competitor containers (Nexus, Artifactory).
The release-gate bench bombards Holger in-process on the full machine, so letting the Nexus / Artifactory containers fall back to their default JVM heap (~512 MB – 1 GB) — or, worse, pinning Nexus to a deliberate -Xmx512m (see [nexus::latency_tail]'s old config) — made the competitors look artificially slow. That is the core unfairness the audit flagged.
[FairResources::detect] reads THIS machine at runtime (pure Rust: [cores] = available_parallelism, [total_ram_bytes] = /proc/meminfo; never shelling nproc/free) and hands each container:
--cpus= every logical core (matches Holger'sthreads_mt = cores());--memory= 75 % of total RAM (host + Holger keep the rest);- a JVM max heap (
-Xmx) = 50 % of the container memory + a direct- memory slice — a genuinely fair heap, which is the whole point of the comparison. The remaining ~37 % inside the cap feeds metaspace, code cache, thread stacks and OS page cache so the JVM is well-fed, not OOM-killed.
struct Throughput { ops: u64, elapsed: Duration, bytes: u64, ops_sec: f64, mb_per_sec: f64 } · src/lib.rs:372
Result of one throughput scenario. ops_sec is the headline number both no_regression and nexus_floor gates compare on.
fn build_crate_archive(n: usize) -> Result<PathBuf> · src/lib.rs:725
fn build_jar_archive(n: usize) -> Result<PathBuf> · src/lib.rs:728
fn build_pip_archive(n: usize) -> Result<PathBuf> · src/lib.rs:731
fn build_crate_corpus(dir: &Path, target_bytes: u64) -> Result<(PathBuf, usize)> · src/lib.rs:747
Build (or reuse) a synthetic crate corpus of at least target_bytes of artifact data as a znippy archive under dir, returning (archive_path, crate_count). Reuses an existing archive of the same budget.
fn build_pip_corpus(dir: &Path, target_bytes: u64) -> Result<(PathBuf, usize)> · src/lib.rs:767
Build (or reuse) a synthetic wheel corpus of at least target_bytes under dir, returning (archive_path, package_count). The Python sibling of [build_crate_corpus]: lays each wheel at packages/{name}/{file} exactly as [crate::served::run_pip_index_fetch] / [PipRepoZnippy] expect, so the served PEP 503 index resolves. Reuses an existing archive of the same budget.
fn pip_corpus(scale: &Scale) -> Result<(PathBuf, usize)> · src/lib.rs:791
The wheel corpus for a bench scale (byte-budgeted on the heavy tiers, entry-budgeted otherwise) — the same fixture the in-process pip_wheel_* arms use, so the served holger.pip_index_serve arm and the in-proc arms share one archive. real mode seeds real PyPI wheels; see [real_enabled].
fn free_disk_bytes(path: &Path) -> Option<u64> · src/lib.rs:800
Free (available-to-unprivileged) disk space in bytes on the filesystem holding path, read purely in Rust via statvfs(3) (libc) — never by shelling df. Mirrors [total_ram_bytes]'s pure-Rust RAM detection. Returns None if the syscall fails, so callers fall back conservatively.
fn free_disk_bytes(_path: &Path) -> Option<u64> · src/lib.rs:814
Non-Unix fallback: no pure-Rust free-disk probe available.
fn propose_dataset_bytes(scale: u64, free_bytes: Option<u64>) -> u64 · src/lib.rs:823
Propose a crate-corpus byte budget for scale given free_bytes available on the data filesystem: scale × 1 GiB of artifact data, clamped to ≤ 25 % of free space (headroom for the OS, the compressed archive and the page cache), and floored at 16 MiB so a tiny smoke run still has data. None free space (detection failed) leaves the want unclamped.
fn rust_crate_st(scale: &Scale) -> Result<Throughput> · src/lib.rs:898
fn rust_crate_mt(scale: &Scale) -> Result<Throughput> · src/lib.rs:911
fn java_jar_st(scale: &Scale) -> Result<Throughput> · src/lib.rs:929
fn java_jar_mt(scale: &Scale) -> Result<Throughput> · src/lib.rs:949
fn pip_wheel_st(scale: &Scale) -> Result<Throughput> · src/lib.rs:974
fn pip_wheel_mt(scale: &Scale) -> Result<Throughput> · src/lib.rs:992
Module holger_bench_scenarios::FairResources
fn detect() -> Self · src/lib.rs:263
Detect from the live machine. Falls back to a modest 8 GiB RAM assumption if /proc/meminfo is unreadable, so we never over-promise memory.
fn jvm_args(&self, extra: &str) -> String · src/lib.rs:275
JVM parameters for a fair heap, shared by Nexus (INSTALL4J_ADD_VM_PARAMS) and Artifactory (EXTRA_JAVA_OPTIONS). extra appends scenario-specific flags (e.g. GC-pause targets / logging); later flags win in the JVM.
Module holger_bench_scenarios::Scale
fn sane() -> Self · src/lib.rs:292
Sane-speed workload for the test matrix — a sub-2s multicore sanity probe, not a measurement. Small entry count + a 1s loop per phase so the holger_multicore_sanity matrix cell runs inside a normal cargo test while still exercising the lock-free read path on every core (threads_mt = available_parallelism()).
fn quick() -> Self · src/lib.rs:297
Fast workload for CI / smoke runs (~seconds total).
fn full() -> Self · src/lib.rs:302
Production-grade workload (~minutes per scenario).
fn huge() -> Self · src/lib.rs:308
~100 GiB of served data per type — large-scale ("drift") throughput stress. Needs a disk-backed bench_root() with room for 3×100 GiB.
fn is_heavy(&self) -> bool · src/lib.rs:316
Heavy = production-grade or byte-budget scale (full/huge) — the tier where [real_enabled] defaults to real downloaded artifacts. quick/sane stay synthetic. A HOLGER_BENCH_BYTES override (which sets target_bytes on any scale) also counts as heavy.
fn from_env() -> Self · src/lib.rs:324
Resolve from env: HOLGER_BENCH_SCALE = quick | full | huge (default quick); HOLGER_BENCH_BYTES overrides the byte budget (any scale becomes byte-driven). NORNIR_BENCH_QUICK=0 is honoured as full for back-compat.
Module holger_bench_scenarios::artifactory
Artifactory-vs-Holger comparison scenario — the heavy, fixture-gated sibling of [
crate::nexus].Mirrors
nexus::nexus_load: it pushes the same cached corpus (crate / jar / wheel znippy archives) through a real JFrog Artifactory OSS container, then drives a served-FETCH-over-HTTP head-to-head — the SAME pure-Rust parallel GET pool ([crate::served::bombard_http_get]) against both a real Holger HTTP server and Artifactory — producing the pairedholger_served_mbs+artifactory_served_mbs(+ops_per_sec) metrics the READMEbenchesmashup bolds head-to-head. The competitor upload throughput is kept as a separateartifactory_upload_mbspublish-phase number, and the old in-processget_file()figure asholger_inprocess_read_ceiling_mbs(an internal Holger-vs-Holger regression ceiling, never beside the competitor).Fixture gate (NOT
--heavy)Exactly like the Nexus scenario, the Artifactory testcontainer is a fixture gate: any failure to reach a container runtime / boot the server becomes an
is_skiperror which the bench harness records as a SKIP row, so this runs in the normal (non---heavy) pass and no-ops gracefully on a box with no Docker/Podman or where the (heavy) image cannot boot in time.Compiled only under the
artifactorycargo feature so the heavy deps (testcontainers,tokio,reqwest,agent-lib) never reach the light default build ofholger-bench-scenarios.
fn is_skip(e: &anyhow::Error) -> bool · src/artifactory.rs:61
True iff e is one of our graceful skips (vs a genuine workload failure).
struct LoadOutcome { concurrency: usize, holger_served_mbs: f64, artifactory_served_mbs: f64, ops_per_sec: f64, artifactory_ops_per_sec: f64, holger_p50_ms: f64, holger_p99_ms: f64, holger_p999_ms: f64, holger_mean_ms: f64, artifactory_p50_ms: f64, artifactory_p99_ms: f64, artifactory_p999_ms: f64, artifactory_mean_ms: f64, artifactory_ok_ops: u64, artifactory_repo: String, artifactory_upload_mbs: f64, holger_inprocess_read_ceiling_mbs: f64, elapsed_ms: f64, holger_files: usize } · src/artifactory.rs:79
Outcome of [artifactory_load]: the honest, like-for-like Holger-vs-Artifactory throughput. Same shape + semantics as [crate::nexus::LoadOutcome].
HEAD-TO-HEAD (both measured the SAME way — concurrent GETs over HTTP):
holger_served_mbs— fetched from a real Holger HTTP server.artifactory_served_mbs— fetched from the Artifactory HTTP server.
artifactory_upload_mbs is the publish phase (separate, not the head-to-head); holger_inprocess_read_ceiling_mbs is the internal in-process regression ceiling — NEVER placed beside the competitor.
async fn artifactory_load(scale: &Scale) -> Result<LoadOutcome> · src/artifactory.rs:352
Honest, like-for-like Holger-vs-Artifactory throughput — the sibling of [crate::nexus::nexus_load]. Upload the corpus to Artifactory (publish phase), stand up a real Holger HTTP server over the same corpus ([crate::served::start_holger_gateway]), then drive the SAME pure-Rust parallel GET pool at each over HTTP (the download-heavy head-to-head).
Returns holger_served_mbs + artifactory_served_mbs (head-to-head), artifactory_upload_mbs (publish phase, separate) and the in-process holger_inprocess_read_ceiling_mbs (internal ceiling only). Fixture-gated.
Module holger_bench_scenarios::audit
Audit-log append throughput scenario.
Drives holger's REAL [
server_lib::audit::ArrowIpcAuditLog::record] — the durable audit write that fires on the hot path of every fetch / put / delete / list — in a tight serial loop, exactly how the live server drives it (one sharedArc<dyn AuditLog>across all request handlers,recordtaking&selfand internallyMutex-synchronised). It reports the append throughput: records/sec (ops_sec) + on-disk MB/s (mb_per_sec).Each
recordencodes a one-row ArrowRecordBatch, writes it to the IPC stream, and flushes the underlyingBufWriter → Fileto the OS — the full per-request audit cost. Every event carries distinct variable-length fields (identity / repo / artifact / source-ip / detail) so no accidental constant folding hides work.This is a single-writer measurement (the server serialises audit writes behind one mutex), NOT a fan-out — no parallelism primitive is used here.
Twin correctness coverage lives in
server/lib/src/audit/*unit tests (the round-trip decode); this module is the numbers. After the burst the segment is finalized (drop writes the IPC end-of-stream marker) and re-read from disk; the row count is asserted so a broken writer fails the bench instead of silently reporting a hollow number.
fn audit_append_throughput(scale: &Scale) -> Result<Throughput> · src/audit.rs:71
Append [event_count] events through a real [ArrowIpcAuditLog], returning the throughput (records/sec = ops_sec, on-disk bytes/sec = mb_per_sec). The segment is finalized and re-read; the round-tripped row count is asserted so a broken append path fails the bench rather than reporting a hollow number.
Module holger_bench_scenarios::nexus
Nexus-vs-Holger comparison scenarios — the heavy, fixture-gated benches.
These port the two
#[ignore]'d integration tests
agent/cli/tests/integration_nexus_load.rs::test_nexus_load_with_real_artifactsagent/cli/tests/integration_latency_tail.rs::test_tail_latency_gc_pressureinto reusable workload functions that both thenornirBencher harness (xtask/examples/nornir-bench.rs) and downscaled correctness twins (agent/cli/tests/integration_nexus_bench_twin.rs) call.Fixture gate (NOT
--heavy)"heavy" means large scale, not needs infra. The Sonatype Nexus 3 testcontainer is a fixture gate: each scenario tries to start/connect to it and, if Docker/Podman or the image is unavailable, returns
Err(NexusUnavailable)which callers map to a recorded skip, not a failure. So these run in the normal (non---heavy) bench pass; they just no-op gracefully wherever there is no container runtime.This module is compiled only under the
nexuscargo feature so the heavy deps (testcontainers,tokio,reqwest,agent-lib) never reach the light default build ofholger-bench-scenarios.
fn is_skip(e: &anyhow::Error) -> bool · src/nexus.rs:48
True iff e is one of our graceful skips (vs a genuine workload failure).
struct LoadOutcome { concurrency: usize, holger_served_mbs: f64, nexus_served_mbs: f64, ops_per_sec: f64, nexus_ops_per_sec: f64, holger_p50_ms: f64, holger_p99_ms: f64, holger_p999_ms: f64, holger_mean_ms: f64, nexus_p50_ms: f64, nexus_p99_ms: f64, nexus_p999_ms: f64, nexus_mean_ms: f64, nexus_upload_mbs: f64, holger_inprocess_read_ceiling_mbs: f64, elapsed_ms: f64, holger_files: usize } · src/nexus.rs:71
Outcome of [nexus_load]: the honest, like-for-like Holger-vs-Nexus throughput.
HEAD-TO-HEAD (both measured the SAME way — concurrent GETs over HTTP):
holger_served_mbs— fetched from a real Holger HTTP server ([crate::served::start_holger_gateway], the native HTTP door).nexus_served_mbs— fetched from the Nexus HTTP server.
PUBLISH PHASE (separate, NOT the head-to-head): nexus_upload_mbs is Nexus's upload throughput while the corpus is pushed in. Kept for context; Holger's symmetric publish-over-HTTP is the next step (see module docs / holger-ds).
INTERNAL CEILING (NOT a competitor comparison): holger_inprocess_read_ceiling_mbs is the old in-process get_file() figure — a Holger-vs-Holger release-regression ceiling only. Deliberately kept apart from the served head-to-head so a demo can never show it beside Nexus.
struct TailOutcome { p50_ms: f64, p99_ms: f64, throughput_mbs: f64, samples: usize, heap_mb: u64 } · src/nexus.rs:102
Outcome of [latency_tail]: tail-latency percentiles under GC pressure.
async fn nexus_load(scale: &Scale) -> Result<LoadOutcome> · src/nexus.rs:278
Honest, like-for-like Holger-vs-Nexus throughput. The realistic registry workload is download-heavy, so the core number is a served-FETCH phase run identically against both: upload the corpus into Nexus, stand up a real Holger HTTP server ([crate::served::start_holger_gateway]) over the same corpus, then drive the SAME pure-Rust parallel GET pool ([crate::served::bombard_http_get]) at each — HTTP fetch vs HTTP fetch.
Returns holger_served_mbs + nexus_served_mbs (the head-to-head), Nexus's nexus_upload_mbs (publish phase, separate), and the in-process holger_inprocess_read_ceiling_mbs (internal regression ceiling only). Fixture-gated: returns an is_skip error if no container.
async fn latency_tail(scale: &Scale) -> Result<TailOutcome> · src/nexus.rs:415
Start Nexus with a fair heap (detected from this machine — ~50 % of the container memory, all cores, ~75 % RAM), upload archives, then record per-request latencies and compute p50/p99 plus throughput. Fixture-gated.
FAIRNESS (årsstädning 2026-06-28): this scenario previously pinned Nexus to a deliberate -Xms512m -Xmx512m to "force GC pressure" — i.e. it handicapped the competitor to a tiny heap while Holger ran on the whole box. That made the p99 comparison dishonest, so we now give Nexus the same fair heap as [nexus_load]. The MaxGCPauseMillis=50 target + GC logging are kept as the only scenario-specific tuning. heap_mb reports the fair heap actually used.
Module holger_bench_scenarios::promote
Promotion-through-jera throughput scenario.
Seals a burst of distinct artifacts through the REAL [
server_lib::promote::PromotionEngine] with a durable [jera::jobs::JobStore] ledger attached, then reports the throughput of the whole hot path a live promotion runs:
- blake3 content-address the bytes,
- write the content-addressed blob (atomic tmp+rename),
- create the exclusive ref (the immutability seal),
- write the
running → donejob row into the redb ledger.Distinct bytes per artifact defeat content-address dedup, so every seal does the full blob write + a fresh ledger row — the realistic per-promotion cost. The ledger is a single-writer redb file (exactly how the server drives it), so this is a serial writer measurement, not a fan-out.
Twin correctness coverage lives in
server/lib/src/promote/mod.rsunit tests (seal_records_a_done_job_in_the_ledgeret al.); this module is the numbers.
fn promote_ledger_throughput(scale: &Scale) -> Result<Throughput> · src/promote.rs:60
Seal [seal_count] distinct artifacts through a real [PromotionEngine] with a jera job ledger attached, returning the throughput (seals/sec = ops_sec, bytes/sec = mb_per_sec). Every seal is a durable done job in the ledger; the count is asserted at the end so a broken ledger fails the bench instead of silently reporting a hollow number.
Module holger_bench_scenarios::retention
Retention/GC planner throughput scenario.
Runs holger's real, grouped [
server_lib::retention::plan_retention] over a synthetic corpus of artifact records and pits it head-to-head against a naive baseline — the O(n²) full-scan an ad-hoc "keep last N versions" script writes when it doesn't group first (for each record, scan every other record to count newer same-name versions). Both compute the same keep/delete verdict (asserted equal), so the numbers are an honest "holger's planner is faster at the identical result" comparison.Wide, contested, system-prefixed metrics so the README
mashupauto-discoversholger_*vsnaive_*per suffix:records_per_sec,plans_per_sec,mean_ms,p50_ms,p99_ms,metadata_mbs. holger wins every column at scale (grouped-once vs scan-per-record).Twin correctness coverage lives in
server/lib/src/retention.rsunit tests +server/lib/tests/retention_adapter.rs; this module is the numbers.
struct RetentionThroughput { records: usize, names: usize, holger_iters: usize, naive_iters: usize, delete_count: usize, holger: SystemNums, naive: SystemNums, large_records: usize, large_records_per_sec: f64, large_plans_per_sec: f64, large_mean_ms: f64, large_delete_count: usize } · src/retention.rs:139
The full head-to-head result: both systems' numbers + the shared correctness witness (identical delete count) and corpus shape, plus holger's throughput at a much larger, holger-only corpus (the heavy absolute number).
struct SystemNums { records_per_sec: f64, plans_per_sec: f64, mean_ms: f64, p50_ms: f64, p99_ms: f64, metadata_mbs: f64 } · src/retention.rs:157
One system's public numbers (flat, so the bench runner can splat them into the metric map with the right prefix).
fn retention_planner_throughput(scale: &Scale) -> Result<RetentionThroughput> · src/retention.rs:183
Run the retention planner head-to-head. holger's grouped planner and the naive O(n²) baseline each run iters plans over the same corpus with the same keep_last_n; both delete counts must agree. Returns both systems' throughput
- latency numbers.
struct RetentionExecutorNums { records: usize, names: usize, delete_count: usize, deleted: usize, reclaimed_bytes: u64, reclaim_per_sec: f64, reclaim_mbs: f64, reclaim_ms: f64, dryrun_per_sec: f64, dryrun_mbs: f64, dryrun_iters: usize } · src/retention.rs:286
The executor's public numbers.
fn retention_executor_throughput(scale: &Scale) -> Result<RetentionExecutorNums> · src/retention.rs:349
Run the retention executor throughput bench: preview (dry-run) then real reclaim over a live file-backed repo. Returns both throughput numbers plus the correctness witness (deleted == planned delete_count).
Module holger_bench_scenarios::secure_update
Secure Update Gateway bundle-path throughput scenario.
This is holger's unique capability: the network-free signed
holger.update-bundle/v1format with a two-person approval + release-signature trust model and a fully offline verifier. Artifactory and Nexus have no equivalent, so — unlike thenexus/artifactoryhead-to-heads — this bench measures holger against itself: it drives the REAL [holger_update_bundle] lifecycle and reports the throughput of each stage.Lifecycle (the live tooling path —
holger-bundle/holger-verify/ the offline server import):prepare → approve ×2 (distinct signers) → finalize (release signature) → [cross airgap] → verify (offline) → anti-rollback → import (atomic)Four arms, each a serial measurement (one bundle is an inherently sequential unit of work — no rayon/thread::spawn pool is used, exactly like the single-writer
auditbench):
- [
bundle_prepare_throughput] — MB/s of artifact bytes packed into a candidate bundle. This is the znippy path.- [
bundle_sign_finalize_throughput] — bundles/sec through approve×2 + finalize (ed25519 detached approval signatures + the ed25519 release signature over the manifest, signer identity = X.509).- [
bundle_verify_throughput] — MB/s of the offline verifier, the protected-side gate that runs before anything touches the data root.- [
bundle_import_throughput] — MB/s of atomic import (verify-before-extract into a fresh tree via staging-dir + rename) into the protected data root, plus the anti-rollback accept.Delta vs whole-payload (the znippy question)
bundle_preparepacks the whole payload — it does NOT delta/patch against a prior bundle.prepare::Preparation::buildinserts each payload's full bytes as a member (update-bundle/src/prepare.rs:82), andcontainer::pack_bundlestages every member and callsznippy_compress::compress_dir(&staging, …)(`update-bundle/src/container.rs …
fn bundle_prepare_throughput(scale: &Scale) -> Result<Throughput> · src/secure_update.rs:271
bundle_prepare — MB/s of artifact bytes packed into a candidate bundle (the znippy path). Each iteration builds a candidate from a fresh firmware image and packs it to a .holger.znippy archive on disk. mb_per_sec is source artifact MB/s; ops_sec is candidates/sec.
ANSWER floor: the packed archive round-trips through load_bundle, the payload member's SHA-256 matches the source bytes, and it is stored whole (the packed member's uncompressed size equals the source size — proof this is a whole-pack, not a delta).
fn bundle_sign_finalize_throughput(scale: &Scale) -> Result<Throughput> · src/secure_update.rs:344
bundle_sign_finalize — bundles/sec through approve×2 (two distinct ed25519 signers) + finalize (the ed25519 release signature over the release manifest, signer identity X.509). A small artifact isolates the signing cost from payload hashing. ops_sec is finalized-bundles/sec.
ANSWER floor: finalize returns a VerifyReport with exactly the two required approvals and a release signer distinct from both approvers (finalize also self-verifies, so a bundle that would not verify is never emitted).
fn bundle_verify_throughput(scale: &Scale) -> Result<Throughput> · src/secure_update.rs:450
bundle_verify — MB/s of the offline verifier (the protected-side gate). Builds one finalized bundle, then loops verify_bundle_file (fully offline: digest re-check of every covered member + the ed25519 release signature). mb_per_sec is artifact MB/s verified; ops_sec is verifications/sec.
ANSWER floor: every verification accepts the honest bundle with exactly 2 approvals, AND a single-byte-tampered copy is rejected.
fn bundle_import_throughput(scale: &Scale) -> Result<Throughput> · src/secure_update.rs:509
bundle_import — MB/s of atomic import into the protected data root. Each iteration verifies-then-extracts the bundle into a fresh tree (staging dir + atomic rename — no partial tree ever visible) and records the accepted sequence in the anti-rollback store. mb_per_sec is artifact MB/s imported; ops_sec is imports/sec.
ANSWER floor: the extracted payload/firmware.bin bytes equal the source, AND the anti-rollback state rejects a replay (same sequence, different bytes) and a downgrade (lower sequence) while accepting an exact idempotent re-import.
Module holger_bench_scenarios::served
Served-over-HTTP driver — the honest comparison foundation.
Why this module exists (årsstädning 2026-06-28 fairness fix, part 2)
The Nexus / Artifactory comparison scenarios used to measure Holger in-process (
repo.get_file()— no syscall, no HTTP framing, no TLS) while the JVM competitors were measured over HTTP. That is not a comparison. The rule from the audit: when you measure Holger's package delivery you MUST go via the package protocol — Holger's real native HTTP door ([RepositoryBackendTrait::handle_http2_request], the very path stock cargo/pip/npm hit, served by [start_http_gateway]).This module stands up a real Holger HTTP server (the same [
start_http_gateway] the productionholger serve/dev-pairpath uses), then drives a pure-Rust, gatling-style parallel GET pool ([bombard_http_get]) against it — the exact same over-the-wire workload the competitors get. The result (holger_served_mbs) is the like-for-like head-to-head number; the old in-process figure is kept only as an internal "Holger-vs-Holger regression ceiling" ([inprocess_read_ceiling]), never placed beside the competitor.Reuse note — this is the
holger-dsfoundationThe publish + fetch-over-protocol pieces here are deliberately standalone, callable functions (not buried in one scenario): the upcoming
holger-dstool drives Holger's served protocol the same way. Keep [start_holger_gateway] / [bombard_http_get] / [crate_download_urls] generic and reusable.
fn capture_container_logs(container: &ContainerAsync<GenericImage>, label: &str) · src/served.rs:54
Perfect instrumentation for the JVM competitor containers (Nexus, Artifactory): stream the container's combined stdout+stderr to a host log file in real time, so startup/crash detail (e.g. Artifactory's master.key / join.key / Access bootstrap) is fully visible instead of an opaque "skipped". Best-effort — capture failures never affect the bench. Writes $HOLGER_BENCH_DIR/<label>-container.log (falls back to /tmp). Call right after image.start().
struct HttpFetchOutcome { ops: u64, bytes: u64, mbs: f64, ops_per_sec: f64, ok_ops: u64, p50_ms: f64, p99_ms: f64, p999_ms: f64, mean_ms: f64 } · src/served.rs:79
Aggregate outcome of one over-the-wire GET bombardment.
async fn start_holger_gateway(repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)>,) -> Result<SocketAddr> · src/served.rs:128
Stand up a real Holger native-HTTP server for repos and return the bound loopback address (e.g. 127.0.0.1:54321). This is the same [start_http_gateway] the production serve / dev-pair path uses — cleartext loopback (the competitors also run cleartext HTTP on loopback, so the transport is symmetric), open auth, no-op audit, 1 GiB body cap.
Must be called from within a Tokio runtime (the gateway tokio::spawns its accept loop). Polls GET /healthz until the listener answers (or a short timeout elapses) so callers can fetch immediately on return.
repos is (route_name, backend): the gateway routes /{route_name}/… to that backend's handle_http2_request, exactly as cargo/pip/npm reach it.
IMPORTANT: drive this from a multi-thread Tokio runtime (e.g. Builder::new_multi_thread). The companion [bombard_http_get] pool blocks OS threads; on a single-thread runtime that blocking would starve this gateway's accept loop and no request would ever be served.
fn crate_download_urls(base: &str, repo: &str, files: &[String]) -> Vec<String> · src/served.rs:167
Build the cargo crate-download URLs (/{repo}/crates/{name}/{ver}/download, the real sparse-protocol fetch path) for every {name}-{ver}.crate file in files. Files that don't parse as name-version.crate are skipped.
fn pip_download_urls(base: &str, repo: &str, files: &[String]) -> Vec<String> · src/served.rs:185
Build the wheel/sdist download URLs (/{repo}/packages/{name}/{filename}, the real PEP 503 fetch path pip/uv hit after resolving the index) for every packages/{name}/{filename} entry in files. Non-packages/ entries are skipped. Mirrors [crate_download_urls] for the Python journey.
fn pip_index_urls(base: &str, repo: &str, files: &[String]) -> Vec<String> · src/served.rs:196
Build the PEP 503 simple detail-page URLs (/{repo}/simple/{name}/, one per unique package) from packages/{name}/… entries — the index-resolve step pip/uv do before downloading. Deduplicated, insertion order irrelevant.
fn bombard_http_get(urls: &[String], concurrency: usize, dur: Duration) -> HttpFetchOutcome · src/served.rs:223
Pure-Rust, gatling-style parallel GET pool (no curl/wrk shelling): spawn concurrency worker threads, each holding a keep-alive reqwest::blocking client and looping random GETs over urls until dur elapses, summing bytes actually read off the wire. This is the symmetric over-the-wire workload used against all three systems (Holger, Nexus, Artifactory).
Unauthenticated variant — for targets (Holger open-auth loopback, Nexus anonymous-read raw repos) that serve GETs without credentials. When a target requires auth on read (Artifactory OSS returns 401 to anonymous GETs), use [bombard_http_get_auth] so the head-to-head transport actually reaches the bytes instead of measuring a wall of 401s as ~0 MB/s.
fn bombard_http_get_auth(urls: &[String], auth: Option<(& str, &str)>, concurrency: usize, dur: Duration,) -> HttpFetchOutcome · src/served.rs:233
As [bombard_http_get], but each GET carries HTTP Basic credentials when auth is Some((user, pass)). Artifactory OSS rejects anonymous reads (401), so its leg of the head-to-head must authenticate; Holger (open auth on the loopback bench) and Nexus (anonymous raw read) pass None. Basic auth is a constant per-request header — the like-for-like transport (concurrent HTTP GET over loopback) is unchanged.
async fn run_crate_fetch(archive: &Path, repo: &str, target: Option<& str>, concurrency: usize, dur: Duration,) -> Result<(usize, HttpFetchOutcome)> · src/served.rs:348
The holger-ds artifact-delivery run: open a synthetic crate archive, expose it over Holger's native HTTP door under route repo (or, when target is Some(base_url), drive an already-running Holger at that base instead of standing one up), then bombard the crate-download path with the gatling pool — measured over the package protocol, never in-process. Returns (file_count, outcome).
Pure reuse: [start_holger_gateway] + [crate_download_urls] + [bombard_http_get] — the exact same over-the-wire path as the competitor head-to-head. A target is expected to serve the matching /{repo} corpus.
Must run inside a multi-thread Tokio runtime (see [start_holger_gateway]).
struct PipServeOutcome { files: usize, packages: usize, wheel: HttpFetchOutcome, index: HttpFetchOutcome, inprocess_ceiling_mbs: f64 } · src/served.rs:394
Aggregate outcome of the holger-ds Python-index journey: the wheel download head-to-head (headline MB/s + ops/s), the PEP 503 index-resolve arm (ops/s — the /simple/{name}/ step, which also computes+caches the SHA-256 pip/uv verify), and the internal in-process read ceiling.
async fn run_pip_index_fetch(archive: &Path, repo: &str, target: Option<& str>, concurrency: usize, dur: Duration,) -> Result<PipServeOutcome> · src/served.rs:415
The holger-ds Python journey run: expose a synthetic wheel corpus over Holger's PEP 503 / PEP 691 simple index (the very door uv/pip hit), then bombard both the wheel-download path and the index-resolve path with the gatling pool — measured over the package protocol, never in-process. The Python sibling of [run_crate_fetch]: same [start_holger_gateway] + [bombard_http_get], a [PipRepoZnippy] backend instead of a crate one.
When target is Some(base_url) an already-running Holger is driven instead of standing one up (it must serve the matching /{repo} corpus). Must run inside a multi-thread Tokio runtime (see [start_holger_gateway]).
fn inprocess_read_ceiling<F>(concurrency: usize, dur: Duration, get: F) -> f64 where F: Fn(u64, usize) -> usize + Send + Sync, · src/served.rs:487
Internal "Holger-vs-Holger regression ceiling" only — NOT a competitor comparison. Concurrent in-process get_file() over files, no HTTP/TLS, returning MB/s. This is the theoretical read ceiling of the znippy archive path; it is reported as holger_inprocess_read_ceiling_mbs, kept apart from the over-the-wire holger_served_mbs head-to-head so a demo can never show it beside the competitor numbers.
Crate holger-build
Holger's MVP/dev/micro build-thing: the 3 tiers × 3 roles (agent · UI · server) matrix as a VIEW over holger's declared build-space, a comparison matrix a UI can render, and the invertible feature↔crate dependency view (the Galois connection: features_pulling / drop_crate). A build-thing CONSUMER, like knut-build — it path-deps nornir-build-thing for the reusable core and reuses holger-build-space's declared BuildVariantSet rather than re-declaring it.
Holger's build-space, viewed as three tiers × three roles (Rickard's matrix, refined 2026-07-23). Same base packages per role — holger, holger-server-ui on gui-core, holger-agent — the tiers differ ONLY by cargo features. Nothing is forked, nothing deleted.
| agent (CLI) | UI | server | |
| micro | intake-transfer, no_default_features; CAN verify a znippy M-of-N signature (a lightweight gatekeeper) |
gui-core + mikro — 0 helix, 0 image, 0 online (~20 MB) |
mini, ReadOnly, zero-egress |
| mvp | + strict, provenance receipts, holger-bundle/holger-verify |
gui-mini + about-card, bundle-status + Critical panes |
mini + Critical + meta-door + secure_update + modgunn content-verdict |
| dev | + online (reqwest/ureq → Nexus/Maven/PyPI) |
gui-embedded — helix, Graph3D, scan, bench, demo (~65 MB) |
full — federation, OIDC/OAuth2 + modgunn behaviour-diff |
Modelled on knut/crates/knut-build: it path-deps [nornir_build_thing] for the reusable core (BuildVariant / BuildVariantSet / Role / ServiceShape) and it reuses holger's ALREADY-declared build-space ([holger_build_space::build_variant_set], the 8 buildable variants with their drift / shape / airgap tests). The matrix here is a regroup of those into the 3×3, not a second manifest — so the live cells can never drift from what actually builds, and there is no parallel BuildVariant type invented alongside the real one.
- signature / integrity — znippy
verify_threshold(M-of-N detached-CMS, integrity + provenance, no modgunn, no online). A * …
Module holger_build
enum Tier { Micro, Mvp, Dev } · src/lib.rs:151
The tier axis — the deployment ambition, expressed only in cargo features.
enum MatrixRole { Agent, Ui, Server } · src/lib.rs:180
The role axis of the matrix — a REFINEMENT of [nornir_build_thing::plugin::Role], not a duplicate of it. holger's coarse role axis is server / client; the matrix splits client into the two things a human and a script drive: the egui browser (Ui) and the transfer CLI (Agent). [MatrixRole::of_variant] derives it from the reused declaration so the split cannot drift.
enum Liveness { Live } · src/lib.rs:230
Whether a cell can build on main today.
enum Gatekeeper { None, Signature, ContentVerdict } · src/lib.rs:242
The two gatekeeper LAYERS, kept distinct on purpose.
enum SizeClass { Small, Medium, Large } · src/lib.rs:268
Approximate size class — the "keep the size lessons" axis. Grounded in the release-binary bytes measured in build-variants.toml where a variant is a UI tier, qualitative otherwise.
struct Cell { tier: Tier, role: MatrixRole, variant: Option<String>, package: Option<String>, features: Vec<String>, crates_total: usize, crates_pulled: Vec<String>, size_class: SizeClass, gatekeeper: Gatekeeper, liveness: Liveness, capability: String, pending_note: Option<String> } · src/lib.rs:294
One cell of the 3×3 matrix: what building this (tier, role) produces, derived FROM the declaration so a UI can render it and it cannot drift from the manifests.
struct VariantMatrix { cells: Vec<Cell> } · src/lib.rs:336
The whole 3×3 comparison matrix.
struct HolgerBuild · src/lib.rs:405
holger's MVP/dev/micro build-thing.
fn capability_of(feature: &str) -> &'static str · src/lib.rs:743
One line on what capability a feature buys — what [HolgerDropPlan] reports as lost. Hand-written prose, deliberately: the resolver knows that turning off signin drops 90 crates, and cannot know that what the user loses is the OIDC device-code tab.
Every key is held to the manifests by capability_lines_name_real_features, so a feature that is renamed or removed breaks the build instead of quietly describing nothing.
struct HolgerDropPlan { crate_name: String, removable_from: Vec<RootDropPlan>, unconditional_in: Vec<String>, absent_from: Vec<String> } · src/lib.rs:776
The drop plan across every matrix root. One crate can be unconditional for one root, conditional for another and absent from a third — modgunn is exactly that — so a single global verdict would be wrong for two of the three.
struct RootDropPlan { package: String, crate_id: String, features_to_disable: BTreeSet<String>, capability_lost: Vec<String>, alternatives: Vec<BTreeSet<String>>, causes_complete: bool, and_group_causes: Vec<FeatureCause>, verified: bool, note: String } · src/lib.rs:849
A drop plan for one crate version as seen from one root package.
fn matrix_root_packages(set: &BuildVariantSet) -> BTreeSet<String> · src/lib.rs:872
The root packages the declared build-space actually builds — derived from the reused declaration, so MATRIX_ROOTS can never quietly go stale.
fn declared_cell_features(set: &BuildVariantSet) -> BTreeSet<String> · src/lib.rs:883
Every feature any declared matrix cell enables — the set capability_of is gated against, plus the composite features a cell names.
Module holger_build::Cell
fn coord(&self) -> String · src/lib.rs:329
micro/server, dev/ui, … — the cell's coordinate as a stable label.
Module holger_build::Gatekeeper
fn as_str(self) -> &'static str · src/lib.rs:254
Module holger_build::HolgerBuild
fn declared(&self) -> anyhow::Result<BuildVariantSet> · src/lib.rs:409
The reused declaration — holger's 8 buildable variants, NOT re-declared here.
fn compare(&self) -> anyhow::Result<VariantMatrix> · src/lib.rs:422
Produce the 3×3 comparison matrix FROM the declaration. Live cells pull their features verbatim from the reused [BuildVariantSet]; pending cells are marked honestly. A UI renders this; it can never drift from the manifests because the live half is read straight out of holger-build-space and the crate columns are RESOLVED from the manifests rather than looked up in a table.
Errors if the feature axis cannot be derived — a matrix with silently empty crate columns is worse than no matrix (LAW 2).
fn features_pulling(&self, crate_name: &str) -> anyhow::Result<Vec<CrateOnRoot>> · src/lib.rs:463
Adjoint 1 — the preimage, DERIVED. Every minimal conjunction of root features that brings crate_name into the build, per matrix root package.
This replaced a FEATURE_PULLS lookup that returned one flat list of feature names for every package at once. The answer genuinely differs by root — reqwest is pulled by eight holger-server-ui features and four holger ones — so the root is now part of the answer.
A name no locked package carries is an [AxisError::UnknownCrate], never an empty list.
fn drop_crate(&self, crate_name: &str) -> anyhow::Result<HolgerDropPlan> · src/lib.rs:475
Adjoint 2 — the drop plan, DERIVED and VERIFIED. The smallest set of features to turn off so crate_name leaves each root's build, with the capability each disabled feature costs.
The plan is a minimum-cardinality hitting set of the minimal causes, and verified is true only when c ∉ F(Φ \ R) was actually re-resolved. Where the crate is Unconditional or Never for a root, that is said plainly instead of promising a saving.
fn feature_axis(&self) -> anyhow::Result<& 'static FeatureAxis> · src/lib.rs:483
The derived feature axis itself, for a caller that wants to ask a question this crate does not wrap.
Module holger_build::HolgerDropPlan
fn removable(&self) -> bool · src/lib.rs:788
Whether ANY root can lose this crate by turning features off.
fn all_verified(&self) -> bool · src/lib.rs:792
Whether every plan on offer was confirmed by re-resolving.
Module holger_build::MatrixRole
fn as_str(self) -> &'static str · src/lib.rs:191
fn of_variant(v: &nornir_build_thing::variant::BuildVariant) -> Option<MatrixRole> · src/lib.rs:205
Derive the matrix role of a declared build-space variant from its coarse [Role] + package. Server → server; a Client is Agent iff its package is holger-agent-cli, else Ui. Returns None for a role the matrix does not model (library / tool). This is the ONE place the coarse→fine mapping lives, and matrix_cells_reference_declared_live_variants holds it to the declaration.
Module holger_build::SizeClass
fn as_str(self) -> &'static str · src/lib.rs:278
Module holger_build::Tier
fn as_str(self) -> &'static str · src/lib.rs:163
Module holger_build::VariantMatrix
fn cell(&self, tier: Tier, role: MatrixRole) -> &Cell · src/lib.rs:342
The cell at (tier, role). Every coordinate is present (9 cells).
fn live(&self) -> Vec<& Cell> · src/lib.rs:349
fn pending(&self) -> Vec<& Cell> · src/lib.rs:353
fn state_json(&self) -> serde_json::Value · src/lib.rs:358
The whole surface as JSON a viz pane / MCP tool reads.
fn to_markdown(&self) -> String · src/lib.rs:371
A Markdown table a docs page or a UI can render directly.
Module holger_build::feature_axis
The feature→crate graph, DERIVED from holger's real manifests.
This module is what replaced the hand-written
FEATURE_PULLS/FEATURE_EXPANDS/UNCONDITIONALtables. Those were deleted, not softened. Measured againstcargo tree -p <pkg> -e no-dev --target all, 2026-08-01, the hand table was wrong in five independent ways at once, and one of them was unfixable by editing it:
the table said the manifests say facett-model3dis linked unconditionallyno package of that name exists anywhere in the lock facett-helix/imageare unconditionalboth are Conditional— dropping the UI features removes themsigninpullsbase64base64 0.22.1isUnconditionalinholger-server-ui; droppingsignincannot remove itwarehouse-scanpullsicebergicebergis not in holger-server-ui's closure at all, under any featureserver-lib,agent-lib,bench-scenarios,world-connector,znippy-rust-reponone are package names ( holger-server-lib,holger-agent-lib, …)and the structural one: the table had no package axis.
reqwestis pulled by{bench, demo, gui, gui-embedded, robot-remote, signin, tools, warehouse-scan}inholger-server-uiand by{bench, default, full, online}inholger. One flat list cannot be right for both, so it was right for neither. Every answer here is stated against a named root package.What is derived and what is not
Everything about the graph is derived. The only hand-written thing left is [
crate::capability_of] — one line of English per feature, which no resolver can produce — and it is GATED:capability_lines_name_real_featuresfails if a key is not a feature some matrix root actually declares.Failing loudly
A query that resolves nothing must never look like "nothing pulls this". Every entry point returns [
anyhow::Result] and refuses:
- no workspace root / no
Cargo.lock→ [AxisError::NoWorkspace]- a lock …
const MATRIX_ROOTS: & [& str] · src/feature_axis.rs:57
The root packages every derived answer is stated against — the packages the 3×3 matrix's cells actually build. Derived from the reused build-space declaration by [crate::matrix_root_packages]; this const is the fallback the gate matrix_roots_are_the_declared_packages holds it to.
enum AxisError { NoWorkspace, EmptyUniverse, NoRootsLoaded, NoSuchRoot, AmbiguousRoot, UnknownCrate, TruncatedClosure, UndeclaredFeature, Parse } · src/feature_axis.rs:63
Everything this module refuses to answer, and why. Each variant exists because the alternative was a confident empty set.
struct FeatureCause { package: String, features: BTreeSet<String> } · src/feature_axis.rs:147
One minimal reason a crate is in the build: a conjunction of root features of one root package. A features set of size 1 is the ordinary case; size ≥ 2 is an AND-group (a weak pkg?/feat edge) — measured 0 of 728 for holger, see the module doc of [crate].
enum RootVerdict { Unconditional, Never, Conditional } · src/feature_axis.rs:163
What one root package can say about one crate.
struct FeatureAxis · src/feature_axis.rs:187
The derived world. Built once from Cargo.lock plus every manifest the lock's local packages live in, and shared by every query so two answers can never come from two parses (LAW 5).
struct CrateOnRoot { package: String, crate_name: String, versions: Vec<CrateVersionVerdict> } · src/feature_axis.rs:459
One crate, seen from one root package: one entry per locked version.
struct CrateVersionVerdict { id: String, verdict: RootVerdict, causes: Vec<FeatureCause>, causes_complete: bool, removable: bool, features_to_disable: BTreeSet<String>, alternatives: Vec<BTreeSet<String>>, verified: bool, note: String } · src/feature_axis.rs:482
The verdict for one exact locked version.
fn discriminating(subject: &BTreeSet<String>, siblings: &[BTreeSet<String>]) -> Vec<String> · src/feature_axis.rs:607
The crates that are in subject but not in EVERY set of siblings — what this build links that its peers do not. Used to give a cell a small, DERIVED signature set instead of the 359-crate full closure or a hand-written list.
fn shared() -> Result<& 'static FeatureAxis, AxisError> · src/feature_axis.rs:623
A shared axis, so the nine cells of one compare() are nine views of ONE parse. Rebuilt on failure — a failed discovery is never cached as "empty".
Module holger_build::feature_axis::CrateOnRoot
fn present_anywhere(&self) -> bool · src/feature_axis.rs:467
Whether any locked version of this crate is in this root's build at all.
fn causes(&self) -> Vec<& FeatureCause> · src/feature_axis.rs:471
Every cause across every version — the flat preimage a UI lists.
fn and_groups(&self) -> Vec<& FeatureCause> · src/feature_axis.rs:475
The causes that are conjunctions — what a naive UI would lie about.
Module holger_build::feature_axis::FeatureAxis
fn for_root(root: &Path) -> Result<Self, AxisError> · src/feature_axis.rs:198
Build the axis for a specific holger checkout.
fn discover() -> Result<Self, AxisError> · src/feature_axis.rs:240
Find holger's checkout and build the axis.
HOLGER_REPO_ROOT wins; otherwise walk up from this crate's source directory (baked in at compile time) and then from the current directory, looking for a Cargo.lock. Failing to find one is an ERROR, never a silently empty graph.
fn root(&self) -> &Path · src/feature_axis.rs:260
The checkout this axis was derived from.
fn scanned_repos(&self) -> &[PathBuf] · src/feature_axis.rs:264
Every repo root scanned for first-party manifests.
fn package_count(&self) -> usize · src/feature_axis.rs:268
How many packages the derived world holds.
fn features_of(&self, package: &str) -> Result<BTreeSet<String>, AxisError> · src/feature_axis.rs:285
Every feature package declares, plus the implicit optional-dep ones — the real [features] table, which is what the deleted FEATURE_EXPANDS was a stale copy of.
fn crates_pulled_by(&self, package: &str, features: &[String], default_features: bool,) -> Result<BTreeSet<String>, AxisError> · src/feature_axis.rs:294
F(S) — the packages package links when built with exactly these root features. Mirrors cargo tree -p <package> -e no-dev --target all --no-default-features [--features …].
Errors rather than truncating: an unresolvable closure is not a small one.
fn crate_ids_pulled_by(&self, package: &str, features: &[String], default_features: bool,) -> Result<BTreeSet<String>, AxisError> · src/feature_axis.rs:331
The same closure as [Self::crates_pulled_by] but as "name version" IDS.
A caller checking that a specific crate LEFT the build must compare ids: reqwest is locked at 0.12.28 and 0.13.4 in holger-server-ui, so "the name reqwest is still present" and "this reqwest is still present" are different questions, and only the second one is the drop plan's claim.
fn analyse(&self, package: &str, crate_name: &str,) -> Result<CrateOnRoot, AxisError> · src/feature_axis.rs:377
The upper adjoint, derived. Every minimal conjunction of root features that brings crate_name into package's build.
An empty causes with verdict Unconditional means "always there"; with Never it means "never there". The two are never collapsed into a bare empty list, which is what the old table did.
fn analyse_all_roots(&self, crate_name: &str) -> Result<Vec<CrateOnRoot>, AxisError> · src/feature_axis.rs:416
The verdicts for crate_name across every matrix root, because the answer genuinely differs per package and a single flat answer is the bug the deleted table had.
fn matrix_roots(&self) -> Result<Vec<String>, AxisError> · src/feature_axis.rs:426
The matrix root packages present in this universe, in a stable order.
fn crates_lost_by_disabling(&self, package: &str, feature: &str,) -> Result<BTreeSet<String>, AxisError> · src/feature_axis.rs:438
The other direction of the same F: the crates that leave package's build when feature is turned off and everything else stays on. Computed from the same resolver, not from a second table that has to agree.
Module holger_build::feature_axis::FeatureCause
fn is_and_group(&self) -> bool · src/feature_axis.rs:155
Crate holger-build-space
[dev-tooling] build-variants.toml + tier link-audits (auth_ship_small: webauthn/openssl must NOT link).
Holger's build-space, declared as data — the first consumer of nornir's build-thing (task #36). Holger ships ONE source tree built three ways, each producing its OWN distinctly-named EXECUTABLE (the deployment tiers in .nornir/holger-tiers.md):
- embedded — the tiniest airgap/edge executable, bin
holger-embedded: the in-process engine driven as a standalone binary (embeddedmode, no federation), provenance-verify on via the realsignfeature. - mini — the stripped, read-only standalone server for airgap/red-zone, bin
holger-mikro(minimode: minimal serve core, no federation, no Skidbladnir/cloud client linked). - full — the writable everything-on server, bin
holger-full(fullmode: the ONLY tier that compiles in the upstream-registry federation — the Skidbladnir ship — plus proxy / import + the full RON config graph).
All three build the real holger crate; the mode features are mutually exclusive (exclusive) and each bin declares its required-features, so a wrong combo rejects up front. Each tier is a typed, reflectable (package × bin × features × profile × target) point in a [BuildVariantSet], grounded in holger's ACTUAL workspace crates and Cargo features (embedded, mini, full, sign, testmatrix, served, gui) — no hardcoded UI, no invented features. The SAME data ships on disk as .nornir/build-variants.toml (which nornir build discovers to drive the Build tab / CLI); a byte-for-byte crate-local mirror (build-variants.toml) is embedded here via include_str! so the bytes travel inside the published crate, with a drift-guard test keeping the two copies identical in-workspace.
The SAME file ALSO carries the two CLIENT variants — what a human and a script drive AGAINST a holger:
- ui — the egui registry/artifact browser (
server/ui), binholger-server-ui. Its bin isrequired-features = ["gui"], which `cargo build --all-target …
Module holger_build_space
const REPO: & str · src/lib.rs:61
The repo label stamped into the surface + provenance records.
const BUILD_VARIANTS_TOML: & str · src/lib.rs:70
Holger's build-space as data — embedded from the crate-local build-variants.toml so the bytes ship INSIDE the published .crate tarball (an include_str! of the repo's ../../.nornir/build-variants.toml would read OUTSIDE the package dir and break the isolated publish/verify build). The crate-local copy is a byte-for-byte mirror of the on-disk .nornir/build-variants.toml that nornir build discovers; the embedded_matches_on_disk_nornir_copy test guards against drift in-workspace.
fn build_variant_set() -> Result<BuildVariantSet> · src/lib.rs:76
Parse holger's declared build-space into a typed [BuildVariantSet].
This is the descriptor a consumer (the nornir Build tab, nornir build) reflects into choosable rows.
fn build_surface() -> Result<BuildSurface> · src/lib.rs:83
Project holger's build-space onto the data-driven [BuildSurface] a UI renders (one row per tier, each with its materialized cargo command, validity, and a Build action).
fn press_tier(name: &str, repo_root: &Path, outcome: Outcome, artifacts: &[PathBuf],) -> Result<Attestation> · src/lib.rs:111
Press the build-thing button for one declared tier: validate the tier against holger's build-space, then capture a content-addressed build-provenance [Attestation] describing what building it produced.
This is holger consuming nornir-build-thing's provenance path. It is the dep-light describe/attest half — it does not itself shell out to cargo; the caller passes the artifacts a build already emitted (their paths under repo_root) and whether that build succeeded. The record binds the exact tier descriptor (verbatim), the materialized cargo argv + resolved feature/ profile/target axes, the VCS rev + working-tree freshness of repo_root, the detected [Toolchain], the [HostInfo] (cores + memory), the outcome, and the SHA-256 of every artifact — then folds it to a stable digest.
The heavy end-to-end path (dispatch on the tier's kind, spawn cargo build, hash the produced executables, attest + emit build-info lineage in one shot) is [assemble_tier] / [build_tier] over [nornir_build_thing::assemble], which the nornir Build tab and nornir build --variant <tier> also drive. press_tier is the seam a test or an offline tool uses to attest an artifact built out of band.
The returned attestation is unsealed — is_sealed() is false. holger never fabricates a signature; the real seal is znippy's detached-CMS signer, applied via [Attestation::seal_with] behind nornir's build-provenance feature.
fn assemble_tier(name: &str, repo_root: &Path, exec_dir: &Path, info: &dyn BuildInfoSink, builders: &dyn Assemble,) -> Result<Assembled> · src/lib.rs:159
Press the build-thing button END TO END through the unified assemble() seam (nornir-build-thing design §f7) for one declared tier.
This REPLACES the former hand-rolled build_and_press (which shelled the per-tier cargo argv itself via build_variant). A chosen tier now flows through nornir's ONE polymorphic entry point, [nornir_build_thing::assemble]: it validates the tier, dispatches on its ArtifactKind — embedded is an exe, mini/full are service kinds, and both lower to the SAME cargo builder — actually builds it, content-hashes the produced artifact(s), and emits the per-tier build-info lineage (provenance / dep / artifact rows, each artifact row carrying its content_id) to info. It returns ONE content-addressed [Assembled] receipt binding the produced output, the proof, and that lineage. So holger's three deployment tiers ride the exact same dispatch (and future container / vm kinds) as every other suite member — no bespoke per-tier build loop.
Where [press_tier] attests a PRE-BUILT artifact the caller supplies (the dep-light describe/attest seam), assemble_tier is the real "press the button": build THEN attest, in one shot. builders is the injected builder seam — the production [Builders::new] for the real cargo-only path, or a fake in tests; exec_dir is where the built binary / service lands (the BinaryArtifact / ServiceSpec install path). Unknown tiers reject UP FRONT (via [BuildVariantSet::require]) before any build, with the same tier-listing message [press_tier] gives.
The receipt's proof is unsealed — holger never fabricates a signature.
fn build_tier(name: &str, repo_root: &Path) -> Result<(Assembled, Vec<BuildInfoRow>)> · src/lib.rs:183
The real cargo-only [assemble_tier]: build the tier through [nornir_build_thing::assemble] with the production [Builders] (the cargo builder; no image / vm), landing binaries under <repo_root>/target/release, and collect the per-tier build-info lineage in-process. Returns the [Assembled] receipt AND the captured [BuildInfoRow] lineage.
This is what cargo run -p xtask build --variant <tier> drives — holger's first-party equivalent of nornir build --variant <tier>. It is heavy and opt-in: assemble spawns cargo, so a release tier build can take minutes.
Crate holger-embed
The <guest>-embed adapter for holger: hosts holger's registry-browser panel (facett-holger) inside ANOTHER app as a native Box<dyn Facet> (backend (2) — native, no wasm), fed either in-process through holger's functional read API or from a caller-supplied model (the thin-gRPC insertion point). The reusable generalization of nornir's bespoke in-process holger tab.
holger-embed — the reusable <guest>-embed adapter that hosts holger's registry-browser panel inside another app.
This is the first concrete instance of the constellation-wide embed adapter shape (consolidation design §2, native panels; wasm-ui design §3, backend (2)):
a
<guest>-embedadapter crate — links the guest'sfacett-<guest>component crate + a data source, and exposesfn panel(source) -> Box<dyn Panel>: the host app drops the returned panel into a tab. No wasm — the guest'suiruns natively in the host's egui frame at full perf.
It generalizes nornir's bespoke in-process holger tab (nornir/src/holger_embed.rs's read_registry → its own render) into a reusable crate any host links:
- [
read_model] reads holger's on-disk dev-pair registry through holger's functional (in-process) API — no server, no socket — into a backend-agnostic [RegistryModel]; - [
panel] returns [facett_holger::RegistryBrowser] as aBox<dyn Facet>fed from a [Source] (in-process read, a caller-supplied model, or the sample fixture).facett_core::Facet(title/ui/state_json) is the constellation's currentPanelcontract; when the design's dedicatedPaneltrait lands the return type upgrades with no call-site change.
- want holger's PANE (Case A) → link
facett-holger+ feed it data. That is exactly what [panel] does; the data rides either the in-process read ([Source::InProcess], fat mode) or a caller-supplied [Source::Model] assembled from a thin gRPC read (the gRPC insertion point — the adapter stays transport-agnostic so a host with only a socket to holger still uses the same panel). - want holger's CAPABILITY only (Case B) → call holger's API directly and render your own pane; this crate is not needed.
Module holger_embed
enum Source { InProcess, Model, Fixture } · src/lib.rs:45
Where the panel's data comes from. The adapter is transport-agnostic: the same [RegistryBrowser] renders whether the model was read in-process, over a thin gRPC wire (assembled by the caller into [Source::Model]), or is the fixture.
fn read_model(data_dir: &Path, grpc: &str, http: &str, per_repo_limit: usize) -> Result<RegistryModel> · src/lib.rs:75
Read holger's dev-pair registry under data_dir through its functional (in-process) API into a backend-agnostic [RegistryModel]: write/read the dev-pair RON, instantiate_backends, then enumerate each repo's backing archive via list. No server is started and no socket is bound. The first repo that actually holds artifacts becomes the selected_repo (so the panel opens on non-empty data); its listing populates artifacts.
This is the canonical generalization of nornir/src/holger_embed.rs's read_registry — same functional-API walk, but it yields the reusable component's model instead of a bespoke summary struct.
fn panel(source: Source) -> Result<Box<dyn Facet>> · src/lib.rs:137
The <guest>-embed entry point: build holger's registry-browser panel and return it as a Box<dyn Facet> a host app drops into a tab. The panel's ui runs natively in the host's egui frame — no wasm, full perf.
Crate holger-errcode
holger's FROZEN, append-only holger-<subsystem>-<n> application error codes
holger-errcode — holger's FROZEN, append-only application error codesEvery non-UI error holger emits carries two channels:
- a visible human string (what went wrong, in words), and
- a stable machine code
holger-<subsystem>-<n>.
Both ride in the SAME message, so a code survives every transport holger has — an HTTP error body, a log line, a CLI stderr line, an audit row — without needing a side channel.
The Secure Update Gateway core (update-bundle) is deliberately network-free and self-contained. The registry has to be reachable from there and from server/lib while remaining the ONE place codes are allocated — otherwise "unique globally within the app" cannot be enforced by a single test. So this is a leaf crate with no dependencies at all. Keep it that way.
- FROZEN. A code, once published, never changes meaning. Fix the wording of a [
ErrCode::summary] freely; never re-point a code at a different failure. - APPEND-ONLY. Retire a code by leaving it in [
ALL] and ceasing to emit it. NEVER reuse the number — a retired code in an old audit row must stay unambiguous. - UNIQUE within its subsystem and globally across the app. Enforced by [
tests::codes_are_unique], which FAILS on a repeat. - Consumers may prepend a mount prefix; [
parse] finds the bracketed code anywhere in the string. - Every row declares whether it is [
Wiring::Wired] (raised at a real error site in [ErrCode::src_file] TODAY) or [Wiring::Reserved] (the number is claimed and frozen, the site is not coded yet). That claim is guarded byerrcode/tests/wiring_guard.rs, which reads the real holger source tree: awiredrow nothing raises FAILS, and areservedrow that IS raised FAILS.
A registry that merely LISTS codes will happily claim coverage it does not have. holger shipped exactly that: for four sessions t …
Module holger_errcode
struct ErrCode { code: & 'static str, subsystem: & 'static str, n: u16, summary: & 'static str, src_file: & 'static str, wiring: Wiring } · src/lib.rs:61
One frozen entry in holger's error-code registry.
code is the wire identity (holger-<subsystem>-<n>); summary is the human half of the dual channel — a short description of the failure CLASS (the operand goes in the per-site detail passed to [ErrCode::tag]).
enum Wiring { Wired, Reserved } · src/lib.rs:85
Whether a registered code is actually RAISED in holger today, or merely reserved.
The distinction is guarded (errcode/tests/wiring_guard.rs): a Wired row whose [ErrCode::src_file] does not reference it FAILS, and a Reserved row that IS referenced from production code FAILS too. Without that second direction the split would be decoration — a registry can always claim to be wired.
macro_rules! wiring_kind · src/lib.rs:128
wired / reserved as the leading keyword of a [codes!] row → a [Wiring]. Separate macro so the keyword is a plain token in the registry table and a typo (wried) is a compile error naming the row.
macro_rules! codes · src/lib.rs:143
Declare a frozen code AND enrol it in [ALL] in one place, so a code can never be added to the app without also being added to the uniqueness gate.
Each row reads <wired|reserved> IDENT = (subsystem, n, summary, src_file);. The leading keyword is the WIRING CLAIM and src_file is where that claim is checked — both are data the wiring guard reads, not prose.
fn parse(msg: &str) -> Option<& 'static ErrCode> · src/lib.rs:363
Recover the [ErrCode] from any message [ErrCode::tag] produced — a log line, an HTTP body, an audit row, or one of those with a mount prefix prepended.
Matching is on the bracketed form [holger-sub-n], so holger-update-1 can never be mistaken for holger-update-10.
This is what tests assert on — never a substring of the human prose.
fn by_code(code: &str) -> Option<& 'static ErrCode> · src/lib.rs:368
Look a code up by its exact wire string ("holger-update-22").
const WIRE_CODE_KEY: & str · src/lib.rs:374
The HTTP header / gRPC metadata key the code rides in, in ADDITION to being inside the message.
fn wired_count() -> usize · src/lib.rs:381
How many rows claim [Wiring::Wired] — codes holger raises TODAY.
This is the honest coverage numerator. It is a CLAIM until errcode/tests/wiring_guard.rs checks each row against the real source tree; read the two numbers together, never this one alone.
fn reserved_count() -> usize · src/lib.rs:386
How many rows are [Wiring::Reserved] — numbers claimed and frozen, sites not coded.
Module holger_errcode::ErrCode
fn tag(&self, detail: impl Display) -> String · src/lib.rs:109
Render both channels into one message: [code] detail.
The detail is normally the typed error's own Display (holger's error enums already carry carefully-worded, path-free human strings — this prepends the machine channel without disturbing them).
fn bare(&self) -> String · src/lib.rs:114
The dual-channel message using the registry's own summary as the human half.
Module holger_errcode::Wiring
fn label(&self) -> &'static str · src/lib.rs:95
The label the registry doc uses (wired / reserved).
Crate holger-file-oci-repository
OCI registry backend for holger, disk-backed and WRITABLE — full Distribution Spec v2 push and pull (content-addressed sha256), and holger's target for the modern helm push oci://… flow that replaces the bespoke ChartMuseum API.
Disk-backed, writable OCI registry (Distribution Spec v2).
This is holger's push target for the modern Helm flow (helm push chart.tgz oci://host/{repo} and helm pull oci://…) as well as any OCI artifact. Unlike the bespoke ChartMuseum HTTP API it replaces, the OCI protocol is standardized, content-addressed (sha256, integrity built in), and treats every blob as opaque bytes — so the registry never decompresses or parses an uploaded chart, removing that attack surface entirely.
Routes (all under the endpoint's repository name; the gateway routes /v2/{repo}/… to this backend by the first name segment):
GET /v2/ version probe GET|HEAD /v2/{name}/manifests/{ref} manifest by tag or digest GET|HEAD /v2/{name}/blobs/{digest} blob by digest GET /v2/{name}/tags/list tag listing POST /v2/{name}/blobs/uploads/[?digest=] begin (or monolithic) upload PATCH /v2/{name}/blobs/uploads/{uuid} append a chunk PUT /v2/{name}/blobs/uploads/{uuid}?digest=… finalize a blob PUT /v2/{name}/manifests/{ref} store a manifest
Disk layout (content-addressed; blobs shared across images): {base_dir}/blobs/sha256/{hex} {base_dir}/uploads/{id} {base_dir}/manifests/{image}/tags/{tag} {base_dir}/manifests/{image}/sha256/{hex}
Module holger_file_oci_repository
struct OciRepoFile { name: String, base_dir: PathBuf } · src/lib.rs:39
Module holger_file_oci_repository::OciRepoFile
fn new(name: String, base_dir: PathBuf) -> Result<Self> · src/lib.rs:48
Crate holger-handler-bundle-core
The sealed-.znippy bundle repository machinery shared by holger's airgap package handlers
The sealed-bundle repository machinery, written once.
holger's two airgap handlers — Skidbladnir deploy bundles and Rust-toolchain dev bundles — are the same repository: one sealed .znippy per (name, version), served whole, with a classifier that can say what any path inside it is. Only three things differ between them, so only those three are a [BundleKind]:
- the handler's name,
- whether a filename looks like one of its bundles,
- the member classifier.
Everything else — the store key, the coordinate parse, the HTTP routes, fetch/put/list — lives here and is reached by both. Writing it twice would have been ~90% duplication across two crates (LAW 5), and the two copies would then need a guard to watch them agree; one writer needs none.
This crate is dependency-free apart from [holger_plugin_abi], so it compiles for wasm32-unknown-unknown unchanged and each handler's wasm shim reaches exactly the same code the native backend does.
GET /{repo}/ → newline list of bundle files
GET /{repo}/{name}/{version} → the sealed bundle bytes
GET /{repo}/{name}/{version}/classify/{path…} → what that member IS
PUT /{repo}/{name}/{version} → store a bundle
classify is the route that puts the shared classifier on the wire: it is pure path logic, it needs no index read, and it is what the native/wasm agreement guard compares field by field. A /members route that enumerated a bundle's contents would need to parse the znippy Arrow index — storage, which a sandboxed module has no business doing and which would have had to answer with an empty list on the wasm side. An endpoint that can only ever answer "nothing" is the NOT RUN → true shape; it is deliberately absent.
Module holger_handler_bundle_core
const BUNDLE_EXT: & str · src/lib.rs:47
Extension of a sealed bundle.
const BUNDLE_CONTENT_TYPE: & str · src/lib.rs:50
Content type a sealed bundle is served with.
struct MemberDescription { kind: String, component: String, encrypted: bool } · src/lib.rs:57
What one entry inside a bundle is. Owned strings because the two classifiers disagree on lifetime — Skidbladnir's components are a fixed set (&'static str), a Rust-toolchain bundle's are derived from the path (a crate name, a target triple), so only an owned form covers both.
trait BundleKind · src/lib.rs:76
The three things that differ between one bundle handler and another.
struct BundleHandler · src/lib.rs:96
A bundle repository handler, parameterised by which bundle kind it serves.
fn store_key(id: &WireArtifactId) -> String · src/lib.rs:123
Store key for a coordinate: {name}-{version}.znippy — the sealed name znippy itself writes (tillsynia-20260706.znippy, rust-dev-rhel8-1.97.1.znippy).
The namespace is deliberately ignored: a sealed bundle has no group coordinate, and folding one into the key would silently collide two different ids onto one blob.
fn coordinate_from_key(key: &str) -> Option<WireArtifactId> · src/lib.rs:133
The inverse of [store_key].
Splits at the last -, so a hyphenated bundle name survives: rust-dev-rhel8-1.97.1.znippy → ("rust-dev-rhel8", "1.97.1"), not ("rust", "dev-rhel8-1.97.1"). Returns None for a name with no - or no .znippy rather than inventing a coordinate.
Module holger_handler_bundle_core::BundleHandler
const fn new() -> Self · src/lib.rs:99
Module holger_handler_bundle_core::BundleKind
fn describe_member(path: &str) -> Option<MemberDescription> · src/lib.rs:88
Classify one bundle-relative path. None = not a member of this kind of bundle, which the handler reports as a 404 rather than guessing.
fn is_bundle_name(file_name: &str) -> bool · src/lib.rs:92
Does this bare filename look like one of this kind's sealed bundles? Used to filter a store listing that may hold other files.
Module holger_handler_bundle_core::MemberDescription
fn to_line(&self) -> String · src/lib.rs:70
The single serialization of a member, used by the HTTP route and by the agreement guard. One writer, so the native and wasm surfaces cannot render the same member differently.
Crate holger-handler-rust-toolchain
holger package handler for airgapped Rust-toolchain bundles — the whole logic, shared by the native backend and the wasm module
The airgapped Rust-toolchain bundle package handler for holger.
A rust-dev-rhel8-<ver>.znippy holds a standalone rustup-dist toolchain, a cargo vendor crate set, a .cargo/config and a pinned integrity manifest — everything a disconnected box needs to cargo build offline. holger serves those bundles; znippy builds and indexes them.
holger-handler-rust-toolchain-wasm depends on this crate and exports only the eight ABI symbols, and tests/native_wasm_agree.rs there asserts the two surfaces answer identically field by field.
The two are the same repository — one sealed .znippy per (name, version) — differing only in the member classifier, the bundle-name predicate and the handler name. Those three are this file; the store key, coordinate parse, HTTP routes and fetch/put/list come from [holger_handler_bundle_core], written once (LAW 5).
The classifier is znippy_plugin_rust_toolchain::member, taken default-features = false so the ArchiveTypePlugin impl and its znippy-common dependency (arrow · io_uring · gatling) are compiled out and the pure path → MemberKind function crosses to wasm32-unknown-unknown unchanged.
Module holger_handler_rust_toolchain
const HANDLER_NAME: & str · src/lib.rs:36
The handler's stable name.
const HANDLER_FORMAT: & str · src/lib.rs:47
The holger_traits::ArtifactFormat this handler reports.
Same finding as the Skidbladnir handler, and it lands the same way: holger has no RustToolchain variant, and ArtifactFormat::Rust would be wrong — that is the crates.io/registry format served by holger-rust-znippy-repository, not a sealed toolchain bundle. A client asking for Rust expects a .crate and a registry index. znippy is the truthful answer for a .znippy served as the artifact itself; the manifest's handler = "rust-toolchain" carries the real identity. Adding a variant is Rickard's call — see the report.
const BUNDLE_PREFIX: & str · src/lib.rs:50
The prefix every rust-dev bundle carries.
struct RustToolchain · src/lib.rs:53
The rust-toolchain [BundleKind].
type RustToolchainHandler · src/lib.rs:85
The handler instance holger loads.
fn describe_member(path: &str) -> Option<MemberDescription> · src/lib.rs:88
Classify one bundle-relative path — the shared ownership rule, re-exported.
Crate holger-handler-rust-toolchain-wasm
The Rust-toolchain bundle handler compiled to wasm32 — ABI shim only, all logic in holger-handler-rust-toolchain
The Rust-toolchain package handler compiled to wasm32-unknown-unknown.
cargo xtask build-wasm-plugins
# → target/wasm32-unknown-unknown/release/holger_handler_rust_toolchain_wasm.wasm
Holds no bundle logic — see holger-handler-rust-toolchain, which owns all of it and shares the repository machinery with the Skidbladnir handler. The eight ABI exports and the host-backed BlobStore come from export_package_handler!, so this crate is one line of glue over one line of declaration.
tests/native_wasm_agree.rs loads the real module and asserts it agrees with the linked handler field by field; it fails, never skips, on a missing or stale module.
Crate holger-handler-skidbladnir
holger package handler for Skidbladnir airgap bundles — the whole logic, shared by the native backend and the wasm module
The Skidbladnir airgap-bundle package handler for holger.
A Skidbladnir bundle is the Tillsynia/Njord deploy payload: one sealed .znippy holding a pg_dump, S3 document blobs, container images, infra operators, a Talos ISO, declarative config and age-encrypted secrets. holger's job is to serve those bundles; znippy's job is to build and index them.
holger-handler-skidbladnir-wasm depends on this crate and exports only the eight ABI symbols. Everything a request is answered with comes from here and from [holger_handler_bundle_core], so the two surfaces cannot diverge — tests/native_wasm_agree.rs in the wasm crate asserts field-by-field that they do not.
| piece | owner |
| store key · coordinate parse · HTTP routes · fetch/put/list | [holger_handler_bundle_core] — shared verbatim with the rust-toolchain handler, which is the same repository with a different classifier |
| member classification | znippy_plugin_skidbladnir::member, taken default-features = false. That leaves the pure path → MemberKind classifier and compiles out the ArchiveTypePlugin impl with its znippy-common dependency — arrow, io_uring and the gatling engine, none of which links on wasm32-unknown-unknown. |
| the bytes | nowhere here. Storage is a BlobStore the caller supplies. |
That second row is the seam. It is the same one znippy-plugin-maven uses for its wasm build (host-decompressors off ⇒ the threaded ljar fan-out is compiled out, the single-threaded linflate both sides share remains). Unlike maven there is no threading to compile out here — a bundle coordinate scheme was never parallel — so what the feature removes is the Arrow index writer, which this handler does not use either way. Measured 2026-08-05: cargo check -p znippy-plugin-skidbladnir --no-default-features --target wasm32-unknown-unknown builds the crate alone, …
Module holger_handler_skidbladnir
const HANDLER_NAME: & str · src/lib.rs:42
The handler's stable name — what a .wasm reports in its manifest and what an operator writes in a repository config.
const HANDLER_FORMAT: & str · src/lib.rs:60
The holger_traits::ArtifactFormat this handler reports, as its serde-lowercase spelling.
This is a finding, not a preference. holger has no Skidbladnir variant: ALL_ARTIFACT_FORMATS (traits/src/lib.rs:326) is Maven3, Pip, Rust, Go, Nuget, Npm, Gem, Deb, Rpm, Helm, Docker, Conda, Composer, Znippy, Raw. Adding one touches ArtifactFormat, RepositoryType, endpoint_name, znippy_type_id and every exhaustive match over the enum across sixteen backend crates — Rickard's call, so it is reported rather than smuggled in.
znippy is meanwhile the honest answer and not a placeholder: a Skidbladnir bundle is a sealed .znippy served as the artifact itself, which is exactly what ArtifactFormat::Znippy documents itself to mean. What is lost is only telling a bundle from any other .znippy by format alone — the handler's own identity survives, because the plugin manifest carries handler = "skidbladnir" independently of format.
struct Skidbladnir · src/lib.rs:63
Skidbladnir's [BundleKind]: the three things that are not shared.
type SkidbladnirHandler · src/lib.rs:90
The handler instance holger loads.
fn describe_member(path: &str) -> Option<MemberDescription> · src/lib.rs:94
Classify one bundle-relative path — the shared ownership rule, re-exported so a caller need not name the znippy crate.
Crate holger-handler-skidbladnir-wasm
The Skidbladnir airgap-bundle handler compiled to wasm32 — ABI shim only, all logic in holger-handler-skidbladnir
The Skidbladnir package handler compiled to wasm32-unknown-unknown.
cargo xtask build-wasm-plugins
# → target/wasm32-unknown-unknown/release/holger_handler_skidbladnir_wasm.wasm
It holds no bundle logic. Every coordinate parse, store key, HTTP route, listing rule and member classification comes from holger-handler-skidbladnir — the same crate the linked backend is built from — which in turn shares its repository machinery with the rust-toolchain handler (holger-handler-bundle-core) and its classifier with znippy (znippy-plugin-skidbladnir, --no-default-features).
The ABI glue is not written here either: export_package_handler! emits all eight exports and the host-backed BlobStore, so the same pointer protocol serves every handler and cannot drift between two of them.
tests/native_wasm_agree.rs loads the real module through the real host and asserts, field by field, that it answers as the linked handler does. It fails — never skips — when the module is missing or stale.
| native only | why |
znippy_plugin_skidbladnir's ArchiveTypePlugin impl |
it is built on znippy-common — arrow, io_uring and the gatling engine. Compiled out by default-features = false; only the pure classifier in member.rs remains, and that is the half holger needs. |
| the filesystem | a module has no std::fs. Storage arrives through the four host_store_* imports, backed by the same DirStore the linked backend uses — so both surfaces read the same bytes. |
Unlike znippy's maven pair there is no threading seam to find: a bundle coordinate scheme was never parallel, so nothing had to be compiled out for wasm beyond the Arrow index writer this handler does not use either way.
Crate holger-mannequin
[test-infra] Mock PKI+OIDC+Nexus in one process. Test/dev only — never production.
Module holger_mannequin::auth::users
enum AuthMethod { Oidc, Mtls } · src/auth/users.rs:9
How a user authenticated.
struct Identity { subject: String, method: AuthMethod, roles: Vec<String> } · src/auth/users.rs:16
A resolved identity after successful auth.
struct AuthService · src/auth/users.rs:24
Fake user database + auth validation for testing. Ties together OIDC tokens and client certs to identities.
struct UserCredentials { username: String, access_token: String, client_cert_pem: String, client_key_pem: String } · src/auth/users.rs:109
Credentials bundle for a test user.
Module holger_mannequin::auth::users::AuthService
fn new() -> Self · src/auth/users.rs:40
fn validate_oidc_token(&self, token: &str, oidc: &OidcService) -> Option<Identity> · src/auth/users.rs:52
Validate a Bearer token via OIDC service, return identity.
fn validate_client_cert(&self, cn: &str) -> Option<Identity> · src/auth/users.rs:67
Validate a client certificate CN, return identity.
fn issue_credentials(&self, username: &str, oidc: &OidcService, pki: &PkiService,) -> anyhow::Result<UserCredentials> · src/auth/users.rs:81
Issue credentials for a test user (token + client cert).
fn list_users(&self) -> Vec<String> · src/auth/users.rs:98
fn add_user(&self, name: &str, roles: Vec<String>) · src/auth/users.rs:102
Module holger_mannequin::config
struct MannequinConfig { port: u16, ca_cert: Option<CertSource>, ca_key: Option<CertSource>, server_cert: Option<CertSource>, server_key: Option<CertSource> } · src/config.rs:8
Configuration for holger-mannequin. If cert fields are empty/None, fresh certs are generated at startup. Otherwise, the supplied certs (inline PEM or file paths) are used.
enum CertSource { Inline, File } · src/config.rs:32
A certificate/key can be supplied inline (base64/PEM) or as a file path.
Module holger_mannequin::config::CertSource
fn resolve(&self) -> anyhow::Result<String> · src/config.rs:38
Module holger_mannequin::config::MannequinConfig
fn load(path: &std::path::Path) -> anyhow::Result<Self> · src/config.rs:55
Load from a RON file, or return default (generate everything).
fn has_ca(&self) -> bool · src/config.rs:63
Returns true if CA certs are supplied (don't generate).
Module holger_mannequin::event_log
struct LogEntry { timestamp: DateTime<Utc>, system: SystemKind, direction: Direction, summary: String, detail: Option<String> } · src/event_log.rs:5
enum SystemKind { Pki, Oidc, Nexus } · src/event_log.rs:14
enum Direction { Inbound, Outbound } · src/event_log.rs:31
struct EventLog · src/event_log.rs:38
Shared event log accessible from both server handlers and GUI.
Module holger_mannequin::event_log::EventLog
fn new() -> Self · src/event_log.rs:49
fn log(&self, system: SystemKind, direction: Direction, summary: impl Into<String>, detail: Option<String>) · src/event_log.rs:55
fn entries_for(&self, system: &SystemKind) -> Vec<LogEntry> · src/event_log.rs:65
fn all_entries(&self) -> Vec<LogEntry> · src/event_log.rs:75
fn count_for(&self, system: &SystemKind) -> usize · src/event_log.rs:79
Module holger_mannequin::event_log::SystemKind
fn label(&self) -> &'static str · src/event_log.rs:21
Module holger_mannequin::facet_react
holger's REACTION to a facett UI error code (facett error-codes, Phase 2).
facett panes raise a stable, unique UI error code —
facet-<component>-<n>([facett_core::errcode]) — carrying BOTH the human message and the code, so a consumer reacts to the CODE, not a matched string. This module is holger's half.How this COMPOSES with
holger-<subsystem>-<n>holger owns a frozen registry of its OWN application error codes (
holger-errcode,holger-<subsystem>-<n>, exhaustively matched so an un-coded variant is a COMPILE error). The two vocabularies compose, they never collide:
- a facett UI failure surfaced through a holger mount keeps facett's frozen code plus holger's mount prefix —
holger/mannequin:facet-syschart-1;- holger's OWN backend failure keeps
holger-<subsystem>-<n>.The mount
holger embeds
facett_syschart::SystemChartas the mannequin PKI/OIDC/Nexus system map (holger/mannequin). It is declared in facett's shared registry, so the reaction obligations below are generated, not hand-authored.
const APP: & str · src/facet_react.rs:26
holger's application key in the shared reaction matrix.
const MANNEQUIN: ConsumerMount · src/facet_react.rs:31
The mannequin mount — holger's PKI / OIDC / Nexus system map. site is the ANTI-FICTION anchor: the guard test asserts this file exists and really mounts the component whose codes it claims.
const MOUNTS: & [ConsumerMount] · src/facet_react.rs:35
Every holger mount.
fn matrix() -> Vec<(String, &'static str, &'static str, Reaction)> · src/facet_react.rs:39
The reaction-matrix rows holger is contracted to satisfy — sliced from facett's shared table.
fn react(rec: &mut FacetRecorder, mount: ConsumerMount, err: &FacetError) -> Reaction · src/facet_react.rs:47
React to a facett UI error raised at a holger mount. Namespaces the code with the mount, records the MOUNTED code into rec (folded into the GUI's state_json), and returns the canonical [Reaction]. An unregistered code is [Reaction::Diagnose] — never a silent pass.
fn react_to_state(rec: &mut FacetRecorder, mount: ConsumerMount, state: &serde_json::Value,) -> Option<Reaction> · src/facet_react.rs:54
React to whatever a driven facett pane PUBLISHED — the pane writes state_json.error_code when it is in its error state, so holger branches on the CODE in the data rather than scanning a rendered string.
fn state_json(rec: &FacetRecorder) -> serde_json::Value · src/facet_react.rs:70
The observable Phase-2 surface for holger's state_json.
fn reaction_of(code: &str) -> Option<Reaction> · src/facet_react.rs:93
Resolve the canonical reaction for a (possibly mounted) facett code.
Module holger_mannequin::gui
Mannequin GUI — the holger test-dummy system map, drawn through facett components (the nordisk house pattern) instead of raw egui:
- the PKI / OIDC / Nexus system map → a
facett_syschart::SystemChart(clickable peer nodes + edges, a per-node event-count badge, and a click-select → inline detail panel — the component invented for this GUI);- the selected system's event log → a
facett_table::Table(timestamp / direction / summary rows);- the PKI issued-cert list → a small tree/list, with the Regenerate action kept as a live button.
Everything stays live: each frame we re-read the shared [
EventLog] + [PkiService] and refresh the chart badges + the table rows, so the map updates as the daemon serves requests.The shell is a [
Scene] — because this pane really is master/detailThe map and the log were two things stacked in one
ScrollAreawith aui.separator()between them: the operator could not resize either, could not collapse the map to read a long log, and could not blow the log up to read it. But the relationship was master/detail all along — you pick a system ON the map and the panel below is the detail OF that pick.So the shell is declared as one [
facett_app::scene::Scene] (L5 — the same shared pane host holger-server-ui's shell adopted) and rendered by [scene::render]:mannequin-shell ── Split(V, 0.55, DRAGGABLE) ├─ map_pane (Foldable 🗺) slot:map — the SystemChart (the MASTER) └─ detail_pane (Foldable 📜) slot:detail — the selection's log + PKI controlsThat buys a real draggable divider and — via facett-app's
Chrome::Auto— a window title bar on each pane carrying fold · minimise · maximise. Nothing about the two bodies changed: the sameSystemChartand the sameTableare drawn, through a [SceneHost] instead of inline.
const SHELL_PANE: & str · src/gui.rs:53
The root pane's unique name — the DRAGGABLE vertical master/detail split. The Router reports it as the scene route and a robot drags its divider by the rect the render publishes under separators.
const MAP_PANE: & str · src/gui.rs:56
The MASTER pane: the PKI / OIDC / Nexus system map.
const DETAIL_PANE: & str · src/gui.rs:59
The DETAIL pane: the selected system's event log (+ the PKI controls).
fn mannequin_scene() -> Scene · src/gui.rs:68
Build the mannequin shell as a declarative [Scene]: a draggable vertical master/detail split of the system map over the selection's detail. Both panes are foldable, and both wear facett-app's default window chrome (fold · minimise · maximise) — the root stays bare, as it must.
Pure and window-free, so a test can build the exact shipped shell and assert its shape without a display.
fn node_a11y_label(kind: &SystemKind, badge: u64) -> String · src/gui.rs:105
The EXACT AccessKit label facett_syschart::SystemChart registers on a node's clickable button — "{label} · {badge} events" (see facett-syschart FC-4). The robot addresses the REAL on-map node by this string, and state_json publishes it per node, so the drive target and the emit stay one truth.
struct MannequinGui · src/gui.rs:109
fn default_look_preset() -> usize · src/gui.rs:135
The default look preset for the mannequin GUI: the running-OS-matched preset's index into [LookTheme::PRESETS]. Linux/unknown → windows-dark (facett's documented default). Pure → unit-testable with no window.
fn build_system_chart(badges: [(SystemKind, u64); 3]) -> SystemChart · src/gui.rs:146
Build the mannequin system map as a bare facett [SystemChart] — the three mock systems (PKI / OIDC / Nexus) as peer nodes, fully wired with edges. This is the single source of truth for the chart's topology: the live GUI ([MannequinGui::new]) builds from it AND the headless robot-UI matrix cell renders this exact chart offscreen to assert its state_json. Badges start at the supplied counts so a test can inject real numbers and read them back.
Module holger_mannequin::gui::MannequinGui
fn new(event_log: EventLog, pki: Arc<PkiService>) -> Self · src/gui.rs:165
fn look_theme(&self) -> LookTheme · src/gui.rs:186
The active look preset's [LookTheme].
fn set_look_preset(&mut self, idx: usize) -> String · src/gui.rs:192
Switch the active look preset (clamped); returns the new preset name.
fn state_json(&self) -> serde_json::Value · src/gui.rs:215
Remote-observable state (LAW 6): every on-screen VALUE an operator sees, as data, so a robot asserts the value — never the pixels. The drivable half exists too (set_look_preset / select_system / regenerate_pki / toggle_fold …), so a robot can drive each primary function and assert the result IN CODE.
The roster, matched 1:1 against what the shipped shell paints:
look_preset/look_name— the chrome bar's look switcher's active pick;selected_system— the map's one selection (drives the detail pane);events.{pki,oidc,nexus}— the per-node badge numbers on the map;pki_issued— the PKI issued-cert count;chart_nodes[]— every map node's{id,label,badge,selected}PLUS the exact AccessKit label (a11y_label) the facettSystemChartregisters for it, so a robot can click the REAL on-map node (no proxy);detail{}— what the detail pane renders for the current selection, and its EMPTY / ERROR states as booleans (empty,pki_certs_empty,activity_*), so an empty pane is an asserted value, never an eyeball (LAW 2 / 4);scene{}— the pane-chrome geometry + fold/minimise/maximise/split state.
fn toggle_fold(&mut self, pane: &str) -> bool · src/gui.rs:312
Robot/test hook: fold or unfold a scene pane by name (the same toggle the chrome bar's ⏷ control runs). Returns the new folded state.
fn toggle_minimize(&mut self, pane: &str) -> bool · src/gui.rs:318
Robot/test hook: minimise a pane — collapse it to its chrome title bar and hand its space back to its sibling. Returns the new minimised state.
fn toggle_maximize(&mut self, pane: &str) -> bool · src/gui.rs:324
Robot/test hook: maximise a pane — give it the whole shell, shrinking its sibling to zero area. At most one pane holds it. Returns the new state.
fn restore_pane(&mut self, pane: &str) -> bool · src/gui.rs:330
Robot/test hook: restore a pane (drop its minimise AND its maximise). The split ratio was never overwritten, so the layout returns exactly as it was.
fn split_ratio(&self) -> Option<& Vec<f32>> · src/gui.rs:336
Robot/test hook: the live first-child (map) fraction of the master/detail split — what a divider drag moves.
fn select_system(&mut self, id: &str) -> Option<String> · src/gui.rs:343
Robot/test hook: select a system on the map (the headless analogue of clicking a chart node) — the ONE selection the detail pane renders. Re-selecting the same node deselects, per the chart's documented toggle. Returns the new selection.
fn selected_system(&self) -> Option<String> · src/gui.rs:350
Robot/test hook: the ONE selected system id ("pki"/"oidc"/"nexus"), or None when the map has nothing selected. The read half of [select_system].
fn regenerate_pki(&mut self) -> bool · src/gui.rs:358
Robot/test hook: regenerate the PKI CA (the headless analogue of clicking the detail pane's 🔄 Regenerate button). On success it logs the same outbound PKI event the live button does, so the event count a robot asserts moves. Returns whether the regenerate succeeded.
fn issued_certs(&self) -> Vec<String> · src/gui.rs:378
Robot/test hook: the PKI issued-cert common-names (what the detail pane's cert list renders).
fn event_count(&self, kind: &SystemKind) -> usize · src/gui.rs:383
Robot/test hook: the live event count for one system (the badge number).
fn apply_look(&self, ctx: &egui::Context) · src/gui.rs:390
Apply the active preset to ctx — installs the full egui Style and publishes the derived legacy palette so the facett SystemChart + Table follow (COH-1).
fn run(self) -> anyhow::Result<()> · src/gui.rs:394
Module holger_mannequin::nexus
struct NexusRepository { name: String, format: String, repo_type: String, url: Option<String> } · src/nexus/mod.rs:12
struct NexusAsset { path: String, download_url: String, content_type: Option<String>, size: Option<u64> } · src/nexus/mod.rs:21
struct AssetPage { items: Vec<NexusAsset>, continuation_token: Option<String> } · src/nexus/mod.rs:32
struct MockNexusService { proxy: ProxyRepo, prod: ProdRepo, upload: UploadRepo } · src/nexus/mod.rs:39
Aggregates all three Nexus repo types.
Module holger_mannequin::nexus::MockNexusService
fn new(base_url: &str) -> Self · src/nexus/mod.rs:46
fn list_repositories(&self) -> Vec<NexusRepository> · src/nexus/mod.rs:56
fn list_assets(&self, repo: &str) -> Vec<NexusAsset> · src/nexus/mod.rs:79
async fn download(&self, repo: &str, path: &str) -> Option<Vec<u8>> · src/nexus/mod.rs:88
fn upload(&self, repo: &str, path: &str, data: Vec<u8>) -> bool · src/nexus/mod.rs:97
Module holger_mannequin::nexus::prod
struct ProdRepo · src/nexus/prod.rs:6
Prod repo: pre-loaded with a few well-known crates. Read-only.
Module holger_mannequin::nexus::prod::ProdRepo
fn new(base_url: &str) -> Self · src/nexus/prod.rs:25
fn seed_defaults(&mut self) · src/nexus/prod.rs:34
Seed the 4 default crates (fetched lazily on first download).
fn seeded_paths() -> Vec<String> · src/nexus/prod.rs:39
fn list_assets(&self) -> Vec<NexusAsset> · src/nexus/prod.rs:45
async fn download(&self, path: &str) -> Option<Vec<u8>> · src/nexus/prod.rs:66
Async (M1): previously this used block_in_place + Handle::block_on, which panics on a current-thread runtime (in-process integration tests) and otherwise parks a worker thread for the whole fetch. Awaiting the fetch directly — like ProxyRepo::download — is correct on any runtime flavor.
Module holger_mannequin::nexus::proxy
struct ProxyRepo · src/nexus/proxy.rs:6
Proxy-only repo: fetches crates from crates.io on demand, caches in memory.
Module holger_mannequin::nexus::proxy::ProxyRepo
fn new(base_url: &str) -> Self · src/nexus/proxy.rs:13
fn list_assets(&self) -> Vec<NexusAsset> · src/nexus/proxy.rs:21
async fn download(&self, path: &str) -> Option<Vec<u8>> · src/nexus/proxy.rs:30
Module holger_mannequin::nexus::upload
struct UploadRepo · src/nexus/upload.rs:6
Upload repo: starts empty, accepts PUT uploads.
Module holger_mannequin::nexus::upload::UploadRepo
fn new(base_url: &str) -> Self · src/nexus/upload.rs:12
fn list_assets(&self) -> Vec<NexusAsset> · src/nexus/upload.rs:19
fn download(&self, path: &str) -> Option<Vec<u8>> · src/nexus/upload.rs:28
fn upload(&self, path: &str, data: Vec<u8>) · src/nexus/upload.rs:32
Module holger_mannequin::oidc
struct OidcService · src/oidc/mod.rs:7
Mock OIDC provider for testing authentication flows.
struct TokenRecord { access_token: String, subject: String, issued_at: i64, expires_at: i64 } · src/oidc/mod.rs:13
struct OidcDiscovery { issuer: String, authorization_endpoint: String, token_endpoint: String, userinfo_endpoint: String, jwks_uri: String } · src/oidc/mod.rs:21
struct TokenRequest { grant_type: Option<String>, username: Option<String>, password: Option<String>, client_id: Option<String> } · src/oidc/mod.rs:30
struct TokenResponse { access_token: String, token_type: String, expires_in: i64, sub: String } · src/oidc/mod.rs:38
Module holger_mannequin::oidc::OidcService
fn new(issuer: String) -> Self · src/oidc/mod.rs:46
fn discovery(&self) -> OidcDiscovery · src/oidc/mod.rs:53
fn issue_token(&self, subject: &str) -> TokenResponse · src/oidc/mod.rs:64
Issue a token (accepts any credentials — it's a test dummy).
fn validate_token(&self, token: &str) -> Option<String> · src/oidc/mod.rs:85
Validate a token (returns subject if valid).
Module holger_mannequin::pki
struct PkiService · src/pki/mod.rs:6
PKI service: manages a test CA and issues certificates on demand.
struct IssuedCert { common_name: String, cert_pem: String, key_pem: String } · src/pki/mod.rs:11
Module holger_mannequin::pki::PkiService
fn new() -> anyhow::Result<Self> · src/pki/mod.rs:48
Create with a freshly generated CA.
fn from_config(config: &MannequinConfig) -> anyhow::Result<Self> · src/pki/mod.rs:57
Create from config: use supplied certs if present, otherwise generate.
fn regenerate(&self) -> anyhow::Result<()> · src/pki/mod.rs:70
Regenerate the CA and clear all issued certs.
fn ca_cert_pem(&self) -> String · src/pki/mod.rs:78
Get CA certificate in PEM format.
fn issue_server_cert(&self, cn: &str, sans: Vec<& str>) -> anyhow::Result<IssuedCert> · src/pki/mod.rs:84
Issue a server certificate signed by the CA.
fn issue_client_cert(&self, cn: &str) -> anyhow::Result<IssuedCert> · src/pki/mod.rs:113
Issue a client certificate signed by the CA.
fn list_issued(&self) -> Vec<String> · src/pki/mod.rs:141
List all issued certificates.
Module holger_mannequin::robot
Robot-UI wiring for the mannequin GUI — ONE scenario, BOTH modes, REAL shell
This is the mannequin's adoption of the shared
nornir-robotuiframework (edda leaf, 0.2.6 — Watch resolves clicks via AccessKit), following itsdemo.rsreference:
ONE [
Observable] — the mannequin GUI exposes EVERY on-screen value as JSON so a robot asserts the value, not the pixels (delegates to [MannequinGui::state_json]);ONE
draw(ui, app, targets)closure — [mannequin_draw] — that renders the REAL shipped shell ([eframe::App::ui]) into the robot frame. The primary controls a robot drives are therefore the ACTUAL widgets a user touches:
- the facett
SystemChart's map nodes (PKI / OIDC / Nexus) — each a real AccessKit button the component registers (facett-syschart FC-4), labelled"{system} · {badge} events", so the robot clicks the REAL on-map node;- the detail pane's
🔄 Regeneratebutton;- the chrome bar's look-preset switcher (
selectable_labelper preset);- each pane's window-chrome fold control (
"fold <pane>").ONE [
Scenario] — [mannequin_scenario] — the exact steps CI runs headless andholger-mannequin robot --watchreplays visibly.Why the REAL shell, not a proxy control rail
An earlier pass drew a bespoke button rail because the map nodes were believed to be canvas-drawn and thus not click-addressable. That is no longer true: facett's
SystemChartpaints each node AND registers an AccessKit button over it (facett_core::a11y::node, label"{label} · {badge} events"). Driving a proxy would have tested a surface the user never touches (LAW 1 — SHIPPED ≠ TESTED), so the robot now drives the shipped shell directly and asserts values off itsstate_json.state_json["chart_nodes"][*]["a11y_label"]publishes each node's exact click label, keeping the drive target and the emit one truth.The choosable entry point lives in [
run]: pass [RunMode::Headless] for the CI matrix or [ …
const ROBOT_LOOK: & str · src/robot.rs:53
The stable look preset a robot switches TO first — not the running-OS default (windows-dark on Linux), so the click provably CHANGES the observed look name.
const ROBOT_LOOK_2: & str · src/robot.rs:57
A SECOND preset the scenario switches to, to prove the switcher maps EACH preset to its own name (LAW 3 — sweep, not one value).
fn mannequin_draw(ui: &mut egui::Ui, app: &mut MannequinGui, _t: &mut Targets) · src/robot.rs:72
Draw ONE frame of the mannequin robot surface — the real shipped shell. The robot frame hands us a &mut egui::Ui; we render the exact eframe::App::ui a user runs into it (a throwaway kittest Frame, which the shell ignores). Every primary control — the map's AccessKit node buttons, the Regenerate button, the look switcher, the pane fold chrome — is therefore the ACTUAL widget, addressable by its real label in both the headless AccessKit path and the watch cursor.
Targets is unused: the shell's controls all carry AccessKit labels (facett's FC-4 contract), so both modes resolve them from the live tree — no proxy rect to record.
fn mannequin_scenario() -> Scenario · src/robot.rs:86
The mannequin robot [Scenario], driving the REAL shell. Every step that changes state asserts the VALUE changed (not merely that something drew / a selection moved): a Regenerate increments /events/pki, a look switch lands the exact /look_name, a fold flips /scene/folded/<pane>. Primary functions are SWEPT (LAW 3): Regenerate is proven to increment on EACH press (0→1→2, not a 0→1 flip), the look switcher is proven to map TWO distinct presets to their own names, the three map nodes are each selected, and the fold is exercised in BOTH directions. Empty/error states are asserted as values (LAW 2 / 4): the detail placeholder, the empty issued-cert list, and per-system "No activity yet".
fn run(mode: RunMode, cfg: WatchConfig) -> anyhow::Result<RunOutcome> · src/robot.rs:226
The choosable entry point. Run the mannequin robot scenario in mode: [RunMode::Headless] fills the CI matrix (off-screen egui_kittest), [RunMode::Watch] opens a REAL eframe window and replays it visibly. cfg paces the watch run.
Headless builds its own sized [Harness] (rather than run_scenario_mode's default-size one) so the whole shipped map canvas is on-screen and every on-map node is a reachable click target; Watch delegates to the shared runner, which owns the real eframe window.
Module holger_mannequin::server
struct MannequinServer { pki: Arc<PkiService>, oidc: Arc<OidcService>, auth: Arc<AuthService>, nexus: Arc<MockNexusService>, event_log: EventLog, port: u16 } · src/server.rs:19
Module holger_mannequin::server::MannequinServer
fn new(port: u16) -> anyhow::Result<Self> · src/server.rs:38
fn from_config(config: MannequinConfig) -> anyhow::Result<Self> · src/server.rs:42
fn router(&self) -> Router · src/server.rs:54
async fn serve(self) -> anyhow::Result<()> · src/server.rs:87
Crate holger-nexus-connector
Sonatype Nexus connector for the holger agent — a ConnectorTrait Source/Target that lists repositories and assets and moves artifact bytes over Nexus's REST API (basic / bearer / mTLS), so an existing Nexus can be mirrored into holger or fed from it.
Nexus ConnectorTrait — Sonatype Nexus 3 as an agent Source/Target.
Talks the REST v1 API (/service/rest/v1/repositories, /assets) to enumerate repos and continuation-token-paginated assets, downloads via each asset's downloadUrl, and uploads with a raw PUT to /repository/{r}/{p}. Auth is a ConnectorAuth (None/Basic/OidcToken/Mtls) attached per request; with_oidc_token/with_mtls/fetch_oidc_token cover the non-Basic paths.
Two invariants the code exists to hold: the downloadUrl is untrusted, so download_asset rejects any URL that is not same_origin_as_base before a request is sent — this stops SSRF / credential exfiltration to cloud-metadata or internal hosts. That guard only sees the initial URL, hence the client is built redirect-none (a 3xx would otherwise smuggle creds past it). All bodies flow through read_capped (8 GiB artifacts / 256 MiB JSON) so a lying or absent Content-Length can't OOM the agent under parallelism.
Module holger_nexus_connector
enum ConnectorAuth { None, Basic, OidcToken, Mtls } · src/lib.rs:51
How the connector authenticates with the registry.
struct NexusConnector { url: String, auth: ConnectorAuth } · src/lib.rs:62
async fn fetch_oidc_token(token_url: &str, username: &str, _password: &str) -> Result<String> · src/lib.rs:146
Fetch an OIDC access token from a token endpoint.
Module holger_nexus_connector::NexusConnector
fn new(url: String, username: Option<String>, password: Option<String>) -> Result<Self> · src/lib.rs:69
fn with_oidc_token(url: String, token: String) -> Result<Self> · src/lib.rs:88
Create a connector with OIDC token auth.
fn with_mtls(url: String, cert_pem: &[u8], key_pem: &[u8]) -> Result<Self> · src/lib.rs:103
Create a connector with mTLS client cert.
Crate holger-plugin-abi
holger package-handler plugin ABI: the wire contract a handler speaks whether it is linked in or loaded from a .wasm
holger's package-handler plugin contract — the one wire format a handler speaks whether it is linked into the server or loaded from a .wasm at startup.
Rickard's ask was that every package handler should be writable as native or WASM. Before this crate holger had no wasm path at all: the sixteen repository backends are unconditional path dependencies of server/lib, so adding a package handler meant editing server/lib/Cargo.toml and rebuilding the server.
A RepositoryBackendTrait is a Rust trait object and cannot cross a wasm boundary, so this crate splits a handler in two along the line that actually matters:
- [
PackageHandler] — the format logic: coordinate parsing, the HTTP path scheme, listing, content types. This is what varies per ecosystem and what a plugin author writes. It is pure and target-independent, so the one implementation compiles for the host and forwasm32-unknown-unknown. - [
BlobStore] — the bytes. A wasm module has no filesystem and no socket. The host owns storage and hands it in.
That split is what makes "the wasm crate is ABI glue only" true rather than aspirational: the native backend implements BlobStore over the filesystem, the wasm shim implements it over four host imports, and both call the same PackageHandler. It is the same shape as znippy's maven pair, where host-decompressors compiles the threaded ljar fan-out out of the wasm build and leaves the single-threaded linflate both sides share — same logic, different plumbing underneath.
RepositoryBackendTrait (traits/src/lib.rs:404) has twelve methods. Six cross:
| verb | why it is in |
plugin_manifest |
the module must name itself, or registration needs a config file and the whole point of scanning a directory is lost |
fetch |
the read. Non-defaulted on the trait. |
put |
the write. Non-defaulted on the trait. |
list |
enumeration; the UI and retent … |
Module holger_plugin_abi
trait BlobStore · src/lib.rs:129
The bytes behind a handler. Implemented natively over a directory and in wasm over the [imports] host functions — the handler logic above it never learns which.
Keys are handler-chosen store paths (bundles/tillsynia-1.4.znippy), not filesystem paths: the host is free to place them wherever it likes and a module never sees an absolute path.
trait PackageHandler · src/lib.rs:152
The format logic of one package handler. Everything here is pure over a [BlobStore], so the single implementation compiles for the host and for wasm32-unknown-unknown unchanged. A handler crate implements this once; the wasm crate beside it adds no logic, only the ABI shim.
Module holger_plugin_abi::BlobStore
fn get(&self, key: &str) -> Result<Option<Vec<u8>>, String> · src/lib.rs:133
The stored bytes, or None if the key is absent. An Err is a store failure (unreadable, permission denied) and must not be flattened into None — a missing artifact and a broken disk are different answers.
fn put(&self, key: &str, data: &[u8]) -> Result<(), String> · src/lib.rs:136
Store bytes under key, overwriting.
fn list(&self, prefix: &str) -> Result<Vec<(String, u64)>, String> · src/lib.rs:139
(key, size_bytes) for every key starting with prefix.
fn size(&self, key: &str) -> Result<Option<u64>, String> · src/lib.rs:143
Size of one key without reading it. Defaults to reading and measuring — correct everywhere, and a store with a cheaper stat overrides it.
Module holger_plugin_abi::PackageHandler
fn manifest(&self) -> WireManifest · src/lib.rs:154
Identity, reported to the host at load.
fn fetch(&self, store: &dyn BlobStore, id: &WireArtifactId,) -> Result<Option<Vec<u8>>, String> · src/lib.rs:157
Fetch one coordinate's bytes.
fn put(&self, store: &dyn BlobStore, id: &WireArtifactId, data: &[u8]) -> Result<(), String> · src/lib.rs:164
Store one coordinate's bytes.
fn list(&self, store: &dyn BlobStore, name_filter: Option<& str>, limit: usize,) -> Result<Vec<WireArtifactEntry>, String> · src/lib.rs:168
Enumerate held artifacts, filtered by a substring of the name.
fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId> · src/lib.rs:178
Map a raw HTTP suburl back to a coordinate — the inverse of the path this handler serves from. Pure: it must not touch the store, because the serve-time quarantine gate calls it on every request.
fn http(&self, store: &dyn BlobStore, req: &WireHttpRequest,) -> Result<WireHttpResponse, String> · src/lib.rs:181
Answer one HTTP request against this handler's own path scheme.
Module holger_plugin_abi::codec
The byte codec both surfaces share. One writer, one reader — the host links these functions and so does every guest, so a field cannot be encoded one way and decoded another (LAW 5, by construction rather than by a guard watching two copies agree).
Why not JSON
znippy's
wasm_loader::parse_json_to_rowsplits on,and:before stripping quotes, so a value containing either separator silently corrupts the row, and it has no null representation at all — an absent column and an empty one are the same bytes on the wire. Both are properties of the ad-hoc format, not bugs that can be patched out of the parser.This codec is length-prefixed, so a value is copied by length and its contents are never scanned for structure:
,,:,", a NUL and an arbitrary UTF-8 sequence all survive a round trip.Optioncarries an explicit 1-byte tag, soNoneandSome("")are distinct on the wire.codec_tests.rsasserts exactly those two properties.Layout
type bytes u32/u16/u64/i64little-endian, fixed width bool1 byte, 0or1bytes / Stringu32length, then that many bytesOption<T>0u8, or1u8thenTVec<T>u32count, then that manyTResult<T, String>0u8thenT, or1u8then the message
enum DecodeError { Truncated, LengthOverflow, BadTag, NotUtf8, TrailingBytes } · src/codec.rs:37
Why a byte string could not be decoded. Every variant names the field being read, because a decode failure on the host side is otherwise indistinguishable from a plugin that returned nothing.
struct Writer · src/codec.rs:77
Append-only byte writer.
struct Reader · src/codec.rs:149
Cursor over a byte string.
fn write_result<T>(w: &mut Writer, v: &Result<T, String>, ok: impl FnOnce(& mut Writer, &T)) · src/codec.rs:266
Encode a Result<T, String>: tag 0 = ok, tag 1 = the error message.
fn read_result<T>(r: &mut Reader<'_>, field: &'static str, ok: impl FnOnce(& mut Reader<'_>) -> Result<T, DecodeError>,) -> Result<Result<T, String>, DecodeError> · src/codec.rs:280
Decode a Result<T, String> written by [write_result].
Module holger_plugin_abi::codec::DecodeError
fn message(&self) -> String · src/codec.rs:294
Render for a Result<_, String> boundary (the ABI carries messages, not types).
Module holger_plugin_abi::codec::Reader
fn new(buf: &'a [u8]) -> Self · src/codec.rs:155
fn remaining(&self) -> usize · src/codec.rs:159
fn expect_end(&self, field: &'static str) -> Result<(), DecodeError> · src/codec.rs:167
Assert the value consumed the whole buffer. Called at the end of every top-level decode: a decoder that stops early is reading a different shape than the encoder wrote, and silently ignoring the tail is how an ABI skew turns into wrong values instead of an error.
fn u8(&mut self, field: &'static str) -> Result<u8, DecodeError> · src/codec.rs:184
fn u16(&mut self, field: &'static str) -> Result<u16, DecodeError> · src/codec.rs:188
fn u32(&mut self, field: &'static str) -> Result<u32, DecodeError> · src/codec.rs:193
fn u64(&mut self, field: &'static str) -> Result<u64, DecodeError> · src/codec.rs:198
fn i64(&mut self, field: &'static str) -> Result<i64, DecodeError> · src/codec.rs:205
fn bool(&mut self, field: &'static str) -> Result<bool, DecodeError> · src/codec.rs:209
fn bytes(&mut self, field: &'static str) -> Result<Vec<u8>, DecodeError> · src/codec.rs:217
fn str(&mut self, field: &'static str) -> Result<String, DecodeError> · src/codec.rs:229
fn opt_str(&mut self, field: &'static str) -> Result<Option<String>, DecodeError> · src/codec.rs:234
fn opt_bytes(&mut self, field: &'static str) -> Result<Option<Vec<u8>>, DecodeError> · src/codec.rs:242
fn count(&mut self, field: &'static str) -> Result<usize, DecodeError> · src/codec.rs:252
Read a u32 count and check it against the bytes actually left, so a corrupt count cannot make the caller pre-allocate an absurd Vec.
Module holger_plugin_abi::codec::Writer
fn new() -> Self · src/codec.rs:82
fn finish(self) -> Vec<u8> · src/codec.rs:86
fn u8(&mut self, v: u8) -> &mut Self · src/codec.rs:90
fn u16(&mut self, v: u16) -> &mut Self · src/codec.rs:95
fn u32(&mut self, v: u32) -> &mut Self · src/codec.rs:100
fn u64(&mut self, v: u64) -> &mut Self · src/codec.rs:105
fn i64(&mut self, v: i64) -> &mut Self · src/codec.rs:110
fn bool(&mut self, v: bool) -> &mut Self · src/codec.rs:115
fn bytes(&mut self, v: &[u8]) -> &mut Self · src/codec.rs:122
Length-prefixed bytes. The payload is copied verbatim — no escaping, no separator scan, so any byte sequence survives.
fn str(&mut self, v: &str) -> &mut Self · src/codec.rs:128
fn opt_str(&mut self, v: Option<& str>) -> &mut Self · src/codec.rs:133
None and Some("") differ by the tag byte, not by length.
fn opt_bytes(&mut self, v: Option<& [u8]>) -> &mut Self · src/codec.rs:140
Module holger_plugin_abi::exports
Exports a
.wasmpackage handler must provide. The host names the missing one when a module falls short, so a half-built module is a loud load failure rather than a backend that answersNoneto everything.
const ALLOC: & str · src/lib.rs:77
alloc(size: u32) -> u32 — reserve linear memory the host writes into.
const RESULT_LEN: & str · src/lib.rs:79
result_len() -> u32 — byte length of the last returned value.
const MANIFEST: & str · src/lib.rs:81
plugin_manifest() -> u32
const FETCH: & str · src/lib.rs:83
plugin_fetch(ptr, len) -> u32
const PUT: & str · src/lib.rs:85
plugin_put(ptr, len) -> u32
const LIST: & str · src/lib.rs:87
plugin_list(ptr, len) -> u32
const COORDINATE_FOR_PATH: & str · src/lib.rs:89
plugin_coordinate_for_path(ptr, len) -> u32
const HTTP: & str · src/lib.rs:91
plugin_http(ptr, len) -> u32
const ALL: & [& str] · src/lib.rs:94
Every export, in load-check order.
Module holger_plugin_abi::guest
The guest side of the ABI, written once and generated per module.
A wasm module has to export
#[no_mangle] extern "C"symbols, and those cannot come from a generic — so without this macro every-wasmcrate would carry its own copy of the same ~130 lines of pointer juggling, and two copies of an ABI is exactly how one half drifts from the other. [export_package_handler!] emits all eight exports plus the host-backed [crate::BlobStore] from one line, so a handler's wasm crate really is glue and nothing else (LAW 5).Everything it emits is
#[cfg(target_arch = "wasm32")]. On any other target the crate compiles to an empty library — which is what lets a-wasmcrate be an ordinary workspace member thatcargo check --workspacecovers, without a native build trying to linkenv::host_store_get.
macro_rules! export_package_handler · src/guest.rs:26
Generate the eight ABI exports for a [crate::PackageHandler].
holger_plugin_abi::export_package_handler!(holger_handler_skidbladnir::SkidbladnirHandler);
The type must implement PackageHandler and be constructible with Default::default(). Nothing else is required of the calling crate, and nothing about the handler's logic passes through here — this macro moves bytes and calls the handler.
Module holger_plugin_abi::imports
Host functions a module imports from
envto reach storage. The host owns every byte; the module owns only the format logic.All four return a packed
u64:ptr << 32 | len, and0means the host call itself failed — never "absent". Absence is carried inside the payload as an encodedOption, so a stored empty blob and a missing key stay distinguishable (the exact distinction znippy's JSON row format cannot make).
const STORE_GET: & str · src/lib.rs:107
host_store_get(key_ptr, key_len) -> u64 → encoded Option<Vec<u8>>.
const STORE_PUT: & str · src/lib.rs:109
host_store_put(key_ptr, key_len, data_ptr, data_len) -> u64 → encoded Result<(), String>.
const STORE_LIST: & str · src/lib.rs:111
host_store_list(prefix_ptr, prefix_len) -> u64 → encoded Vec<(String, u64)>.
const STORE_SIZE: & str · src/lib.rs:114
host_store_size(key_ptr, key_len) -> u64 → encoded Option<Vec<u8>> holding an 8-byte little-endian size, so "absent" and "zero bytes" stay distinct.
const MODULE: & str · src/lib.rs:119
The module namespace all of the above are imported from.
Module holger_plugin_abi::wire
The values that cross the boundary, and their encoders.
These deliberately mirror
holger_traits::{ArtifactId, ArtifactEntry}rather than reusing them:holger-traitsdepends onznippy-common, which carries arrow, io_uring and the gatling engine, so it cannot be compiled forwasm32-unknown-unknown. The mapping between the two lives in exactly one place —holger_plugin_host::convert— so a guest and a linked backend are projected into the router through the same code.
const ABI_VERSION: u32 · src/wire.rs:15
Bumped when the layout of anything in this module changes. The host refuses a module that reports a different number: a silently mismatched ABI decodes garbage into real fields, which is worse than not loading at all.
struct WireArtifactId { namespace: Option<String>, name: String, version: String } · src/wire.rs:19
A package coordinate. Mirror of holger_traits::ArtifactId.
struct WireArtifactEntry { id: WireArtifactId, size_bytes: i64, content_type: String } · src/wire.rs:54
One listed artifact. Mirror of holger_traits::ArtifactEntry.
struct WireManifest { abi_version: u32, handler: String, format: String, writable: bool } · src/wire.rs:79
What a module says about itself at load time, read once from plugin_manifest. This is why registration can read a directory rather than a config file: the module names itself.
struct WireHttpResponse { status: u16, headers: Vec<(String, String)>, body: Vec<u8> } · src/wire.rs:123
(status, headers, body) — the established HTTP-door contract across holger's backends, carried as one value.
struct WireHttpRequest { method: String, suburl: String, body: Vec<u8> } · src/wire.rs:155
The request half of the HTTP door.
struct WirePutRequest { id: WireArtifactId, data: Vec<u8> } · src/wire.rs:181
Arguments to plugin_put.
struct WireListRequest { name_filter: Option<String>, limit: u32 } · src/wire.rs:203
Arguments to plugin_list.
fn encode_fetch_response(v: &Result<Option<Vec<u8>>, String>) -> Vec<u8> · src/wire.rs:232
fn decode_fetch_response(b: &[u8]) -> Result<Result<Option<Vec<u8>>, String>, DecodeError> · src/wire.rs:240
fn encode_unit_response(v: &Result<(), String>) -> Vec<u8> · src/wire.rs:247
fn decode_unit_response(b: &[u8]) -> Result<Result<(), String>, DecodeError> · src/wire.rs:253
fn encode_list_response(v: &Result<Vec<WireArtifactEntry>, String>) -> Vec<u8> · src/wire.rs:260
fn decode_list_response(b: &[u8]) -> Result<Result<Vec<WireArtifactEntry>, String>, DecodeError> · src/wire.rs:271
fn encode_coordinate_response(v: &Option<WireArtifactId>) -> Vec<u8> · src/wire.rs:286
fn decode_coordinate_response(b: &[u8]) -> Result<Option<WireArtifactId>, DecodeError> · src/wire.rs:300
fn encode_http_response(v: &Result<WireHttpResponse, String>) -> Vec<u8> · src/wire.rs:311
fn decode_http_response(b: &[u8]) -> Result<Result<WireHttpResponse, String>, DecodeError> · src/wire.rs:317
fn encode_store_listing(rows: &[(String, u64)]) -> Vec<u8> · src/wire.rs:325
A (key, size) listing row from [crate::BlobStore::list].
fn decode_store_listing(b: &[u8]) -> Result<Vec<(String, u64)>, DecodeError> · src/wire.rs:334
fn encode_store_get(v: &Option<Vec<u8>>) -> Vec<u8> · src/wire.rs:346
fn decode_store_get(b: &[u8]) -> Result<Option<Vec<u8>>, DecodeError> · src/wire.rs:352
Module holger_plugin_abi::wire::WireArtifactEntry
fn encode(&self, w: &mut Writer) · src/wire.rs:61
fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> · src/wire.rs:66
Module holger_plugin_abi::wire::WireArtifactId
fn encode(&self, w: &mut Writer) · src/wire.rs:26
fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> · src/wire.rs:30
fn to_bytes(&self) -> Vec<u8> · src/wire.rs:38
fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> · src/wire.rs:44
Module holger_plugin_abi::wire::WireHttpRequest
fn to_bytes(&self) -> Vec<u8> · src/wire.rs:162
fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> · src/wire.rs:168
Module holger_plugin_abi::wire::WireHttpResponse
fn encode(&self, w: &mut Writer) · src/wire.rs:130
fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> · src/wire.rs:138
Module holger_plugin_abi::wire::WireListRequest
fn to_bytes(&self) -> Vec<u8> · src/wire.rs:209
fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> · src/wire.rs:215
Module holger_plugin_abi::wire::WireManifest
fn encode(&self, w: &mut Writer) · src/wire.rs:93
fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> · src/wire.rs:97
fn to_bytes(&self) -> Vec<u8> · src/wire.rs:106
fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> · src/wire.rs:112
Module holger_plugin_abi::wire::WirePutRequest
fn to_bytes(&self) -> Vec<u8> · src/wire.rs:187
fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> · src/wire.rs:194
Crate holger-plugin-host
Loads holger package handlers — linked or from a directory of .wasm — and wraps each in one RepositoryBackendTrait
holger's package-handler plugin host.
Loads .wasm package handlers from a directory at startup and wraps each one in a RepositoryBackendTrait, so server/lib/src/exposed/fast_routes.rs cannot tell a linked handler from a loaded one — the route table holds Arc<dyn RepositoryBackendTrait> and both kinds arrive as the same concrete [HandlerBackend].
linked handler ──┐ ┌── plugin_fetch
├─► HandlerRuntime ─► HandlerBackend ─┤
loaded .wasm ───┘ (one impl of RepositoryBackendTrait)
wasm-plugins (naming mirrors znippy-common's) gates dep:wasmtime. With the feature off this crate still builds and still hosts linked handlers — so a server tier that wants the two bundle handlers compiled in, and no wasmtime in its dependency graph, gets exactly that.
holger does not reach znippy-common for any of this. The loader's shape is borrowed from znippy_common::plugins::wasm_loader — a fresh Store per call, alloc/result_len, host functions on a Linker — but no dependency edge is added, because Rickard was explicit that holger touches znippy for the storage surface only.
Two things about that loader were deliberately not copied:
- its
parse_json_to_rowsplits on,and:before stripping quotes and has no null representation, so a value containing a separator corrupts the row silently. The ABI here is length-prefixed instead, andholger-plugin-abi/tests/codec_survives_what_json_did_not.rsasserts the two properties that fixes. - it is written in
.ok()?, so a trap, a missing export and a genuinely absent value are allNone. Here every failure is a namedErr: a repository backend that reported a trapped module as "no such artifact" would be a silent 404 over bytes that exist.
Module holger_plugin_host
fn mount_linked<H: PackageHandler + Send + Sync + 'static>(repo_name: impl Into<String>, handler: H, data_dir: impl AsRef<Path>,) -> Result<HandlerBackend, String> · src/lib.rs:74
Mount a handler that is compiled into the server over a directory store.
The native half of "every package handler can be written native or WASM": the same [PackageHandler] a .wasm is built from, called directly.
fn mount_linked_with<H: PackageHandler + Send + Sync + 'static>(repo_name: impl Into<String>, handler: H, store: SharedStore,) -> Result<HandlerBackend, String> · src/lib.rs:85
Mount a handler over a caller-supplied store (an in-memory one in tests, a directory in production).
fn shared_store(store: impl BlobStore + Send + Sync + 'static) -> SharedStore · src/lib.rs:94
Wrap any BlobStore for use by a mounted handler.
Module holger_plugin_host::agreement
The native-vs-wasm agreement harness, written once.
Every handler ships a pair — a native crate that owns the logic and a wasm crate that is ABI glue — and the claim "they are the same code" is only worth anything if something checks it over real bytes. Each
-wasmcrate'stests/native_wasm_agree.rscalls in here, so the assertions themselves are one implementation: two hand-written guards would be two things that can drift, and one of them would rot silently.What it refuses to do
- It never skips. A missing or stale
.wasmpanics with the build command. A test that skipped when the module was absent would be a permanent green — the exact shape that let znippy's wasm example sit "documented but never built" from the day its loader landed.- It never asserts a bare equality.
assert_eq!(native, wasm)passes when both are empty, so every comparison first asserts the content of the native side, then asserts the two match, and names the field that differs.Both surfaces share one store
The native backend and the loaded module are handed the same [
DirStore], so a disagreement is a disagreement about logic, never about which bytes each one could see.
const BUILD_CMD: & str · src/agreement.rs:39
The command that builds every module. Quoted verbatim in each failure so a red test carries its own fix.
fn locate_module(wasm_file_name: &str, source_dirs: &[PathBuf]) -> PathBuf · src/agreement.rs:47
Locate a built module, or panic with everything needed to fix it.
Checks every place it can legitimately land, most specific first, and then asserts it is not older than the sources it is built from — a stale module would let a changed handler pass a guard that compared it against the new native code.
struct Surfaces { native: HandlerBackend, wasm: HandlerBackend, dir: PathBuf } · src/agreement.rs:112
The two surfaces of one handler, over one shared store.
Module holger_plugin_host::agreement::Surfaces
fn build<H: PackageHandler + Send + Sync + 'static>(repo_name: &str, handler: H, wasm_file_name: &str, source_dirs: &[PathBuf],) -> Self · src/agreement.rs:126
Build both surfaces of handler over a fresh temp directory.
source_dirs are the src/ directories whose mtimes decide staleness — the wasm crate's own and every crate whose logic ends up inside the module, because the whole point of the design is that the logic is shared.
fn seed(&self, key: &str, data: &[u8]) · src/agreement.rs:154
Write a blob both surfaces will see.
fn assert_identity(&self, handler: &str, format: traits::ArtifactFormat, writable: bool) · src/agreement.rs:160
Identity must match, and must be the value asked for — not merely equal on both sides.
fn assert_fetch(&self, id: &ArtifactId, expect: Option<& [u8]>) · src/agreement.rs:172
fetch must return the same bytes. The expected body is asserted first, so two identically-broken surfaces returning None cannot pass.
fn assert_coordinate(&self, suburl: &str, expect: Option<(& str, &str)>) · src/agreement.rs:194
coordinate_for_path must resolve identically — the quarantine gate's lookup, and pure path logic, so any difference is a real ABI defect.
fn assert_http(&self, method: &str, suburl: &str, body: &[u8], expect_status: u16, expect_body: Option<& [u8]>,) -> (Vec<(String, String)>, Vec<u8>) · src/agreement.rs:211
The HTTP door must answer with the same status, headers and body.
Every part is compared separately so a failure names which part differs — a single assert_eq! on the tuple would only say "they differ".
fn assert_list(&self, filter: Option<& str>, limit: usize, expect: &[(& str, &str, i64)]) · src/agreement.rs:237
list must return the same rows in the same order.
fn assert_put_crosses(&self, id: &ArtifactId, data: &[u8]) · src/agreement.rs:259
A write through one surface must be readable through the other — the strongest form of "same store, same key scheme".
fn assert_put_refused(&self, id: &ArtifactId, why: &str) · src/agreement.rs:286
A coordinate both surfaces must REFUSE to store. Asserted on both, because a put that silently succeeded would write a blob whose key coordinate_from_key can never read back.
Module holger_plugin_host::backend
The one
RepositoryBackendTraitimplementation.There is exactly one in this crate, and both a linked handler and a loaded
.wasmreach the router through it. That is not a convention that has to be checked —fast_routes.rsreceives anArc<dyn RepositoryBackendTrait>whose concrete type is [HandlerBackend] in both cases, so there is no second code path for it to take. If the router ever needed to know which kind it had, the design would be wrong.The split that makes this possible is [
HandlerRuntime]: how a verb is executed (a direct call, or an ABI round trip through wasmtime) versus what the router asks for. Storage is bound into the runtime at construction, so by the time the trait is implemented the two are the same shape.
type SharedStore · src/backend.rs:26
A [BlobStore] usable from the server's request path.
trait HandlerRuntime · src/backend.rs:33
One handler, with its storage already bound — however it is executed.
The verbs mirror [PackageHandler] minus the store argument: a runtime owns its store, because a wasm module's store has to live in the wasmtime Store data and cannot be handed in per call.
struct LinkedHandler · src/backend.rs:45
A handler compiled into the server, bound to a store. The native half of "every package handler can be written native or WASM".
struct HandlerBackend · src/backend.rs:93
A package handler as holger's router sees it.
Debug prints the repository name, the format and the handler name — never the runtime, because the whole point is that a caller cannot tell a linked handler from a loaded module by looking at one.
Module holger_plugin_host::backend::HandlerBackend
fn new(repo_name: impl Into<String>, runtime: Box<dyn HandlerRuntime>) -> Result<Self, String> · src/backend.rs:106
Wrap a runtime. Fails if the handler declared a format holger does not know — never falling back to Raw, because a mis-declared format that registered anyway would serve real bytes under the wrong protocol.
fn handler_name(&self) -> &str · src/backend.rs:113
The handler's own name (skidbladnir), as distinct from the repository name this backend is mounted at.
fn into_backend(self) -> Arc<dyn RepositoryBackendTrait> · src/backend.rs:117
Module holger_plugin_host::backend::HandlerRuntime
fn manifest(&self) -> &WireManifest · src/backend.rs:34
fn fetch(&self, id: &WireArtifactId) -> Result<Option<Vec<u8>>, String> · src/backend.rs:35
fn put(&self, id: &WireArtifactId, data: &[u8]) -> Result<(), String> · src/backend.rs:36
fn list(&self, name_filter: Option<& str>, limit: usize) -> Result<Vec<WireArtifactEntry>, String> · src/backend.rs:37
fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId> · src/backend.rs:39
fn http(&self, req: &WireHttpRequest) -> Result<WireHttpResponse, String> · src/backend.rs:40
Module holger_plugin_host::backend::LinkedHandler
fn new(handler: H, store: SharedStore) -> Self · src/backend.rs:52
Module holger_plugin_host::catalog
Which handler READS the container.
A repository config already names the container:
ron_upstream_format = "znippy" the containerand
server/lib/src/repository.rsmaps that string ontoArtifactFormat::Znippy. Two handlers that both land onZnippy— a Skidbladnir deploy bundle and a Rust-toolchain bundle are both sealed.znippyfiles — cannot be told apart from there. znippy is a container: compressed files in a directory, sometimes with an extra index and extra functionality. What the config could not say is who reads the extra index.ron_handler = "skidbladnir" the reader; empty = generic znippyThis catalog is the lookup behind that string, and its whole job is that the string does not care which kind of handler answers it:
register_linked(SkidbladnirHandler::new) ──┐ ├─► "skidbladnir" ─► HandlerBackend load_dir("/var/lib/holger/plugins") ───────┘A linked handler names itself from its own [
PackageHandler::manifest]; a loaded.wasmnames itself from the manifest it reports over the ABI. Neither name is invented by the registrar, which is what makes the two interchangeable under one config string — and is why a new handler is a string in the config plus a.wasmin the directory, with no holger rebuild.An unknown name is a boot error
The precedent is
ron_upstream_formatin the same config file: a value that is present but unparseable hard-fails, because collapsing it with.unwrap_or(default)meant a typo silently built a repository with the wrong shape and every coordinate 404'd with no warning at boot. Same here. The error lists what is accepted — including every handler found in the plugin directory, which is the half an operator cannot read off the source.
enum Origin { Linked, Loaded } · src/catalog.rs:67
Where a catalog entry came from. Carried only so an operator-facing listing can say linked or name the file — never consulted when mounting, because a caller that branched on it would be re-introducing the distinction this design exists to remove.
struct RejectedModule { path: PathBuf, reason: String } · src/catalog.rs:111
One module in a plugin directory that could not join the catalog. Reported, never dropped: a broken or duplicate module is an operator error that must be visible, and the modules beside it still register.
enum HandlerError { Unknown, Unmountable } · src/catalog.rs:124
Why a configured ron_handler could not be honoured.
struct HandlerCatalog · src/catalog.rs:180
Every package handler this holger can put behind a ron_handler string.
Built once at startup and consulted per repository, so the plugin directory is read — and each module compiled — exactly once no matter how many repositories name a handler.
Module holger_plugin_host::catalog::HandlerCatalog
fn new() -> Self · src/catalog.rs:186
fn register_linked<H, F>(&mut self, make: F) -> Result<String, String> where H: PackageHandler + Send + Sync + 'static, F: Fn() -> H + Send + Sync + 'static, · src/catalog.rs:200
Register a handler compiled into this holger.
make is called once immediately to read the handler's own manifest — the name is the handler's, not the caller's. A registrar that passed a name in could mount SkidbladnirHandler under "rust-toolchain", and the config string would then mean two different things depending on whether it resolved to a linked handler or a module.
Returns the registered name. A second handler claiming a name already held is an Err rather than a silent replacement.
fn load_dir(&mut self, dir: impl AsRef<Path>) -> Result<Vec<RejectedModule>, ScanError> · src/catalog.rs:235
Read a plugin directory and register every .wasm in it under the name its own manifest declares.
A directory that is missing, unreadable or a file is a [ScanError] — never an empty catalog. A directory that exists and holds no module is Ok(vec![]), which is a different answer, and the caller can tell them apart because only one of them is an Err.
The returned rejections are modules that were present and did not register. They are not an error (the modules beside them still serve) and they must not be discarded — they are the only report a broken module makes.
fn load_dir(&mut self, dir: impl AsRef<Path>) -> Result<Vec<RejectedModule>, ScanError> · src/catalog.rs:273
Fail-closed stand-in for a build without the wasm host. A configured plugin directory that cannot be read is louder than a catalog that quietly holds only the linked handlers.
fn accepted(&self) -> Vec<String> · src/catalog.rs:278
Every accepted ron_handler value, sorted.
fn origin(&self, handler: &str) -> Option<Origin> · src/catalog.rs:283
Where a handler came from, or None if the name is not registered.
fn is_empty(&self) -> bool · src/catalog.rs:287
fn mount(&self, handler: &str, repo_name: &str, store: SharedStore,) -> Result<HandlerBackend, HandlerError> · src/catalog.rs:295
Mount handler at repository repo_name over store.
The repository name and the storage are the operator's; a handler never picks where it is served from or where it writes.
fn mount_dir(&self, handler: &str, repo_name: &str, data_dir: impl AsRef<Path>,) -> Result<HandlerBackend, HandlerError> · src/catalog.rs:313
[HandlerCatalog::mount] over a directory store — what a repository config's data directory resolves to.
Module holger_plugin_host::convert
The one mapping between the ABI's wire types and holger's own.
holger-plugin-abicannot useholger_traits::{ArtifactId, ArtifactEntry}directly —holger-traitsdepends onznippy-common, which carries arrow, io_uring and the gatling engine, none of which links forwasm32-unknown-unknown. So the ABI mirrors those two types, and everything that projects a handler into the router passes through the four functions here.Both the linked-handler backend and the wasm backend use them, which is what makes the two indistinguishable to
fast_routes.rsby construction rather than by inspection.
fn id_to_wire(id: &ArtifactId) -> WireArtifactId · src/convert.rs:17
fn id_from_wire(id: WireArtifactId) -> ArtifactId · src/convert.rs:25
fn entry_from_wire(e: WireArtifactEntry) -> ArtifactEntry · src/convert.rs:29
fn format_from_wire(s: &str) -> Result<ArtifactFormat, String> · src/convert.rs:47
Parse the manifest's format string into a real [ArtifactFormat].
Delegates to ArtifactFormat::from_format_str, holger's own parser, so a plugin and a RON config spell a format the same way — there is no second table here to drift.
An unrecognised spelling is an error naming the string and every accepted alternative, never a fallback to Raw. A module that mis-declared its format and was silently registered as raw would serve real bytes under a format the router did not mean, which is worse than refusing to load.
Module holger_plugin_host::registry
Registration reads a directory, not
server/lib/Cargo.toml.That is the whole point of the lane. Today holger's sixteen repository backends are unconditional path dependencies of
server/lib(server/lib/Cargo.toml:21-51) — none optional, none feature-gated — so adding a package handler means editing a manifest and rebuilding the server. A module dropped into the plugin directory is registered on the next start with no rebuild at all.Absence is loud
A plugin directory that is missing, unreadable, or a file rather than a directory is a named [
ScanError], never an empty list. This tree's disease is the check that returnstruebecause it never ran; a plugin loader that answered "no plugins" to a typo'd path would be the same shape, and would be discovered as a 404 in production rather than as an error at boot.A directory that exists and genuinely holds no
.wasmis a different answer — an empty registry withscannedpointing at the real path — and the caller can tell the two apart because only one of them is anErr.
const MODULE_EXT: & str · src/registry.rs:29
Extension a package-handler module must carry.
enum ScanError { Missing, NotADirectory, Unreadable, NoWasmHost } · src/registry.rs:34
Why a plugin directory could not be scanned at all. Distinct from a single module failing to load, which leaves the other modules usable.
struct LoadFailure { path: PathBuf, reason: String } · src/registry.rs:82
One module that was found but could not be used. Kept rather than dropped: a broken module in the directory is an operator error that must be visible, and the modules beside it still load.
struct Registry { scanned: PathBuf, backends: Vec<HandlerBackend>, failures: Vec<LoadFailure> } · src/registry.rs:95
The result of scanning one plugin directory.
trait Mounts · src/registry.rs:129
How a module's repository name and store are chosen.
A module names its handler (skidbladnir); the repository it is mounted at and the directory it stores into are the operator's, so the host supplies them rather than letting a plugin pick where it writes.
struct FileStemMounts { data_root: PathBuf } · src/registry.rs:143
The default mount policy: the repository is named after the module file's stem, and its store is <data_root>/<stem>.
Naming the repository after the file rather than the handler is deliberate — two copies of the same handler serving two different bundle stores is an ordinary deployment, and keying on the handler name would make the second one silently replace the first in the route table.
fn module_paths(dir: impl AsRef<Path>) -> Result<Vec<PathBuf>, ScanError> · src/registry.rs:167
Every .wasm in dir, sorted — the one directory walk both [scan] and [crate::catalog::HandlerCatalog::load_dir] read a plugin directory through, so "what counts as a module here" is decided once.
The three ways a directory can be unusable are three distinct errors; read_dir alone reports "not found" for all of them.
fn scan(dir: impl AsRef<Path>, mounts: &dyn Mounts) -> Result<Registry, ScanError> · src/registry.rs:200
Scan dir for .wasm package handlers and load each one.
Module holger_plugin_host::registry::Mounts
fn repo_name(&self, path: &Path, handler: &str) -> String · src/registry.rs:131
Repository name for a module found at path with handler name handler.
fn store(&self, path: &Path, handler: &str) -> SharedStore · src/registry.rs:133
The store that module gets.
Module holger_plugin_host::registry::Registry
fn is_empty(&self) -> bool · src/registry.rs:109
True when the directory held no .wasm at all — as distinct from a directory that could not be read, which is a [ScanError].
fn summary(&self) -> String · src/registry.rs:114
A one-line summary for a startup log.
Module holger_plugin_host::store
The host's own [
BlobStore]: a directory of files.This is the storage a linked handler runs on, and it is also what the wasm host hands to a loaded module through the four
host_store_*imports — so both surfaces read and write the same bytes in the same place, which is what lets the agreement guard compare them at all.
struct DirStore · src/store.rs:16
A directory-backed blob store. Keys are handler-chosen store paths (tillsynia-20260706.znippy), never absolute paths — a module never learns where the root is.
Module holger_plugin_host::store::DirStore
fn new(root: impl Into<PathBuf>) -> Self · src/store.rs:21
fn root(&self) -> &Path · src/store.rs:25
Module holger_plugin_host::wasm
The wasmtime host: load a
.wasm, and answer holger's verbs through it.Shape borrowed from
znippy_common::plugins::wasm_loader— a freshStoreper call,alloc/result_lenfor the buffer protocol, host functions registered on aLinker. What is deliberately not borrowed is its error handling: that loader is written in.ok()?, so every failure — a missing export, a trap, a bad UTF-8 result — collapses intoNone, which its caller reads as "this plugin found no metadata". Here a failure is a namedErrall the way out, because a repository backend that answers "no such artifact" to a trapped module would be a silent 404 over real bytes.holger does not reach znippy for any of this. The pattern is copied; no dependency edge is added.
struct WasmHandler · src/wasm.rs:41
A .wasm package handler, loaded and ready to answer.
Engine and Module are Send + Sync; a Store is not, so one is created per call. That also means a call cannot observe another call's linear memory, which is what makes a &self verb sound on a shared backend.
Module holger_plugin_host::wasm::WasmHandler
fn load(path: impl AsRef<Path>, store: SharedStore) -> Result<Self, String> · src/wasm.rs:56
Load a module and read its manifest.
Every failure names the module path and what was wrong with it. In particular a missing export is named individually: a module that is half-built is otherwise indistinguishable from one that simply answers nothing.
fn manifest(&self) -> &WireManifest · src/wasm.rs:120
fn path(&self) -> &Path · src/wasm.rs:124
fn into_runtime(self) -> Box<dyn HandlerRuntime> · src/wasm.rs:445
fn with_store(&self, store: SharedStore) -> Self · src/wasm.rs:457
The same compiled module, bound to a different store.
Compiling the Module is the expensive part of [WasmHandler::load]; binding a store is free. A catalog reads a plugin directory ONCE at boot and then mounts the same handler under however many repositories name it, each with its own storage — so this must not recompile, and it must not re-read the manifest either (a module whose manifest answered differently on a second read would be registered under one name and serve as another).
Crate holger-rust-file-repository
Rust / Cargo repository backend for holger, disk-backed and WRITABLE — speaks the same Cargo sparse protocol as holger-rust-znippy-repository but over a live loose-file tree, so it can accept cargo publish.
Disk-backed (loose-file) Rust/Cargo repository backend — the writable sibling of znippy-rust, which serves the same sparse index synthesized from a read-only znippy archive instead of a live directory tree.
[RustRepoFile] stores each crate as one file at base_dir/{ns}/{name}/{ver} (namespace: None maps to the literal _ dir) and speaks Cargo's sparse protocol over [RepositoryBackendTrait::handle_http2_request]: /index/config.json advertises the dl template, /index/{p1}/{p2}/{name} returns one NDJSON line per stored version, and /crates/{name}/{ver}/download streams the bytes. The sparse index re-hashes every version on each request, so real SHA-256 digests are memoized by a (len, mtime) [FileStamp] under a Mutex — sound only because stored artifacts are immutable (atomic write-then-rename, never overwritten).
THE GOTCHA: namespace/name/version arrive verbatim from the network and are each joined as a single path component, so every one MUST pass [check_component] before any join — a .., separator, or absolute component would otherwise let PathBuf::join traverse (or replace) base_dir into arbitrary host file read/write. Mirrors file-oci's guard.
Module holger_rust_file_repository
struct RustRepoFile { name: String } · src/lib.rs:81
Disk-backed, writable Rust repository backend.
struct RepoPath { p1: & 'a str, p2: & 'a str, name: & 'a str } · src/lib.rs:97
Module holger_rust_file_repository::RustRepoFile
fn new(name: String, base_dir: PathBuf, base_url: String) -> Self · src/lib.rs:110
fn sparse_path<'a>(crate_name: &'a str) -> RepoPath<'a> · src/lib.rs:136
Convert crate name to Cargo sparse 3-part path (p1, p2, name)
fn sparse_crate_from_parts<'a>(parts: &'a [& 'a str]) -> Option<& 'a str> · src/lib.rs:163
Reverse matcher: takes a sparse path slice ["xx","yy","name"] -> crate name
fn crate_sha256_hex(data: &[u8]) -> String · src/lib.rs:171
Crate holger-rust-znippy-repository
[storage-backends] Only backend that consumes the FormatGate reader pin and overrides has_archive; only one with a writable dev-mode archive.
Rust-crate repository backend: serves a Cargo sparse index synthesized on the fly from the .crate files inside a read-only znippy archive — the Rust sibling of the maven/python/npm znippy backends, all implementing [RepositoryBackendTrait]. handle_http2_request speaks the sparse protocol cargo expects: index/config.json (advertises dl, and api only when writable), index/{p1}/{p2}/{crate} (per-version ndjson with real SHA-256 cksums computed from the archived bytes), and crates/{c}/{v}/download. fetch resolves via the typed rust view ([ZnippyArchive::as_rust]) so hyphenated names and pre-release versions come from the index columns the cargo plugin wrote, not a {name}-{version}.crate filename guess — the generic-reader and writable paths fall back to that filename convention.
Two modes: static "drift" archives (read-only, served from reader) and writable dev-mode (with_writable_archive, the /sparring and /cache primary) where put/cargo publish append into an on-disk .znippy and reads re-open it live. Gotcha: put is a read-modify-write of one shared archive, so the write_lock must serialize the contains-check + create/append — without it two concurrent puts (e.g. pull-through cache-miss fetches) both see "absent" and both append, corrupting the archive.
Module holger_rust_znippy_repository
struct RustRepoZnippy { name: String } · src/lib.rs:33
Znippy-backed Rust crate repository.
Serves crates directly from any ZnippyReader backend using random-access extraction — no need to decompress the full archive.
Module holger_rust_znippy_repository::RustRepoZnippy
fn new(name: String) -> Self · src/lib.rs:71
fn with_writable_archive(name: String, archive_path: PathBuf) -> Self · src/lib.rs:88
Create a writable dev-mode repo backed by a .znippy at archive_path (created on the first put). Unlike with_archive, which serves a frozen archive read-only, this serves a live archive that put grows via znippy_common::meta_sink_append — the /sparring and /cache-primary backend. Reboot-safe: each put durably re-seals.
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:107
Create a repo backed by a specific znippy archive file.
The archive's recorded znippy format version is gated before a single row of it is parsed: one written by a newer znippy than this build reads is refused with a [traits::ArchiveFormatError], so the repo is disabled (instantiate_backends keeps every other repo live) instead of an unknown layout reaching the reader.
fn with_archive_and_url(name: String, archive_path: PathBuf, base_url: String) -> Result<Self> · src/lib.rs:123
Create a repo backed by a specific znippy archive file with a configured base URL. Format-version gated exactly like with_archive.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:138
Create a repo backed by any ZnippyReader implementation.
fn with_base_url(mut self, url: String) -> Self · src/lib.rs:151
Override the public base URL used in sparse index config.json.
fn list_files(&self) -> Vec<String> · src/lib.rs:189
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:196
fn contains(&self, relative_path: &str) -> bool · src/lib.rs:203
Crate holger
[server-core] The holger-server binary; tier compile_error! guard lives here. default-features=false on server-lib (the airgap keystone).
Module holger_server_cli::appliance
The appliance identity ceremony, holger-side — a thin caller of edda's
wg-appliance-core::ceremony(the ONE implementation holger, gunnar and korp share; nothing ceremony-shaped is implemented here).The gate lives in the declaration, not in folklore
The ceremony runs iff env [
GATE_ENV]=1. The[[build.container]]appliance cell in.nornir/build-variants.tomlDECLARES that env into the image, and the ISO shape will declare it on the kernel cmdline's environment — so "this binary is an appliance" is a property of the packed artifact, never of a hand-typed flag. A plainholger-mvp starton a dev box prints nothing and serves normally.Fixed paths, fixed forever
<root>/wg/hub.conf,<root>/wg/identity.key,<root>/wg/identity.pub, where<root>is [DATA_ENV] or/data— the SAME names the ISO shape uses on P3 and the container shape uses on its volume (one dialect, both shapes; the filenames are constants in the shared crate). Deliberately NOT$HOLGER_DATA: that is the registry data root, movable per install; the ceremony's directory is the durable identity root the volume/partition mounts, and overloading one env with both meanings would let a config change silently relocate the identity.Fail loud, never half-up
- Corrupt/missing-permission identity → refuse BY NAME (the shared crate's error), nonzero exit — the appliance never invents an identity.
hub.confpresent but the tunnel cannot come up (no/dev/net/tun, noCAP_NET_ADMIN) →wg-appliance tunnel: FAILED — …naming the missing precondition, nonzero exit. A declared hub that is not reachable is a broken appliance, not a warning.- No
hub.conf→ identity-only ceremony (print the key, serve). That is first-boot reality: the operator needs the public key OUT before the hub can be configured at all.Named remainder (recorded, not invented): no interface-address writer. The shared crate deliberately carries none (see its crate doc) and holger …
const PRODUCT: & str · src/appliance.rs:51
The product name in the canonical identity line.
const GATE_ENV: & str · src/appliance.rs:53
The gate: the ceremony runs iff this env is 1.
const DATA_ENV: & str · src/appliance.rs:55
Overrides the durable root (default [DEFAULT_ROOT]) — the test seam.
const DEFAULT_ROOT: & str · src/appliance.rs:58
The durable identity root: P3's mountpoint on metal, the volume in a container. wg/ under it holds the ceremony files.
const INTERFACE: & str · src/appliance.rs:60
The tun interface name, same on every shape.
struct Appliance { public_b64: String, minted: bool, tunnel: Option<TunnelHandle> } · src/appliance.rs:64
What the ceremony did — the caller emits testmatrix rows from this and holds the guard for the life of the serve loop.
fn gate_enabled() -> bool · src/appliance.rs:77
Is the appliance gate on? (Exact 1, nothing fuzzy: HOLGER_APPLIANCE=true is a config typo, and a typo that half-enables an identity ceremony is worse than one that visibly does nothing.)
fn run_ceremony() -> Result<Appliance, String> · src/appliance.rs:92
Run the ceremony (the gate is the CALLER's check, so a gated-off run prints nothing at all). Prints the MINTED line (first boot only), then the ONE canonical identity line, then either brings the declared tunnel up or says exactly why it cannot. Every error string names the thing to go look at.
Module holger_server_cli::secure_update
Secure Update Gateway offline operations: verified bundle import (with anti-rollback), integrity scrub, backup/restore, and audit-segment verify.
All of these reuse the network-free
holger-update-bundlecrate. Import and scrub key off the target RON config + a raw repository name (whoseron_data_diris the served bundle store); backup packages that store into a single verifiable znippy archive.
struct BundleArgs { op: BundleOp } · src/secure_update.rs:26
enum BundleOp { Import } · src/secure_update.rs:32
struct ImportArgs { config: PathBuf, repo: String, bundle: PathBuf, policy: PathBuf, target_product: Option<String>, target_model: Option<String>, target_revision: Option<String> } · src/secure_update.rs:39
struct ScrubArgs { config: PathBuf, repo: String, policy: PathBuf } · src/secure_update.rs:61
struct BackupArgs { op: BackupOp } · src/secure_update.rs:72
enum BackupOp { Create, Verify, Restore } · src/secure_update.rs:78
struct BackupCreateArgs { config: PathBuf, repo: String, output: PathBuf } · src/secure_update.rs:90
struct BackupVerifyArgs { input: PathBuf } · src/secure_update.rs:100
struct BackupRestoreArgs { input: PathBuf, data_root: PathBuf } · src/secure_update.rs:106
struct AuditVerifyArgs { op: AuditOp } · src/secure_update.rs:115
enum AuditOp { Verify } · src/secure_update.rs:121
struct AuditVerifyInner { config: PathBuf } · src/secure_update.rs:127
fn run_bundle(a: BundleArgs) -> Result<()> · src/secure_update.rs:185
fn run_import(a: ImportArgs) -> Result<()> · src/secure_update.rs:191
fn run_scrub(a: ScrubArgs) -> Result<()> · src/secure_update.rs:356
fn run_backup(a: BackupArgs) -> Result<()> · src/secure_update.rs:420
fn run_audit(a: AuditVerifyArgs) -> Result<()> · src/secure_update.rs:570
Crate holger-server-lib
[server-core] Wiring engine, 8 gRPC services, HTTP door, auth, audit, promote, retention, replication, raw. Central high-coupling crate.
The holger server crate: turns a RON config into a running artifact server.
Config is a directed graph of three node kinds — [Repository] (a backend plus its upstreams), [ExposedEndpoint] (a gRPC + optional HTTP/OCI gateway), and [StorageEndpoint] (disk or a znippy archive). [wire_holger] resolves that graph in passes: index every node by ron_name, wire forward refs (repo→storage, repo→exposed) and reverse links (exposed→repos), then build a per-endpoint [FastRoutes] table. Any repo with configured upstreams is wrapped in a ProxyBackend at this stage so a 404 in the local archive falls through to the upstream pull-through automatically.
Two entry modes share the same wired graph: call [Holger::fetch]/[Holger::put] in-process, or [Holger::start] to spin up the tonic gRPC services (and HTTP gateways) fronted by shared auth ([auth]) and one append-only audit sink ([audit]). Boot is deliberately fault-tolerant — a single bad archive is disabled and logged (see [Holger::instantiate_backends]) rather than sinking the whole server; only an all-repos failure hard-fails.
Gotcha: the wiring uses raw *const pointers between nodes, so the graph is valid only while the owning Vecs stay put — never mutate or reallocate repositories/exposed_endpoints/storage_endpoints after wire_holger. Related seams: ron_url/ron_http_url parse as bare SocketAddrs, so any scheme'd URL must go through bare_socket_addr first (see [dev_pair_ron]).
Module holger_server_lib
const GIT_HASH: & str · src/lib.rs:104
Short git commit hash of the build tree, stamped by build.rs (pure-Rust, no shelling — reads .git/HEAD directly). "unknown" when built without git metadata (e.g. a source tarball).
const VERSION_LINE: & str · src/lib.rs:110
The single-source holger version identity: holger v<CARGO_PKG_VERSION> (<git-hash>). Every version surface — the server bins' --version/-V, the gRPC + in-process health.version, and any About/identity readout — prints THIS exact string so they can never drift. Built at compile time (concat!).
fn wire_holger(holger: &mut Holger) -> Result<()> · src/lib.rs:134
enum SecurityProfile { Standard, Critical } · src/lib.rs:280
Server security posture. Standard keeps the existing availability-oriented semantics (open reads, best-effort audit). Critical is opt-in and fail-closed: startup refuses any configuration that cannot enforce the profile (see [Holger::validate_profile]).
enum OperationMode { ReadWrite, ReadOnly } · src/lib.rs:291
Whether the node accepts network mutations at all. ReadWrite is the existing behavior; ReadOnly rejects every network PUT/POST/PATCH/DELETE and every mutating gRPC call before the request body is collected, at the auth choke points, in addition to the existing write-gate.
struct Holger { repositories: Vec<Repository>, exposed_endpoints: Vec<ExposedEndpoint>, storage_endpoints: Vec<StorageEndpoint>, auth: Option<auth::AuthConfig>, audit: AuditSettings, security_profile: SecurityProfile, operation_mode: OperationMode, ron_plugin_dir: Option<String> } · src/lib.rs:298
struct AuditSettings { dir: String, enabled: bool } · src/lib.rs:340
Audit-log configuration. On by default; the default backend is the append-only Arrow IPC log ([audit::ArrowIpcAuditLog]).
fn read_ron_config<P: AsRef<Path>>(path: P) -> Result<Holger> · src/lib.rs:476
fn dev_pair_ron(data_dir: &Path, grpc_url: &str, http_addr: &str) -> String · src/lib.rs:514
Autogenerate the RON config for the dev release pair that nornir embeds:
/cache— a writable-znippy primary with acrates-io(rust-remote) upstream → a transparent caching crates.io mirror (write-through fills it)./sparring— a writable-znippy registry → the release rehearsal target;sealfreezes it into a static drift snapshot.
nornir writes this to a file and launches holger start --config <file> as a subprocess (no library linking, no version coupling). cargo speaks the HTTP SPARSE protocol, so it talks to http_addr (the FastRoutes gateway), e.g. sparse+http://<http_addr>/cache/index/; grpc_url is the control plane. Archives live under data_dir. Render the dev release pair config — the registry nornir embeds:
/cache— a writable-znippy primary with acrates-io(rust-remote) upstream → a transparent caching crates.io mirror (write-through fills it)./sparring— a writable-znippy registry → the release rehearsal target;sealfreezes it into a static drift snapshot.
nornir writes this to a file and launches holger start --config <file> as a subprocess (no library linking, no version coupling). cargo speaks the HTTP SPARSE protocol, so it talks to http_addr (the FastRoutes gateway), e.g. sparse+http://<http_addr>/cache/index/; grpc_url is the control plane. Archives live under data_dir.
This pair was the FIRST instance of "provision a repo set", spelled out as a hand-formatted string. It is now [provision::Manifest::dev_pair] rendered through the ONE provisioning renderer (L5), so the pair cannot drift from the general path. The output is asserted byte-identical to what it produced before — nornir launches holger start --config <that file>, which makes it a shipped contract, not an internal detail.
Module holger_server_lib::Holger
fn fetch(&self, repository: &str, id: &traits::ArtifactId) -> Result<Option<Vec<u8>>> · src/lib.rs:522
Fetch an artifact by ID from a named repository
fn put(&self, repository: &str, id: &traits::ArtifactId, data: &[u8]) -> Result<()> · src/lib.rs:527
Store an artifact (write-enabled repos only)
fn list_repositories(&self) -> Vec<& str> · src/lib.rs:535
List all configured repository names
fn search_all(&self, query: &search::SearchQuery) -> search::SearchResults · src/lib.rs:545
Cross-repo artifact SEARCH (parity §H, .nornir/search-design.md) — the in-process entry the CLI's search verb drives. Collects every configured repository's live backend and runs the pure [search::search_repos] engine against query. Repos with no materialized backend (a virtual group, or one disabled on init) are skipped — their members are searched directly. Read-only: it never mutates a backend.
fn replicate_into(&self, dest: &Holger, only: Option<& str>, opts: &replication::ReplicationOptions,) -> replication::ReplicationReport · src/lib.rs:578
Replicate this store's artifacts INTO another holger store (parity §I, .nornir/replication-design.md) — the in-process entry the CLI's replicate --from --to verb drives. Reads only from self (the source) and writes only into dest (the destination); content-id idempotent, so a re-run over an unchanged source is a no-op.
Repos are paired by NAME: a source repo foo replicates into the destination repo foo. A source repo with no same-named writable destination is reported (its artifacts fail closed), never silently dropped. only restricts the run to a single source repo name; None replicates every repo that has a destination match. See [replication::replicate_pairs].
fn resolved_audit_dir(&self) -> std::path::PathBuf · src/lib.rs:621
The configured audit dir, anchored. An ABSOLUTE audit.dir is honoured verbatim (explicit config always wins). A RELATIVE one — including the holger-audit default — resolves against $HOLGER_DATA when set, and only against the process CWD when it is not.
$HOLGER_DATA is the data root the installer already establishes (~/.holger user scope, ~/.holger system scope — see holger-server install, which also sets WorkingDirectory=). An installed server therefore resolves identically either way. What this fixes is the EMBEDDED/uninstalled case: holger running in-process inside dwarves or nornir inherits their CWD, so a CWD-relative default scattered a fresh audit log, job ledger, PROD store, SBOM and property store into whatever directory the host happened to start in.
fn sbom_store(&self) -> sbom::SharedSbomStore · src/lib.rs:641
The hosted-SBOM store (<data_root>/holger-sboms). Derived once from the config so every surface — the SbomService gRPC RPCs, the GET /-/sbom HTTP door, and the holger-server sbom CLI verb — reads and writes the same on-disk store. See [sbom::SbomStore], .nornir/sbom-hosting-design.md.
fn property_store(&self) -> properties::SharedPropertyStore · src/lib.rs:650
The custom-property store (<data_root>/holger-props). Derived once from the config so every surface — the gRPC Search property axis, the GET/PUT /-/properties HTTP door, and the holger-server properties CLI verb — reads and writes the same on-disk store. See [properties::PropertyStore], .nornir/properties-design.md.
fn promotion_engine(&self) -> Result<promote::PromotionEngine> · src/lib.rs:672
The promotion engine (<data_root>/holger-prod-store), gates and all. Derived once from the config so every surface — the gRPC PromotionService.PromoteArtifact RPC and the holger-server promote CLI verb — seals into the SAME immutable PROD store under the SAME policy.
The gates are read from the environment, exactly as serve has always read them, so a local promotion cannot be a way to walk around the boundary the served path enforces:
- Policy gate ([
promote::PolicyGate::from_env]) — OFF unlessHOLGER_PROMOTE_ENFORCEis set. A configured-but-unreadable advisory feed is anErrhere (fail-closed): serve refuses to start and the CLI refuses to promote, rather than waving the artifact through unscanned. - Property requirement (
HOLGER_PROMOTE_REQUIRE_PROPERTY=key=value, or a barekey) — OFF unless set.
No job ledger is attached here: that is serve-side observability which the server layers on with [promote::PromotionEngine::with_job_ledger].
fn set_artifact_property(&self, repo: &str, id: &traits::ArtifactId, key: &str, values: Vec<String>,) -> std::io::Result<properties::PropertyMap> · src/lib.rs:696
Set (or, with empty values, remove) one custom-property key on the artifact coordinate (repo, id) — the write side the HTTP door + CLI drive. Returns the resulting full property map. Last-wins per key; other keys are untouched.
fn get_artifact_properties(&self, repo: &str, id: &traits::ArtifactId,) -> properties::PropertyMap · src/lib.rs:708
All custom properties attached to (repo, id) (empty when none) — the read side the HTTP door + CLI drive.
fn plan_repo_retention(&self, repository: &str, policy: &retention::RetentionPolicy, now: i64,) -> Result<retention::RetentionPlan> · src/lib.rs:728
Plan retention/GC for one repository: gather its listing (backend.list()) as [retention::ArtifactRecord]s and run the pure [retention::plan_retention] planner against policy at now (unix seconds).
This is a planner only — it computes which artifacts a retention policy would keep vs reclaim; it deletes nothing (holger's znippy archives are immutable, so carrying a plan out is an archive-set drop delegated to a jera job). Backends that can't enumerate return an empty listing, so the plan is empty rather than an error. created_at is left None (the listing carries no per-artifact ingest time today), so keep_last_n / max_total_bytes apply by version + size while max_age_secs no-ops — see .nornir/retention-design.md.
fn execute_repo_retention(&self, repository: &str, plan: &retention::RetentionPlan, opts: &retention::GcOptions, audit: &dyn audit::AuditLog, jobs: Option<& Arc<jera::jobs::JobStore>>,) -> Result<retention::GcReport> · src/lib.rs:783
Carry out a [retention::RetentionPlan] against one repository — the executor to plan_repo_retention's planner. The planner decided what to reclaim; this acts on that decision and historizes the outcome.
Gated + safe:
opts.dry_rundefaults totrue— a preview reclaims nothing. An actual drop only happens when the caller passes [retention::GcOptions::execute].- A record the plan kept is never touched (only
decision.deleterows are considered), and — defense in depth against a hand-built or corrupted plan — any delete row still carrying aProtectedLatest/MinPerNameFloorreason is refused (skipped), so a protected/latest generation can never be dropped through this path. - Every delete is content-id verified: the executor reads the stored bytes, computes their SHA-256, and hands it to the backend, which re-verifies against what is on disk before it unlinks — a store that changed under the plan is refused, never deleted blind.
Historized: each actual reclaim is appended to audit as an [audit::AuditAction::Delete] event (identity holger-retention-gc, the reclaimed byte count, the plan's reasons as detail) — the append-only Arrow warehouse record of what the GC took. When a jera job ledger is attached (jobs), the whole run is wrapped in one tracked retention-gc job (running → done|failed + a reclaim summary), reusing the same durable job seam promotion uses.
fn run_repo_retention(&self, repository: &str, policy: &retention::RetentionPolicy, now: i64, opts: &retention::GcOptions, audit: &dyn audit::AuditLog, jobs: Option<& Arc<jera::jobs::JobStore>>,) -> Result<retention::GcReport> · src/lib.rs:986
Plan and execute retention for one repository in a single call — a convenience over plan_repo_retention + execute_repo_retention. Still gated: opts.dry_run decides whether anything is actually reclaimed.
fn audit_log(&self) -> Result<Arc<dyn audit::AuditLog>> · src/lib.rs:1003
Build the shared audit sink from this config (append-only Arrow IPC, or a no-op when audit is disabled) — the same sink the server installs at boot. Public so one-shot tools (the retention GC executor CLI) can historize into the very same warehouse the running server writes.
fn get_repo(&self, name: &str) -> Result<& Arc<dyn RepositoryBackendTrait>> · src/lib.rs:1012
Get a repository backend by name.
Public so one-shot tools drive the very same backends the server serves — the holger-server promote CLI verb resolves its SOURCE repo through this, exactly as the gRPC PromoteArtifact handler does.
fn start(&self) -> Result<()> · src/lib.rs:1026
Start gRPC servers (and optional HTTP gateways) for all configured exposed_endpoints
fn validate_profile(&self) -> Result<()> · src/lib.rs:1309
Validate that the configuration can enforce the requested security profile. Pure (no I/O, no backend state) and fail-closed: in Critical any missing requirement is a hard error; Standard always passes. Never rewrites the operator's config — it only refuses.
fn handler_catalog(&self) -> Result<handlers::HandlerCatalog> · src/lib.rs:1367
The handlers any repository's ron_handler may name: the linked ones, plus every .wasm in ron_plugin_dir when one is configured.
Built ONCE per config, so the directory is read and each module compiled a single time however many repositories name a handler.
fn instantiate_backends(&mut self) -> Result<()> · src/lib.rs:1390
Module holger_server_lib::audit
Append-only audit log.
Records who did what to which artifact from where. The default backend ([
ArrowIpcAuditLog]) persists events as append-only Arrow IPC stream segments on disk — columnar, never rewritten, and directly loadable by any Arrow/Parquet tool for offline review.The [
AuditLog] trait is the swap point: the backend can be replaced with a Parquet writer, a database sink, an in-memory buffer ([MemoryAuditLog], used in tests), or disabled entirely ([NoopAuditLog]) without touching the request path. The server holds anArc<dyn AuditLog>and the default wiring installsArrowIpcAuditLog.
enum AuditAction { Download, Upload, Delete, List, Promote, Other } · src/audit/mod.rs:24
The operation an audit record describes.
struct AuditEvent { ts_nanos: i64, ident: String, action: AuditAction, repo: String, artifact: String, source_ip: String, status: u16, bytes: u64, detail: String } · src/audit/mod.rs:72
One immutable audit record.
The fields are the security-relevant 5 W's: time, identity, action, the artifact + repo it touched, and the client source address — plus the result status and transferred byte count.
fn now_nanos() -> i64 · src/audit/mod.rs:131
Current wall-clock time in nanoseconds since the Unix epoch (0 if the clock is before the epoch, which cannot happen on a sane host).
trait AuditLog · src/audit/mod.rs:141
Pluggable audit sink. Implementations MUST be append-only and thread-safe; the server shares a single Arc<dyn AuditLog> across all request handlers, so record takes &self and is expected to be internally synchronised.
fn default_audit_log(dir: impl AsRef<Path>) -> Result<Arc<dyn AuditLog>, AuditError> · src/audit/mod.rs:154
Construct the default audit backend ([ArrowIpcAuditLog]) writing append-only Arrow IPC segments under dir, as a ready-to-share trait object.
struct NoopAuditLog · src/audit/mod.rs:161
A disabled audit log — records nothing. The default the server falls back to when no audit directory is configured.
struct MemoryAuditLog · src/audit/mod.rs:172
In-memory audit log — keeps every event in a Vec. For tests and for callers that want to inspect/route events themselves.
enum AuditError { Io, Arrow, Other } · src/audit/mod.rs:197
Errors from an audit backend.
Module holger_server_lib::audit::AuditAction
fn as_str(&self) -> &'static str · src/audit/mod.rs:41
Stable lowercase wire string (what gets written to the log column).
fn from_wire(s: &str) -> Self · src/audit/mod.rs:54
Inverse of [AuditAction::as_str]; unknown strings map to Other so a reader never fails on a value written by a newer server.
Module holger_server_lib::audit::AuditError
fn code(&self) -> &'static holger_errcode::ErrCode · src/audit/mod.rs:212
The FROZEN holger-audit-<n> code — the machine half of the dual channel.
Exhaustive on purpose: a new variant without a code is a COMPILE error. An audit-log write failure is the one error class that must never be swallowed silently, so it carries a code an operator can alert on.
fn tagged(&self) -> String · src/audit/mod.rs:221
BOTH channels in one string.
Module holger_server_lib::audit::AuditEvent
fn new(ident: impl Into<String>, action: AuditAction, repo: impl Into<String>, artifact: impl Into<String>, source_ip: impl Into<String>, status: u16, bytes: u64,) -> Self · src/audit/mod.rs:100
Build an event stamped with the current wall-clock time and no detail.
fn with_detail(mut self, detail: impl Into<String>) -> Self · src/audit/mod.rs:123
Attach a detail string (builder style).
Module holger_server_lib::audit::AuditLog
fn record(&self, event: AuditEvent) -> Result<(), AuditError> · src/audit/mod.rs:144
Append one event. Should be durable by the time it returns (the default backend flushes per call).
fn flush(&self) -> Result<(), AuditError> · src/audit/mod.rs:147
Flush any buffered state to durable storage. Default: no-op.
Module holger_server_lib::audit::MemoryAuditLog
fn new() -> Self · src/audit/mod.rs:178
Empty log.
fn events(&self) -> Vec<AuditEvent> · src/audit/mod.rs:183
Snapshot of every recorded event, in insertion order.
Module holger_server_lib::audit::arrow_ipc
Default audit backend: append-only Arrow IPC stream segments.
Each [
ArrowIpcAuditLog] instance opens one fresh segment file under the configured directory and only ever appends record batches to it — the file is never rewritten or truncated, so the log is append-only by construction. A process restart opens a new segment; nothing existing is touched. Reading back ([ArrowIpcAuditLog::read_dir]) replays every segment in the directory.One event = one single-row batch, flushed to the OS on each
record, so a crash loses at most the in-flight call. Audit volume is low relative to artifact serving, so the per-row batch overhead is irrelevant here.
fn audit_schema() -> SchemaRef · src/audit/arrow_ipc.rs:36
The columnar schema for audit records. Field order is the canonical column order used by both the writer and [ArrowIpcAuditLog::read_dir].
struct ArrowIpcAuditLog · src/audit/arrow_ipc.rs:52
Append-only Arrow IPC audit log. Cloneable handles are not provided directly; share via Arc<dyn AuditLog> (see [super::default_audit_log]).
Module holger_server_lib::audit::arrow_ipc::ArrowIpcAuditLog
fn new(dir: impl AsRef<Path>) -> Result<Self, AuditError> · src/audit/arrow_ipc.rs:62
Open a new append-only segment under dir (created if missing).
fn segment_path(&self) -> &Path · src/audit/arrow_ipc.rs:104
Path of the segment this instance is appending to.
fn read_dir(dir: impl AsRef<Path>) -> Result<Vec<AuditEvent>, AuditError> · src/audit/arrow_ipc.rs:126
Replay every audit-*.arrows segment in dir, oldest-named first, returning all events in file then in-file order. Missing dir = empty.
fn read_segment(path: impl AsRef<Path>) -> Result<usize, AuditError> · src/audit/arrow_ipc.rs:201
Strictly read ONE segment file, returning the event count. Unlike read_dir (which tolerates a crash-truncated segment for availability), this errors on an unreadable header or a corrupt/truncated batch — so an offline audit verify flags the bad segment instead of silently skipping it.
Module holger_server_lib::auth
Authentication and authorization for holger-server.
The general auth POLICY / composition layer this module used to own — the RBAC role model ([
Role], [AuthConfig] + itsrole_for/role_for_repo/rbac_enabled/authorize_read/ thewarn_*boot checks), the declarative [AuthMethodConfig] (OIDC / OAuth2 / mTLS / static-token), [AuthIdentity] / [AuthError], and [validate_request] (which builds the enabled, feature-gatednornir_service::AuthProviderset from the config and dispatches a bearer + client-cert CN through it) — was promoted verbatim into the sharednornir-auth-policyedda leaf (suite auth-unify) so korp / dwarves / nornir adopt the SAME login + authorization model. holger is now the reference CONSUMER, not the owner.This module re-exports that public API UNCHANGED, so every holger call site (
grpc.rs,exposed/http.rs) keeps usingcrate::auth::{…}exactly as before — the promotion is zero-behavior-change. The provider feature set is still pinned per tier by holger's ownauth-{bearer,oidc,oauth2,mtls}features, which forward 1:1 into the leaf.
Module holger_server_lib::exposed
Exposed endpoints: the listener side of the wiring graph.
An [
ExposedEndpoint] is a RON-configured serving surface — a gRPC address, optionally a twin HTTP/OCI address (ron_http_url) fronting the same repos for ecosystem clients (helm/pip/cargo, OCI registry) plus/healthz+/readyz. Oneron_tlscovers BOTH ports; absent = cleartext, dev/loopback only. Submodules:http(the gateway),tls(settings/loading),fast_routes(per-endpoint aggregated repo lookup, attached last in the wiring pass).Gotcha:
wired_{in,out}_repositorieshold raw*const Repositoryback-refs and are#[serde(skip)]— they are populated by the wiring engine after the graph is indexed, so they are dangling/empty on a freshly deserialized config and must never outlive theRepositoryarena that backs them.
struct ExposedEndpoint { ron_name: String, ron_url: String, ron_http_url: Option<String>, ron_tls: Option<crate::exposed::tls::TlsSettings>, ron_max_body_bytes: Option<usize>, aggregated_routes: Option<FastRoutes>, wired_in_repositories: Vec<* const Repository>, wired_out_repositories: Vec<* const Repository> } · src/exposed/mod.rs:25
An exposed gRPC endpoint configured in RON
Module holger_server_lib::exposed::fast_routes
Repo-name → backend dispatch table on the gRPC/HTTP hot path.
An
ExposedEndpointfans out to many repos; every request must resolve the repo name to itsRepositoryBackendTraitbeforefetch()/put(). Rather than aHashMapthis uses a first-byte bucketed table: routes are sorted by initial byte (then length), andfirst_byte_index/first_byte_lengive the[start, len)slice for a byte in O(1), solookupscans only same-first-byte names with exact==compare — no hashing, cache-friendly for the small, fixed route sets the wiring engine attaches.Built once and immutable:
FastRoutes::newis called after the graph is wired, and the struct isClone(cheap —Arcbackends) but never mutated. Gotcha: bucketing is purely on the first byte, and empty names fold into byte 0 (unwrap_or(0)), so an empty-string route collides with anything else keyed at index 0 — names are assumed non-empty and unique.
struct FastRoute { name: String, backend: Arc<dyn RepositoryBackendTrait> } · src/exposed/fast_routes.rs:22
struct FastRoutes { routes: Vec<FastRoute>, first_byte_index: [usize; 256], first_byte_len: [usize; 256] } · src/exposed/fast_routes.rs:29
Module holger_server_lib::exposed::fast_routes::FastRoutes
fn new(routes: Vec<(String, Arc<dyn RepositoryBackendTrait>)>) -> Self · src/exposed/fast_routes.rs:36
fn lookup(&self, name: &str) -> Option<& Arc<dyn RepositoryBackendTrait>> · src/exposed/fast_routes.rs:73
fn all_repos(&self) -> Vec<(String, Arc<dyn RepositoryBackendTrait>)> · src/exposed/fast_routes.rs:92
Module holger_server_lib::exposed::http
HTTP/OCI gateway: the one non-gRPC ingress, letting HTTP-only clients (helm, pip, cargo, OCI registry pushes via
oci://…) reach the same repository backends the tonic service ingrpc.rsserves — each request is routed to a backend and dispatched through itshandle_http2_request. [GatewaySettings] carries the shared [FastRoutes], auth config, optional TLS, body cap, and the audit sink into every connection.Security-app rules, deliberately kept in lockstep with the gRPC path: TLS terminates here and mTLS CN is lifted from the handshake once per connection; reads (GET/HEAD) stay open and audit as
anonymous, while writes must passauth::validate_requestthenauthorize_http_write(RBAC mirror ofgrpc::authorize_write, DELETE = admin, other writes = writer-or-admin, fail-closed under a configured policy). Only/healthz,/readyz, and the/v2OCI version probe are unauthenticated and unaudited. Routing keys off the first path segment (/v2/{name}/…for OCI, else/{repo}/…) via [route_key]; backends strip their own prefix from the passed-through path.Gotcha: the read-only write-gate (
backend.is_writable()) and the body-size cap both fire BEFORE the body is collected, so a rejected write never buffers the upload — this is intentional, closing the DoS surface the pre-gate HTTP path left open (it used to hand writes to a read-only backend that silently swallowed them).
struct GatewaySettings { routes: FastRoutes, auth_config: Arc<AuthConfig>, tls: Option<Arc<ServerConfig>>, max_body_bytes: usize, audit: Arc<dyn AuditLog>, operation_mode: crate::OperationMode, security_profile: crate::SecurityProfile, sboms: Option<crate::sbom::SharedSbomStore>, properties: Option<crate::properties::SharedPropertyStore>, quarantine: Option<(String, String)> } · src/exposed/http.rs:65
Everything a gateway connection needs, shared behind an Arc.
fn start_http_gateway(addr: std::net::SocketAddr, settings: Arc<GatewaySettings>,) -> anyhow::Result<()> · src/exposed/http.rs:109
Bind the HTTP/OCI gateway and start its accept loop.
The bind happens synchronously, in the caller's context, so a failure (EADDRINUSE from a second holger or any other process on the port, EACCES on a privileged port under a non-root unit) is returned as an Err and reaches Holger::start() → holger-server start, which exits non-zero.
It used to bind inside the spawned task, where the only way to report failure was an eprintln! and a bare return while this function returned Ok(()) regardless. The process then reported full success — exit status 0, server_started=true, systemd active (running) — and blocked on Ctrl+C while serving nothing. Every client got connection-refused and no automated check ever fired. Only the accept LOOP belongs in the background; readiness is a caller-visible fact.
pub(crate) struct SbomCoordQuery { repository: String, id: traits::ArtifactId } · src/exposed/http.rs:923
The parsed coordinate for the GET /-/sbom door: which repository + artifact (namespace/name/version) an SBOM is requested for. Missing pieces are empty strings (the handler rejects a query lacking repo/name/version).
pub(crate) fn serve_sbom_door(sboms: Option<& crate::sbom::SharedSbomStore>, raw_query: &str,) -> (u16, &'static str, Vec<u8>, String, String) · src/exposed/http.rs:968
Serve the read-only GET /-/sbom door: resolve the coordinate from raw_query, fetch the hosted SBOM from sboms, and return (status, content_type, body, repository, label). Pure over the store it is given, so the HTTP handler and its test share exactly one code path — the byte-identical serving is asserted directly, not through a live socket.
- 400 when
repo/name/versionare not all present. - 200 + the byte-identical CycloneDX document when an SBOM is attached.
- 404 when no SBOM is attached (or hosting is not configured on this server).
- 500 on a store read error.
pub(crate) fn serve_properties_door(props: Option<& crate::properties::SharedPropertyStore>, raw_query: &str,) -> (u16, Vec<u8>, String, String) · src/exposed/http.rs:1005
Serve the read-only GET /-/properties door: resolve the coordinate from raw_query (reusing the SBOM coordinate parser), read its property map from props, and return (status, json_body, repository, label). Pure over the store it is given, so the handler and its test share one code path.
- 400 when
repo/name/versionare not all present. - 200 + the coordinate's
{ key: [values] }JSON ({}when none, or when the store is not configured — an unset coordinate has no properties either way).
pub(crate) fn serve_properties_write(props: Option<& crate::properties::SharedPropertyStore>, method: &str, raw_query: &str,) -> (u16, Vec<u8>, String, String) · src/exposed/http.rs:1042
Serve a PUT/POST/DELETE /-/properties WRITE: parse the coordinate + key (+ repeatable value= for a set) from raw_query, apply it to props, and return (status, json_body, repository, label). Pure over the store, so the handler and its test share one code path (the auth gate runs in the handler, before this).
- 400 when
repo/name/version/keyare not all present. DELETE, orPUT/POSTwith novalue=, removes the key.PUT/POSTwith values sets them (last-wins, deduped).- 200 + the coordinate's resulting
{ key: [values] }JSON. - 503 when no property store is configured on this server.
Module holger_server_lib::exposed::tls
Shared TLS material for an exposed endpoint.
One [
TlsSettings] (loaded from RON) drives both transports of an endpoint: the gRPC server (tonic) and the HTTP/OCI gateway (hyper + rustls). rustls is already in the dependency tree via reqwest, so this adds no OpenSSL surface — consistent with the airgap posture.Client-auth semantics differ by transport on purpose:
- gRPC: when
client_cais set, mTLS is required (clients are the agent / CI, which can always present a cert).- HTTP gateway: client certs are requested but optional, so anonymous pulls and Kubernetes
/healthzprobes still work; write requests are rejected at the application layer unless a valid identity is present.
struct TlsSettings { cert: String, key: String, client_ca: Option<String> } · src/exposed/tls.rs:24
fn ensure_crypto_provider() · src/exposed/tls.rs:68
Install the ring crypto provider as the process default, once.
Both ring and aws-lc-rs end up in the dependency tree (via reqwest), so rustls cannot pick a default on its own and panics on first use. Pinning ring makes every rustls consumer in-process (our servers, reqwest) deterministic.
fn leaf_common_name(chain: &[CertificateDer<'_>]) -> Option<String> · src/exposed/tls.rs:135
Extract the subject Common Name from the leaf of a DER client-cert chain. Used to map an mTLS peer to an identity for [crate::auth::validate_request].
Module holger_server_lib::exposed::tls::TlsSettings
fn mtls_enabled(&self) -> bool · src/exposed/tls.rs:36
fn rustls_gateway_config(&self) -> Result<Arc<ServerConfig>> · src/exposed/tls.rs:81
Build a rustls ServerConfig for the HTTP/OCI gateway. Client certs are requested but not required when client_ca is set; the app layer enforces identity on writes.
fn tonic_config(&self) -> Result<tonic::transport::ServerTlsConfig> · src/exposed/tls.rs:119
Build a tonic ServerTlsConfig for the gRPC server. When client_ca is set, mTLS is required.
Module holger_server_lib::generated::holger.v1
struct ArtifactId { namespace: ::prost::alloc::string::String, name: ::prost::alloc::string::String, version: ::prost::alloc::string::String } · src/generated/holger.v1.rs:6
struct FetchArtifactRequest { repository: ::prost::alloc::string::String, id: ::core::option::Option<ArtifactId> } · src/generated/holger.v1.rs:16
struct FetchArtifactResponse { data: ::prost::alloc::vec::Vec<u8>, content_type: ::prost::alloc::string::String, size_bytes: i64 } · src/generated/holger.v1.rs:23
struct ArtifactChunk { data: ::prost::bytes::Bytes } · src/generated/holger.v1.rs:32
struct ListArtifactsRequest { repository: ::prost::alloc::string::String, name_filter: ::prost::alloc::string::String, limit: i32, page_token: ::prost::alloc::string::String } · src/generated/holger.v1.rs:37
struct ArtifactEntry { id: ::core::option::Option<ArtifactId>, size_bytes: i64, content_type: ::prost::alloc::string::String } · src/generated/holger.v1.rs:49
struct ListArtifactsResponse { artifacts: ::prost::alloc::vec::Vec<ArtifactEntry>, next_page_token: ::prost::alloc::string::String } · src/generated/holger.v1.rs:58
struct PutArtifactRequest { repository: ::prost::alloc::string::String, id: ::core::option::Option<ArtifactId>, data: ::prost::alloc::vec::Vec<u8> } · src/generated/holger.v1.rs:65
struct PutArtifactResponse { success: bool, message: ::prost::alloc::string::String } · src/generated/holger.v1.rs:74
struct ListArchiveFilesRequest { repository: ::prost::alloc::string::String, prefix: ::prost::alloc::string::String } · src/generated/holger.v1.rs:81
struct ListArchiveFilesResponse { paths: ::prost::alloc::vec::Vec <::prost::alloc::string::String>, total_files: i64 } · src/generated/holger.v1.rs:89
struct ArchiveInfoRequest { repository: ::prost::alloc::string::String } · src/generated/holger.v1.rs:96
struct ArchiveInfoResponse { file_count: i64, total_uncompressed_bytes: i64, archive_path: ::prost::alloc::string::String } · src/generated/holger.v1.rs:101
struct HealthRequest · src/generated/holger.v1.rs:110
struct HealthResponse { status: ::prost::alloc::string::String, version: ::prost::alloc::string::String, uptime_seconds: i64 } · src/generated/holger.v1.rs:112
struct ListRepositoriesRequest · src/generated/holger.v1.rs:122
struct RepositoryInfo { name: ::prost::alloc::string::String, repo_type: ::prost::alloc::string::String, writable: bool, has_archive: bool } · src/generated/holger.v1.rs:124
struct ListRepositoriesResponse { repositories: ::prost::alloc::vec::Vec<RepositoryInfo> } · src/generated/holger.v1.rs:136
struct ServerProfileRequest · src/generated/holger.v1.rs:141
struct PromoteArtifactRequest { from_repo: ::prost::alloc::string::String, to_repo: ::prost::alloc::string::String, artifact: ::core::option::Option<ArtifactId> } · src/generated/holger.v1.rs:143
struct PromoteArtifactResponse { success: bool, message: ::prost::alloc::string::String, content_address: ::prost::alloc::string::String, promoted_by: ::prost::alloc::string::String } · src/generated/holger.v1.rs:154
struct SearchRequest { name: ::prost::alloc::string::String, namespace: ::prost::alloc::string::String, version: ::prost::alloc::string::String, checksum: ::prost::alloc::string::String, path: ::prost::alloc::string::String, repositories: ::prost::alloc::vec::Vec <::prost::alloc::string::String>, limit: i32, properties: ::prost::alloc::vec::Vec<PropertyMatch> } · src/generated/holger.v1.rs:167
struct PropertyMatch { key: ::prost::alloc::string::String, value: ::prost::alloc::string::String } · src/generated/holger.v1.rs:205
One custom-property filter: the artifact must carry key, and — when value is non-empty — that exact value among the key's values (empty value = "the key is present, any value").
struct ArtifactHit { repository: ::prost::alloc::string::String, id: ::core::option::Option<ArtifactId>, size_bytes: i64, content_type: ::prost::alloc::string::String, checksum: ::prost::alloc::string::String } · src/generated/holger.v1.rs:214
One artifact matched by name / coordinate / checksum, tagged with the repo it was found in.
struct PathHit { repository: ::prost::alloc::string::String, path: ::prost::alloc::string::String } · src/generated/holger.v1.rs:230
One raw archive path matched by the path axis, tagged with its repo.
struct SearchResponse { artifacts: ::prost::alloc::vec::Vec<ArtifactHit>, paths: ::prost::alloc::vec::Vec<PathHit>, truncated: bool } · src/generated/holger.v1.rs:237
struct AttachSbomRequest { repository: ::prost::alloc::string::String, artifact: ::core::option::Option<ArtifactId>, document: ::prost::alloc::vec::Vec<u8> } · src/generated/holger.v1.rs:247
struct AttachSbomResponse { success: bool, message: ::prost::alloc::string::String, content_address: ::prost::alloc::string::String } · src/generated/holger.v1.rs:260
struct FetchSbomRequest { repository: ::prost::alloc::string::String, artifact: ::core::option::Option<ArtifactId> } · src/generated/holger.v1.rs:270
struct FetchSbomResponse { document: ::prost::alloc::vec::Vec<u8>, size_bytes: i64, content_address: ::prost::alloc::string::String } · src/generated/holger.v1.rs:277
struct PropertyEntry { key: ::prost::alloc::string::String, values: ::prost::alloc::vec::Vec <::prost::alloc::string::String> } · src/generated/holger.v1.rs:290
One property: a key with its ordered set of values (the map's entry form on the wire, since proto has no first-class ordered multimap here).
struct SetPropertyRequest { repository: ::prost::alloc::string::String, artifact: ::core::option::Option<ArtifactId>, key: ::prost::alloc::string::String, values: ::prost::alloc::vec::Vec <::prost::alloc::string::String> } · src/generated/holger.v1.rs:297
struct SetPropertyResponse { success: bool, message: ::prost::alloc::string::String, properties: ::prost::alloc::vec::Vec<PropertyEntry> } · src/generated/holger.v1.rs:310
struct GetPropertiesRequest { repository: ::prost::alloc::string::String, artifact: ::core::option::Option<ArtifactId> } · src/generated/holger.v1.rs:320
struct GetPropertiesResponse { properties: ::prost::alloc::vec::Vec<PropertyEntry> } · src/generated/holger.v1.rs:327
struct ReplicateRequest { from_repo: ::prost::alloc::string::String, to_repo: ::prost::alloc::string::String, execute: bool, limit: i32 } · src/generated/holger.v1.rs:333
struct ReplicateResponse { examined: i64, copied: i64, skipped: i64, would_copy: i64, failed: i64, copied_bytes: i64, truncated: bool, complete: bool, message: ::prost::alloc::string::String } · src/generated/holger.v1.rs:349
struct WhoAmIRequest · src/generated/holger.v1.rs:378
struct WhoAmIResponse { authenticated: bool, subject: ::prost::alloc::string::String, method: ::prost::alloc::string::String, global_role: ::prost::alloc::string::String, read_gate: bool } · src/generated/holger.v1.rs:380
struct ServerProfileResponse { profile: ::prost::alloc::string::String, read_only: bool, label: ::prost::alloc::string::String, writable_repo_count: i32 } · src/generated/holger.v1.rs:403
Module holger_server_lib::generated::holger.v1::admin_service_client
Generated client implementations.
struct AdminServiceClient · src/generated/holger.v1.rs:771
Health / admin
Module holger_server_lib::generated::holger.v1::admin_service_client::AdminServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:776
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:792
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:796
fn with_interceptor<F>(inner: T, interceptor: F,) -> AdminServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:800
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:824
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:830
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:838
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:846
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn health(&mut self, request: impl tonic::IntoRequest<super::HealthRequest>,) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status> · src/generated/holger.v1.rs:851
Server health check
async fn list_repositories(&mut self, request: impl tonic::IntoRequest<super::ListRepositoriesRequest>,) -> std::result::Result<tonic::Response<super::ListRepositoriesResponse>, tonic::Status,> · src/generated/holger.v1.rs:873
List configured repositories
async fn server_profile(&mut self, request: impl tonic::IntoRequest<super::ServerProfileRequest>,) -> std::result::Result<tonic::Response<super::ServerProfileResponse>, tonic::Status,> · src/generated/holger.v1.rs:900
Report the server's operating profile — static (sealed, read-only, "rigged for silent running") vs dynamic (writable / proxy). Server-reported truth so the operator UI reflects the server, not a local toggle.
async fn who_am_i(&mut self, request: impl tonic::IntoRequest<super::WhoAmIRequest>,) -> std::result::Result<tonic::Response<super::WhoAmIResponse>, tonic::Status> · src/generated/holger.v1.rs:930
Who does the SERVER think the caller is? Resolves the presented credential (Bearer / mTLS CN) through the same validation every gated call uses and reports the resulting identity + effective GLOBAL role — introspection only, no secrets, and NOT an authorization decision: every operation is still checked by its own gate. Anonymous callers get an answer too (the default role), so a UI can explain what "not logged in" means here.
Module holger_server_lib::generated::holger.v1::admin_service_server
Generated server implementations.
trait AdminService · src/generated/holger.v1.rs:2229
Generated trait containing gRPC methods that should be implemented for use with AdminServiceServer.
struct AdminServiceServer · src/generated/holger.v1.rs:2266
Health / admin
const SERVICE_NAME: & str · src/generated/holger.v1.rs:2557
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::admin_service_server::AdminService
async fn health(&self, request: tonic::Request<super::HealthRequest>,) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status> · src/generated/holger.v1.rs:2231
Server health check
async fn list_repositories(&self, request: tonic::Request<super::ListRepositoriesRequest>,) -> std::result::Result<tonic::Response<super::ListRepositoriesResponse>, tonic::Status,> · src/generated/holger.v1.rs:2236
List configured repositories
async fn server_profile(&self, request: tonic::Request<super::ServerProfileRequest>,) -> std::result::Result<tonic::Response<super::ServerProfileResponse>, tonic::Status,> · src/generated/holger.v1.rs:2246
Report the server's operating profile — static (sealed, read-only, "rigged for silent running") vs dynamic (writable / proxy). Server-reported truth so the operator UI reflects the server, not a local toggle.
async fn who_am_i(&self, request: tonic::Request<super::WhoAmIRequest>,) -> std::result::Result<tonic::Response<super::WhoAmIResponse>, tonic::Status> · src/generated/holger.v1.rs:2259
Who does the SERVER think the caller is? Resolves the presented credential (Bearer / mTLS CN) through the same validation every gated call uses and reports the resulting identity + effective GLOBAL role — introspection only, no secrets, and NOT an authorization decision: every operation is still checked by its own gate. Anonymous callers get an answer too (the default role), so a UI can explain what "not logged in" means here.
Module holger_server_lib::generated::holger.v1::admin_service_server::AdminServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:2274
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:2277
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:2286
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2297
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2303
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2311
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2319
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::archive_service_client
Generated client implementations.
struct ArchiveServiceClient · src/generated/holger.v1.rs:627
Archive service — znippy archive operations
Module holger_server_lib::generated::holger.v1::archive_service_client::ArchiveServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:632
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:648
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:652
fn with_interceptor<F>(inner: T, interceptor: F,) -> ArchiveServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:656
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:680
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:686
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:694
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:702
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn list_archive_files(&mut self, request: impl tonic::IntoRequest<super::ListArchiveFilesRequest>,) -> std::result::Result<tonic::Response<super::ListArchiveFilesResponse>, tonic::Status,> · src/generated/holger.v1.rs:707
List files in a znippy archive backing a repository
async fn archive_info(&mut self, request: impl tonic::IntoRequest<super::ArchiveInfoRequest>,) -> std::result::Result<tonic::Response<super::ArchiveInfoResponse>, tonic::Status,> · src/generated/holger.v1.rs:732
Get archive metadata (file count, total size, compression ratio)
Module holger_server_lib::generated::holger.v1::archive_service_server
Generated server implementations.
trait ArchiveService · src/generated/holger.v1.rs:1991
Generated trait containing gRPC methods that should be implemented for use with ArchiveServiceServer.
struct ArchiveServiceServer · src/generated/holger.v1.rs:2011
Archive service — znippy archive operations
const SERVICE_NAME: & str · src/generated/holger.v1.rs:2212
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::archive_service_server::ArchiveService
async fn list_archive_files(&self, request: tonic::Request<super::ListArchiveFilesRequest>,) -> std::result::Result<tonic::Response<super::ListArchiveFilesResponse>, tonic::Status,> · src/generated/holger.v1.rs:1993
List files in a znippy archive backing a repository
async fn archive_info(&self, request: tonic::Request<super::ArchiveInfoRequest>,) -> std::result::Result<tonic::Response<super::ArchiveInfoResponse>, tonic::Status,> · src/generated/holger.v1.rs:2001
Get archive metadata (file count, total size, compression ratio)
Module holger_server_lib::generated::holger.v1::archive_service_server::ArchiveServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:2019
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:2022
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:2031
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2042
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2048
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2056
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2064
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::promotion_service_client
Generated client implementations.
struct PromotionServiceClient · src/generated/holger.v1.rs:969
Promotion service — the DEV → PROD transition (.nornir/promotion-design.md). Promotion moves an artifact from a mutable DEV repository into an immutable, content-addressed, sealed PROD store: the promoted artifact is immutable (re-promote refused) and blake3 content-addressed. Admin-gated, fail-closed.
Module holger_server_lib::generated::holger.v1::promotion_service_client::PromotionServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:974
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:990
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:994
fn with_interceptor<F>(inner: T, interceptor: F,) -> PromotionServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:998
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1022
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1028
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1036
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1044
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn promote_artifact(&mut self, request: impl tonic::IntoRequest<super::PromoteArtifactRequest>,) -> std::result::Result<tonic::Response<super::PromoteArtifactResponse>, tonic::Status,> · src/generated/holger.v1.rs:1049
Promote (seal) one artifact from a DEV repo into an immutable PROD store.
Module holger_server_lib::generated::holger.v1::promotion_service_server
Generated server implementations.
trait PromotionService · src/generated/holger.v1.rs:2574
Generated trait containing gRPC methods that should be implemented for use with PromotionServiceServer.
struct PromotionServiceServer · src/generated/holger.v1.rs:2589
Promotion service — the DEV → PROD transition (.nornir/promotion-design.md). Promotion moves an artifact from a mutable DEV repository into an immutable, content-addressed, sealed PROD store: the promoted artifact is immutable (re-promote refused) and blake3 content-addressed. Admin-gated, fail-closed.
const SERVICE_NAME: & str · src/generated/holger.v1.rs:2745
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::promotion_service_server::PromotionService
async fn promote_artifact(&self, request: tonic::Request<super::PromoteArtifactRequest>,) -> std::result::Result<tonic::Response<super::PromoteArtifactResponse>, tonic::Status,> · src/generated/holger.v1.rs:2576
Promote (seal) one artifact from a DEV repo into an immutable PROD store.
Module holger_server_lib::generated::holger.v1::promotion_service_server::PromotionServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:2597
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:2600
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:2609
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2620
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2626
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2634
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2642
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::property_service_client
Generated client implementations.
struct PropertyServiceClient · src/generated/holger.v1.rs:1366
Property service — set/get custom artifact PROPERTIES (parity §8, .nornir/properties-design.md). A property (key → values) is attached to an artifact coordinate and drives the property search axis (SearchRequest). SetProperty is a WRITE (writer/admin, fail-closed under a configured role policy, scoped to the artifact's repository); GetProperties is an open read, mirrored by the read-only GET /-/properties HTTP door + the holger-server properties CLI verb. All share one on-disk store.
Module holger_server_lib::generated::holger.v1::property_service_client::PropertyServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:1371
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:1387
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:1391
fn with_interceptor<F>(inner: T, interceptor: F,) -> PropertyServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:1395
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1419
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1425
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1433
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1441
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn set_property(&mut self, request: impl tonic::IntoRequest<super::SetPropertyRequest>,) -> std::result::Result<tonic::Response<super::SetPropertyResponse>, tonic::Status,> · src/generated/holger.v1.rs:1447
Set key's values on a coordinate (last-wins per key). An EMPTY values REMOVES the key. Returns the resulting full property map. Write-gated.
async fn get_properties(&mut self, request: impl tonic::IntoRequest<super::GetPropertiesRequest>,) -> std::result::Result<tonic::Response<super::GetPropertiesResponse>, tonic::Status,> · src/generated/holger.v1.rs:1472
Read all properties on a coordinate (empty map when none). Open read.
Module holger_server_lib::generated::holger.v1::property_service_server
Generated server implementations.
trait PropertyService · src/generated/holger.v1.rs:3190
Generated trait containing gRPC methods that should be implemented for use with PropertyServiceServer.
struct PropertyServiceServer · src/generated/holger.v1.rs:3217
Property service — set/get custom artifact PROPERTIES (parity §8, .nornir/properties-design.md). A property (key → values) is attached to an artifact coordinate and drives the property search axis (SearchRequest). SetProperty is a WRITE (writer/admin, fail-closed under a configured role policy, scoped to the artifact's repository); GetProperties is an open read, mirrored by the read-only GET /-/properties HTTP door + the holger-server properties CLI verb. All share one on-disk store.
const SERVICE_NAME: & str · src/generated/holger.v1.rs:3418
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::property_service_server::PropertyService
async fn set_property(&self, request: tonic::Request<super::SetPropertyRequest>,) -> std::result::Result<tonic::Response<super::SetPropertyResponse>, tonic::Status,> · src/generated/holger.v1.rs:3193
Set key's values on a coordinate (last-wins per key). An EMPTY values REMOVES the key. Returns the resulting full property map. Write-gated.
async fn get_properties(&self, request: tonic::Request<super::GetPropertiesRequest>,) -> std::result::Result<tonic::Response<super::GetPropertiesResponse>, tonic::Status,> · src/generated/holger.v1.rs:3201
Read all properties on a coordinate (empty map when none). Open read.
Module holger_server_lib::generated::holger.v1::property_service_server::PropertyServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:3225
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:3228
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:3237
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:3248
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:3254
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:3262
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:3270
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::replication_service_client
Generated client implementations.
struct ReplicationServiceClient · src/generated/holger.v1.rs:1519
Replication service — copy a repository's artifacts from a SOURCE into a DESTINATION (parity §I, .nornir/replication-design.md). Within one running server this replicates every artifact from from_repo into a writable to_repo, CONTENT-ID IDEMPOTENT: an artifact the destination already holds by content id (sha256) is skipped, so a re-run over an unchanged source is a no-op. Reads only from the source, writes only into the destination. Admin- gated on the destination (like promotion), fail-closed. The holger-server replicate --from --to CLI verb drives the same engine across two SEPARATE file-backed stores.
Module holger_server_lib::generated::holger.v1::replication_service_client::ReplicationServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:1524
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:1540
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:1544
fn with_interceptor<F>(inner: T, interceptor: F,) -> ReplicationServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:1548
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1572
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1578
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1586
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1594
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn replicate(&mut self, request: impl tonic::IntoRequest<super::ReplicateRequest>,) -> std::result::Result<tonic::Response<super::ReplicateResponse>, tonic::Status,> · src/generated/holger.v1.rs:1599
Replicate from from_repo into to_repo (dry-run unless execute).
Module holger_server_lib::generated::holger.v1::replication_service_server
Generated server implementations.
trait ReplicationService · src/generated/holger.v1.rs:3435
Generated trait containing gRPC methods that should be implemented for use with ReplicationServiceServer.
struct ReplicationServiceServer · src/generated/holger.v1.rs:3455
Replication service — copy a repository's artifacts from a SOURCE into a DESTINATION (parity §I, .nornir/replication-design.md). Within one running server this replicates every artifact from from_repo into a writable to_repo, CONTENT-ID IDEMPOTENT: an artifact the destination already holds by content id (sha256) is skipped, so a re-run over an unchanged source is a no-op. Reads only from the source, writes only into the destination. Admin- gated on the destination (like promotion), fail-closed. The holger-server replicate --from --to CLI verb drives the same engine across two SEPARATE file-backed stores.
const SERVICE_NAME: & str · src/generated/holger.v1.rs:3610
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::replication_service_server::ReplicationService
async fn replicate(&self, request: tonic::Request<super::ReplicateRequest>,) -> std::result::Result<tonic::Response<super::ReplicateResponse>, tonic::Status,> · src/generated/holger.v1.rs:3437
Replicate from from_repo into to_repo (dry-run unless execute).
Module holger_server_lib::generated::holger.v1::replication_service_server::ReplicationServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:3463
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:3466
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:3475
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:3486
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:3492
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:3500
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:3508
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::repository_service_client
Generated client implementations.
struct RepositoryServiceClient · src/generated/holger.v1.rs:431
Repository service — query and manage artifacts
Module holger_server_lib::generated::holger.v1::repository_service_client::RepositoryServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:436
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:452
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:456
fn with_interceptor<F>(inner: T, interceptor: F,) -> RepositoryServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:460
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:484
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:490
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:498
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:506
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn fetch_artifact(&mut self, request: impl tonic::IntoRequest<super::FetchArtifactRequest>,) -> std::result::Result<tonic::Response<super::FetchArtifactResponse>, tonic::Status,> · src/generated/holger.v1.rs:511
Fetch a single artifact by ID
async fn list_artifacts(&mut self, request: impl tonic::IntoRequest<super::ListArtifactsRequest>,) -> std::result::Result<tonic::Response<super::ListArtifactsResponse>, tonic::Status,> · src/generated/holger.v1.rs:536
List artifacts in a repository (with optional filter)
async fn put_artifact(&mut self, request: impl tonic::IntoRequest<super::PutArtifactRequest>,) -> std::result::Result<tonic::Response<super::PutArtifactResponse>, tonic::Status,> · src/generated/holger.v1.rs:561
Upload an artifact (write-enabled repos only)
async fn stream_artifact(&mut self, request: impl tonic::IntoRequest<super::FetchArtifactRequest>,) -> std::result::Result<tonic::Response<tonic::codec::Streaming<super::ArtifactChunk>>, tonic::Status,> · src/generated/holger.v1.rs:586
Stream download for large artifacts
Module holger_server_lib::generated::holger.v1::repository_service_server
Generated server implementations.
trait RepositoryService · src/generated/holger.v1.rs:1637
Generated trait containing gRPC methods that should be implemented for use with RepositoryServiceServer.
struct RepositoryServiceServer · src/generated/holger.v1.rs:1679
Repository service — query and manage artifacts
const SERVICE_NAME: & str · src/generated/holger.v1.rs:1974
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::repository_service_server::RepositoryService
async fn fetch_artifact(&self, request: tonic::Request<super::FetchArtifactRequest>,) -> std::result::Result<tonic::Response<super::FetchArtifactResponse>, tonic::Status,> · src/generated/holger.v1.rs:1639
Fetch a single artifact by ID
async fn list_artifacts(&self, request: tonic::Request<super::ListArtifactsRequest>,) -> std::result::Result<tonic::Response<super::ListArtifactsResponse>, tonic::Status,> · src/generated/holger.v1.rs:1647
List artifacts in a repository (with optional filter)
async fn put_artifact(&self, request: tonic::Request<super::PutArtifactRequest>,) -> std::result::Result<tonic::Response<super::PutArtifactResponse>, tonic::Status,> · src/generated/holger.v1.rs:1655
Upload an artifact (write-enabled repos only)
async fn stream_artifact(&self, request: tonic::Request<super::FetchArtifactRequest>,) -> std::result::Result<tonic::Response<Self::StreamArtifactStream>, tonic::Status,> · src/generated/holger.v1.rs:1669
Stream download for large artifacts
Module holger_server_lib::generated::holger.v1::repository_service_server::RepositoryServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:1687
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:1690
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:1699
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1710
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1716
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1724
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1732
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::sbom_service_client
Generated client implementations.
struct SbomServiceClient · src/generated/holger.v1.rs:1216
SBOM service — host + serve SBOM documents as first-class servable artifacts (parity §6/§11, .nornir/sbom-hosting-design.md). An SBOM (CycloneDX 1.5, built from modgunn's shared SBOM model) is ATTACHED to an artifact coordinate (repository + namespace/name/version) and served back byte-identical. Attach is a write (writer/admin, fail-closed under a configured role policy); fetch is an open read, also exposed as a read-only HTTP door (GET /-/sbom).
Module holger_server_lib::generated::holger.v1::sbom_service_client::SbomServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:1221
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:1237
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:1241
fn with_interceptor<F>(inner: T, interceptor: F,) -> SbomServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:1245
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1269
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1275
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1283
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1291
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn attach_sbom(&mut self, request: impl tonic::IntoRequest<super::AttachSbomRequest>,) -> std::result::Result<tonic::Response<super::AttachSbomResponse>, tonic::Status,> · src/generated/holger.v1.rs:1296
Attach (host) an SBOM document for a coordinate. Write-gated; last-wins.
async fn fetch_sbom(&mut self, request: impl tonic::IntoRequest<super::FetchSbomRequest>,) -> std::result::Result<tonic::Response<super::FetchSbomResponse>, tonic::Status,> · src/generated/holger.v1.rs:1321
Fetch the hosted SBOM document for a coordinate (NOT_FOUND when none).
Module holger_server_lib::generated::holger.v1::sbom_service_server
Generated server implementations.
trait SbomService · src/generated/holger.v1.rs:2948
Generated trait containing gRPC methods that should be implemented for use with SbomServiceServer.
struct SbomServiceServer · src/generated/holger.v1.rs:2973
SBOM service — host + serve SBOM documents as first-class servable artifacts (parity §6/§11, .nornir/sbom-hosting-design.md). An SBOM (CycloneDX 1.5, built from modgunn's shared SBOM model) is ATTACHED to an artifact coordinate (repository + namespace/name/version) and served back byte-identical. Attach is a write (writer/admin, fail-closed under a configured role policy); fetch is an open read, also exposed as a read-only HTTP door (GET /-/sbom).
const SERVICE_NAME: & str · src/generated/holger.v1.rs:3173
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::sbom_service_server::SbomService
async fn attach_sbom(&self, request: tonic::Request<super::AttachSbomRequest>,) -> std::result::Result<tonic::Response<super::AttachSbomResponse>, tonic::Status,> · src/generated/holger.v1.rs:2950
Attach (host) an SBOM document for a coordinate. Write-gated; last-wins.
async fn fetch_sbom(&self, request: tonic::Request<super::FetchSbomRequest>,) -> std::result::Result<tonic::Response<super::FetchSbomResponse>, tonic::Status,> · src/generated/holger.v1.rs:2958
Fetch the hosted SBOM document for a coordinate (NOT_FOUND when none).
Module holger_server_lib::generated::holger.v1::sbom_service_server::SbomServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:2981
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:2984
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:2993
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:3004
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:3010
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:3018
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:3026
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::generated::holger.v1::search_service_client
Generated client implementations.
struct SearchServiceClient · src/generated/holger.v1.rs:1095
Search service — cross-repo artifact discovery (parity §H, .nornir/search-design.md). A thin, read-only aggregate over the backends' list() + archive_files(): find artifacts across every configured repository by name, GAVC-style coordinate (namespace/group · name · version), sha256 content checksum, or a raw archive path substring. Reads are open (anonymous), like the other read RPCs; also exposed as a read-only HTTP door (GET /-/search).
Module holger_server_lib::generated::holger.v1::search_service_client::SearchServiceClient
async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error> where D: TryInto<tonic::transport::Endpoint>, D::Error: Into<StdError>, · src/generated/holger.v1.rs:1100
Attempt to create a new client by connecting to a given endpoint.
fn new(inner: T) -> Self · src/generated/holger.v1.rs:1116
fn with_origin(inner: T, origin: Uri) -> Self · src/generated/holger.v1.rs:1120
fn with_interceptor<F>(inner: T, interceptor: F,) -> SearchServiceClient<InterceptedService<T, F>> where F: tonic::service::Interceptor, T::ResponseBody: Default, T: tonic::codegen::Service<http::Request<tonic::body::Body>, Response = http::Response<<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,>,>,<T as tonic::codegen::Service<http::Request<tonic::body::Body>,>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync, · src/generated/holger.v1.rs:1124
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1148
Compress requests with the given encoding.
This requires the server to support it otherwise it might respond with an error.
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:1154
Enable decompressing responses.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1162
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:1170
Limits the maximum size of an encoded message.
Default: usize::MAX
async fn search(&mut self, request: impl tonic::IntoRequest<super::SearchRequest>,) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> · src/generated/holger.v1.rs:1175
Search artifacts across all (or a named subset of) repositories.
Module holger_server_lib::generated::holger.v1::search_service_server
Generated server implementations.
trait SearchService · src/generated/holger.v1.rs:2762
Generated trait containing gRPC methods that should be implemented for use with SearchServiceServer.
struct SearchServiceServer · src/generated/holger.v1.rs:2776
Search service — cross-repo artifact discovery (parity §H, .nornir/search-design.md). A thin, read-only aggregate over the backends' list() + archive_files(): find artifacts across every configured repository by name, GAVC-style coordinate (namespace/group · name · version), sha256 content checksum, or a raw archive path substring. Reads are open (anonymous), like the other read RPCs; also exposed as a read-only HTTP door (GET /-/search).
const SERVICE_NAME: & str · src/generated/holger.v1.rs:2931
Generated gRPC service name
Module holger_server_lib::generated::holger.v1::search_service_server::SearchService
async fn search(&self, request: tonic::Request<super::SearchRequest>,) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> · src/generated/holger.v1.rs:2764
Search artifacts across all (or a named subset of) repositories.
Module holger_server_lib::generated::holger.v1::search_service_server::SearchServiceServer
fn new(inner: T) -> Self · src/generated/holger.v1.rs:2784
fn from_arc(inner: Arc<T>) -> Self · src/generated/holger.v1.rs:2787
fn with_interceptor<F>(inner: T, interceptor: F,) -> InterceptedService<Self, F> where F: tonic::service::Interceptor, · src/generated/holger.v1.rs:2796
fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2807
Enable decompressing requests with the given encoding.
fn send_compressed(mut self, encoding: CompressionEncoding) -> Self · src/generated/holger.v1.rs:2813
Compress responses with the given encoding, if the client supports it.
fn max_decoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2821
Limits the maximum size of a decoded message.
Default: 4MB
fn max_encoding_message_size(mut self, limit: usize) -> Self · src/generated/holger.v1.rs:2829
Limits the maximum size of an encoded message.
Default: usize::MAX
Module holger_server_lib::group
Virtual group backend: a read-only repository that owns no storage and aggregates an ordered list of MEMBER backends behind a single repo name — the Nexus/Artifactory "group repository". A client hits one coordinate and holger resolves across the members in declaration order (first hit wins), so a team can expose e.g.
maven-all = [maven-releases, maven-snapshots, central-proxy]as one URL.Distinct from
ProxyBackend: a proxy has a WRITABLE primary plus pull-through upstreams (an upstream hit is cached into the primary); a group has no primary, caches nothing, and is always read-only.putis refused.formatis the members' shared format (first member's — groups are homogeneous by construction; an empty group isRaw).
struct GroupBackend · src/group.rs:19
A read-only aggregate over ordered member backends (the "group repo").
Module holger_server_lib::group::GroupBackend
fn new(name: String, members: Vec<Arc<dyn RepositoryBackendTrait>>) -> Self · src/group.rs:29
Build a group named name over members (resolved in order). The group's format is the first member's (members are expected homogeneous); an empty group reports [ArtifactFormat::Raw] and resolves nothing.
Module holger_server_lib::grpc
The tonic gRPC surface — holger's only network API (no REST; the OCI/HTTP gateway in
exposed/http.rsis a separate shim). [HolgerGrpc] is the shared state behind three services impl'd onArc<HolgerGrpc>:RepositoryService(fetch/list/put/stream artifacts),ArchiveService(list/stat the underlying znippy archive), andAdminService(health, repo catalog,ServerProfile). Every RPC resolves the target through [FastRoutes] to a [RepositoryBackendTrait] and appends a best-effort audit line.Trust model, and the gotcha: READS ARE OPEN (anonymous), only writes go through [
HolgerGrpc::authorize_write] (Bearer/mTLS-CN authN, then RBAC write-gate that fails closed once role policy is configured). Because reads are unauthenticated, backend errors must be laundered through [internal_err] — raw errors carry archive/file paths and never reach the client; the detail lives only in the server log and audit sink.ServerProfilereports server-side truth (read-only iff no repo is writable, unless pinned via [HolgerGrpc::with_read_only_profile]). The generated proto is committed (generated/holger.v1.rs,cargo xtask gen-proto), not built at compile time.
const DEFAULT_MAX_BODY_BYTES: usize · src/grpc.rs:41
Default ingest body cap (1 GiB) — matches the HTTP gateway's default (lib.rs), so both surfaces share one configurable knob (ron_max_body_bytes).
pub(crate) fn coded(ec: &'static holger_errcode::ErrCode, grpc: tonic::Code, detail: impl std::fmt::Display,) -> Status · src/grpc.rs:62
THE ONE WRITER of every coded gRPC Status holger returns. Every refusal on the network surface is built here, or it is not coded.
A remote client sees only the Status, so a registry that codes internal error enums and leaves Status::* bare codes nothing that matters (that was SK178: 65 registry codes, 0 of 38 Status sites carrying one). This puts the frozen holger-<subsystem>-<n> on the wire in both channels, matching what nornir, korp and dwarves already do:
- in the message —
[holger-server-1] Repository 'x' not found, so a client that only logsstatus.message()still records the code; and - in the metadata — [
holger_errcode::WIRE_CODE_KEY] (x-holger-code), so a client reacts to the exact verdict structurally without parsing prose.
One helper rather than 38 hand-tagged sites, so a site cannot drift into a slightly-different shape and the message/metadata halves can never disagree. errcode/tests/wiring_guard.rs proves each code really is raised here, and server/lib/tests/grpc_error_codes_on_the_wire.rs proves a real remote client receives it.
pub(crate) fn functional_status(component: &str, check: &str, ok: bool, detail: &str) · src/grpc.rs:109
Introspection / emit marker — record one functional-status row for the nornir test matrix. Wraps nornir_testmatrix::functional_status behind the testmatrix feature (a compiled-out #[inline] no-op otherwise, with no nornir dep in a release build). component is the reporting surface (e.g. "holger-grpc/fetch_artifact"), check what it verified, ok the verdict, detail a short human note. Every gRPC handler + the RBAC write gate emit one so nornir test --features testmatrix SEES each live surface. Mirrors the reference emitter in korp-collectors/src/lib.rs.
struct HolgerGrpc { routes: FastRoutes, start_time: Instant, auth_config: Arc<AuthConfig>, audit: Arc<dyn AuditLog>, max_body_bytes: usize, read_only_profile: Option<bool>, promotion: Option<Arc<crate::promote::PromotionEngine>>, sboms: Option<crate::sbom::SharedSbomStore>, properties: Option<crate::properties::SharedPropertyStore>, quarantine: Option<(String, String)>, operation_mode: crate::OperationMode } · src/grpc.rs:124
Shared state for all gRPC services
Module holger_server_lib::grpc::HolgerGrpc
fn new(routes: FastRoutes) -> Self · src/grpc.rs:171
fn with_auth(routes: FastRoutes, auth_config: Arc<AuthConfig>) -> Self · src/grpc.rs:175
fn with_operation_mode(mut self, mode: crate::OperationMode) -> Self · src/grpc.rs:193
Set the network mutation posture (builder style). ReadOnly fails every write/admin RPC before the body is read.
fn with_promotion(mut self, engine: Arc<crate::promote::PromotionEngine>) -> Self · src/grpc.rs:199
Install the immutable PROD store for the promote workflow (builder style).
fn with_sboms(mut self, store: crate::sbom::SharedSbomStore) -> Self · src/grpc.rs:207
Install the hosted-SBOM store for the SbomService (builder style). The server wires Holger::sbom_store() here so gRPC, HTTP, and the CLI share one on-disk store.
fn with_properties(mut self, store: crate::properties::SharedPropertyStore) -> Self · src/grpc.rs:215
Install the custom-property store powering the Search property axis (builder style). The server wires Holger::property_store() here so gRPC, HTTP, and the CLI share one on-disk store.
fn with_quarantine(mut self, key: String, value: String) -> Self · src/grpc.rs:223
Install the serve-time quarantine predicate (key, value) (builder style). The server wires it from HOLGER_QUARANTINE_PROPERTY; a matching artifact is refused at the fetch boundary. Requires a property store to bite.
pub(crate) fn is_quarantined(&self, repo: &str, id: &traits::ArtifactId) -> bool · src/grpc.rs:232
Whether (repo, id)'s properties match the configured quarantine predicate (so it must NOT be served). false when no quarantine or no store is configured — the shared properties::map_matches rule, so quarantine=true means what it does everywhere else.
fn with_audit(mut self, audit: Arc<dyn AuditLog>) -> Self · src/grpc.rs:243
Install an audit backend (builder style). The server wires the default Arrow-IPC log here when an audit directory is configured.
fn with_max_body_bytes(mut self, max_body_bytes: usize) -> Self · src/grpc.rs:250
Override the ingest body cap (builder style). The server wires the endpoint's ron_max_body_bytes here so gRPC and HTTP share one limit.
fn with_read_only_profile(mut self, read_only: bool) -> Self · src/grpc.rs:259
Declare the server's profile explicitly (builder style). true pins the static "rigged for silent running" / read-only profile, false pins dynamic. Without this the ServerProfile RPC derives the profile from the live route table. A sealed static-holger sets true here.
Module holger_server_lib::grpc::services
The three tonic gRPC service impls on
Arc<HolgerGrpc>—RepositoryService(fetch/list/put/stream),ArchiveService(list/stat the znippy archive), andAdminService(health/catalog/profile). Split out of thegrpcmodule root to keep it focused on the shared state + helpers; as a child module this still reachesHolgerGrpc's private fields/methods and the parent's free helpers (internal_err/peer_addr_string/artifact_label).
pub(crate) fn search_query_from_proto(req: SearchRequest) -> search::SearchQuery · src/grpc/services.rs:35
Build the engine's [search::SearchQuery] from the proto request.
pub(crate) fn trait_id_from_proto(p: &ArtifactId) -> traits::ArtifactId · src/grpc/services.rs:56
Convert a proto [ArtifactId] into the internal [traits::ArtifactId] (an empty namespace string ⇒ None). Shared by the SBOM RPCs.
pub(crate) fn search_results_to_proto(r: search::SearchResults) -> SearchResponse · src/grpc/services.rs:65
Map engine [search::SearchResults] onto the proto response.
Module holger_server_lib::grpc::services::marker_tests
Introspection-marker tests (§B audit wave): every server-side gRPC surface + the RBAC gate must emit a
functional_statusrow — GREEN on the success path, RED (ok=false) on the failure path — with a real detail string. These drive each handler's failure path and assert the RED row actually lands in the nornir-testmatrix buffer, so the marker is red-WHEN-broken, not decorative. Gated behindtestmatrix(the only build where the emit is compiled in).
Module holger_server_lib::handlers
ron_handler— which handler reads a repository's container.
ron_repo_typeandron_upstream_formatname the container.znippyis a container: compressed files in a directory, sometimes with an extra index and extra functionality. A Skidbladnir deploy bundle, a Rust-toolchain dev bundle and a plain sealed.znippyare allArtifactFormat::Znippyand all correct — what the config could not say is who reads the extra index.ron_upstream_format = "znippy" the container ron_handler = "skidbladnir" the reader; empty = generic znippyAdding an
ArtifactFormatvariant per handler was the other way to do this, and it is the wrong level: it would describe the payload as if it were a format, and touchArtifactFormat,RepositoryType,endpoint_name,znippy_type_idand an exhaustivematchin sixteen backend crates. This costs one optional string.What is in the catalog
- the handlers linked into this holger, registered here;
- plus every
.wasminHolger::ron_plugin_dir, when thewasm-pluginsfeature is on.Both kinds register under the name their own manifest declares, so one config string resolves to either without the config knowing which — which is the whole point. A new handler is then a string plus a file, with no holger rebuild.
fn builtin_catalog() -> anyhow::Result<HandlerCatalog> · src/handlers.rs:47
The handlers compiled into this holger.
Each registers under its own manifest().handler, never a name chosen here: a registrar that passed the name in could mount one handler under another's string, and ron_handler would then mean different things depending on whether it resolved to a linked handler or to a module.
A collision between two builtins is an Err, not a silent replacement — though with the names being handler-crate constants it is a build-time impossibility today, which is exactly why it must not be an unwrap: the third handler is the one that would hit it.
Module holger_server_lib::metrics
Prometheus metrics — a process-global counter/gauge registry backing the read-only
GET /metricsdoor (parity §K, thefull-tier operability gap).Design (deliberately dependency-free): a handful of
AtomicU64counters bumped at the natural call sites the server already flows through — the HTTP/OCI gateway (exposed::http) increments a per-verb request counter and the bytes-served total on every backend request/search/sbom door hit, and the retention executor (Holger::execute_repo_retention) bumps a run counter. There is no scrape-time cost beyond a snapshot of the atomics; the only I/O is the artifact-count gauge, which lists each backend at scrape time (bounded, off the hot path, run underspawn_blockingby the door).Why a process-global (not a threaded handle): the emitting sites live on three unrelated planes (the HTTP door, the gRPC services, the retention executor), and threading one
Arc<Metrics>through all of them would be a large, cross-cutting change for a purely additive observability feature. A singleOnceLock<Metrics>(metrics::global()) lets any site bump the same registry without wiring, and the render path is a pure function over a&Metricssnapshot + a supplied repo→count list, so tests exercise an isolated instance (no global interference) and assert exact counts (RED-when-broken).Output is Prometheus text exposition format 0.0.4: one
# HELP/# TYPEheader per family, then samples. Counter families end in_total.
enum Verb { Get, Put, Delete, Promote, Search, Sbom } · src/metrics.rs:38
The logical verb a request is counted under — the same counter family is fed by BOTH doors: the HTTP/OCI gateway (exposed::http) and the gRPC services (via HolgerGrpc::record_audit), so /metrics reflects the whole registry's traffic, not just the HTTP half. GET/HEAD (and listings) → Get, PUT/POST/PATCH → Put, DELETE → Delete, a DEV→PROD promotion → Promote, plus the two read-only /-/ doors as their own verbs so operators can see search and SBOM traffic distinctly from artifact pulls.
enum ProxyOutcome { PrimaryHit, UpstreamHit, NegcacheHit, Miss, UpstreamError } · src/metrics.rs:60
The outcome of one proxy/group-repo request — the labels of the holger_proxy_requests_total{result="…"} family. A [ProxyBackend] chains a primary with an ordered upstream fallback list; every read exits through exactly ONE of these outcomes, so summing the family gives total proxy traffic and the ratio of primary_hit/upstream_hit to miss is the cache hit-rate an operator watches. Emitted at the clean outcome points in ProxyBackend::fetch / handle_http2_request.
struct Metrics · src/metrics.rs:183
Process-wide registry of registry-activity counters. All fields are plain atomics; construction is free, so tests spin up a throwaway instance while the live server bumps [global].
fn global() -> &'static Metrics · src/metrics.rs:414
The process-global metrics registry every live call site bumps and the GET /metrics door renders. Lazily created on first use.
Module holger_server_lib::metrics::Metrics
fn new() -> Self · src/metrics.rs:203
A fresh, all-zero registry.
fn record_request(&self, verb: Verb, status: u16, bytes: u64) · src/metrics.rs:211
Count one handled request under verb, add its response bytes to the bytes-served total, and, when status >= 500, bump the error counter. This is THE single bump point — every door funnels through it so a verb can never be counted without its bytes/errors also being accounted.
fn record_retention_run(&self) · src/metrics.rs:223
Count one retention-executor invocation (a GC "run", dry-run or not).
fn record_proxy(&self, outcome: ProxyOutcome) · src/metrics.rs:230
Count one proxy/group-repo request under its [ProxyOutcome]. Bumped at the single outcome point in the proxy read path (fetch / handle_http2_request), so every proxy read lands on exactly one result= label.
fn proxy_count(&self, outcome: ProxyOutcome) -> u64 · src/metrics.rs:248
Current value of one proxy outcome's counter (test/observability accessor).
fn observe_request_duration(&self, seconds: f64) · src/metrics.rs:256
Observe one request's handling duration (seconds) in the holger_request_duration_seconds histogram. Called at the HTTP door's per-request exit, alongside the request counter, so latency is measured at the same point traffic is counted.
fn request_duration_count(&self) -> u64 · src/metrics.rs:261
Total number of duration observations (test/observability accessor).
fn request_duration_sum_seconds(&self) -> f64 · src/metrics.rs:266
Sum of all observed durations, in seconds (test/observability accessor).
fn request_count(&self, verb: Verb) -> u64 · src/metrics.rs:285
Current value of one verb's request counter (test/observability accessor).
fn bytes_served(&self) -> u64 · src/metrics.rs:290
Total bytes served across all verbs.
fn errors(&self) -> u64 · src/metrics.rs:295
Total 5xx responses.
fn retention_runs(&self) -> u64 · src/metrics.rs:300
Total retention-executor runs.
fn render_prometheus(&self, repo_counts: &[(String, usize)]) -> String · src/metrics.rs:308
Render the full registry as Prometheus text exposition format 0.0.4. Pure: the per-repo artifact gauge is driven entirely by repo_counts (a (repo_name, artifact_count) list the caller gathers), so this function does no I/O and one code path serves both the live door and the test.
Module holger_server_lib::metrics::ProxyOutcome
fn label(self) -> &'static str · src/metrics.rs:90
The Prometheus result= label value for this outcome.
Module holger_server_lib::metrics::Verb
fn label(self) -> &'static str · src/metrics.rs:103
The Prometheus label value for this verb.
fn from_audit_action(action: AuditAction) -> Option<Verb> · src/metrics.rs:121
The /metrics verb an [AuditAction] is counted under — the ONE audit-action→verb classifier, shared by the gRPC record_audit choke point and the HTTP door (Law #5, no twin). None ⇒ the action is not request traffic and is not counted (e.g. [AuditAction::Other]). Reads (download + listing) fold into Get; a store is Put; delete and promote are their own verbs. (Search/Sbom are bumped explicitly by their dedicated read-only HTTP doors, which don't route through an audit action here.)
Module holger_server_lib::object
HolgerObjectadapters — two transports behind one interface.An external partner holds an
Arc<dyn HolgerObject>(seeholger-traits) and never cares what's behind the vtable, exactly like a Java interface reference. Two concrete implementations live here:
- [
LocalHolger] — in-process: calls the local engine directly (no network, no serialization). Built from the same [FastRoutes] table the gRPC server uses, so it isSend + Sync.- [
RemoteHolger] — remote: forwards every call over gRPC using the tonic-generated client. The language-neutral contract isholger.proto.Pick the transport once at startup; the CLI / UI-backend code is identical either way.
pub(crate) fn paginate_artifacts(repo: &dyn RepositoryBackendTrait, name_filter: Option<& str>, page_size: usize, page_token: Option<& str>,) -> Result<(Vec<ArtifactEntry>, String)> · src/object.rs:42
Paginate a backend listing with an opaque offset cursor.
RepositoryBackendTrait::list makes no stable-order guarantee across calls (a HashMap / read_dir may reorder), so offset paging can't slice the raw listing directly. We fetch the full matching set once, sort it into a stable total order (namespace, name, version), then return the requested window. page_token is the integer offset (empty/None = start); the returned token is the next offset, or empty when the listing is exhausted. page_size == 0 means "everything from the offset" (no further paging).
NB: fetching the whole set per page is O(n) — fine for the archive sizes holger serves today; a cursor pushed into the backend would scale further.
struct LocalHolger · src/object.rs:79
In-process HolgerObject. Resolves repositories through a [FastRoutes] table and calls the backends directly.
struct BearerAuth · src/object.rs:219
A tonic interceptor that stamps authorization: Bearer <token> onto every request when a token is configured. A None token is a no-op, so the same client construction path serves both authenticated and open servers.
struct RemoteHolger · src/object.rs:232
Remote HolgerObject. Forwards every call to a Holger gRPC server using the tonic-generated client. Cheap to clone (the channel is reference-counted).
Module holger_server_lib::object::LocalHolger
fn new(routes: FastRoutes) -> Self · src/object.rs:85
Module holger_server_lib::object::RemoteHolger
async fn connect(endpoint: impl Into<String>) -> Result<Self> · src/object.rs:240
Connect to a Holger gRPC endpoint, e.g. http://127.0.0.1:50051, with no credentials (open servers / read-only browsing).
async fn connect_with_token(endpoint: impl Into<String>, token: impl AsRef<str>,) -> Result<Self> · src/object.rs:248
Connect with an OIDC bearer token injected on every request — the auth path a UI uses for write access (validated server-side by auth::validate_request).
async fn connect_with_tls(endpoint: impl Into<String>, ca_pem: Option<Vec<u8>>, client_identity: Option<(Vec<u8>, Vec<u8>)>, token: Option<String>,) -> Result<Self> · src/object.rs:272
Connect over TLS (and, when a client_identity is supplied, mTLS).
ca_pem— PEM bytes of the CA to verify the server certificate against.Nonefalls back to the platform/webpki roots tonic uses by default.client_identity—(cert_pem, key_pem)for the client certificate that authenticates us to the server.Some(..)is what turns plain TLS into mTLS (the holger CN → role auth path).token— an optional bearer token stamped on every request, exactly as in [RemoteHolger::connect_with_token]; orthogonal to TLS so a server can require either or both.
NOTE: this only establishes the client side. There is no end-to-end mTLS integration test here — exercising it needs generated certs and a TLS server (a manual / mannequin step), so this path is verified to compile only. See the task brief (feature B).
fn from_channel(channel: Channel) -> Self · src/object.rs:293
Build from an already-established channel (no credentials).
Module holger_server_lib::promote
Promotion — the DEV → PROD transition (
.nornir/promotion-design.md).Promotion moves an artifact from a mutable DEV repository into an immutable, content-addressed PROD store. Per the design (§1, §10.1), the thing that moves is the artifact's content address (blake3), and the seal is the commit: once a coordinate is sealed into a PROD repo it is immutable — a re-promote or overwrite is refused. Bytes are stored once, content-addressed, so identical content de-dups.
Scope of THIS slice (faithful minimal reading of the design):
- [
PromotionEngine::seal] performs the storage transition: content-address the bytes (blake3), write the blob under its hash, and create an exclusive ref (the seal) for the(to_repo, artifact)coordinate. The ref's existence is the immutability guard — a second seal of the same coordinate returns [PromoteError::AlreadyPromoted].- Integrity on read ([
PromotionEngine::fetch]) re-verifies the blob's blake3 against the ref, so a tampered blob is detected offline.- The audited unit is [
PromotionRecord] (the design's §1 record, minus the deferred fields).Deferred, per design §6 (noted, not faked): CMS/ed25519 signing of the sealed manifest (the ref records the blake3 content address — the manifest anchor a signature would later cover); generation chaining / deletion-vector overlays (§10.4); the policy gate (§4, currently pass-through — promotion is unconditional here); znippy
seal()of a whole generation (§2). The store is a content-addressed blob+ref tree, which is the immutable, blake3-addressed substrate those build on.
enum Enforcement { Warn, Block } · src/promote/mod.rs:51
How hard the promotion policy gate (design §4) bites at a boundary.
The gate is a seam: when no gate is configured on the engine the promote is unconditional (the default, unconfigured path is unchanged). When one IS configured the enforcement level decides what a failing verdict does.
struct PolicyGate · src/promote/mod.rs:66
The promotion policy gate (design §4) — the CVE / license / provenance check run at the boundary an artifact crosses. It wraps the real modgunn scan seam ([OsvScanner] via [security::scan_listing]'s underlying scanner.scan), NOT the demo_* stub: the caller supplies the scanner (real policy + an imported advisory DB), so nothing demo leaks into the enforcement path. A promote is refused when the verdict is Block and enforcement is [Enforcement::Block].
struct PromotionRecord { artifact: String, from_repo: String, to_repo: String, content_address: String, size: u64, promoted_by: String, promoted_at: i64 } · src/promote/mod.rs:138
The audited outcome of one promotion — the design's §1 PromotionRecord restricted to the fields this slice fills (generation/signature fields are deferred). promoted_at is a unix timestamp supplied by the caller.
enum PromoteError { AlreadyPromoted, PolicyBlocked, Io, Ladder } · src/promote/mod.rs:157
Why a promotion could not be sealed.
pub(crate) fn coord_key(id: &ArtifactId) -> String · src/promote/mod.rs:237
A filesystem-safe key for a (namespace, name, version) coordinate. Path separators and other awkward characters are replaced so the ref lives in a single directory level per repo and can't escape it.
pub(crate) fn sanitize_repo(to_repo: &str) -> String · src/promote/mod.rs:263
A filesystem-safe single path segment for a to_repo name.
to_repo arrives verbatim from an untrusted PromoteArtifactRequest, so it MUST NOT be joined into the PROD store path as-is: a value like "../../../../tmp/pwn" would let a seal climb out of refs/ and write (or a probe read) an arbitrary-location .ref file. This collapses path separators and every other awkward character to _ (path separators can therefore never survive), and neutralises the dot-only escapes (./.., and the empty string), so the result is always exactly one directory level that stays inside refs/. A legitimate repo name (a RON node key like rust-prod) passes through unchanged; the same philosophy already guards the coordinate via [coord_key].
struct PromotionEngine · src/promote/mod.rs:284
The immutable, content-addressed PROD store.
Layout under prod_root:
blobs/<blake3-hex> — the artifact bytes, content-addressed (dedup)
refs/<to_repo>/<coord>.ref — the seal: JSON PromotionRecord (exclusive)
A blob is written idempotently (same content ⇒ same file). A ref is created with create_new, so a coordinate can be sealed exactly once — the seal is the immutability commit and the re-promote guard, race-safe.
Module holger_server_lib::promote::PolicyGate
fn new(scanner: OsvScanner, enforcement: Enforcement) -> Self · src/promote/mod.rs:73
A gate over scanner at the given enforcement level.
fn from_env() -> anyhow::Result<Option<PolicyGate>> · src/promote/mod.rs:88
Build a gate from the environment — the operator config flag — or Ok(None) when promotion enforcement is not configured (the default path, unchanged). Fail-closed on misconfiguration: a configured advisory feed that cannot be read/parsed is an Err (serve refuses to start) rather than a silently-empty gate that would wave promotions through.
HOLGER_PROMOTE_ENFORCE=block|warn(unset /off⇒Ok(None))HOLGER_PROMOTE_ADVISORY_DB= path to an OSV/GHSA/RUSTSEC JSON snapshot (the imported CVE feed); absent ⇒ empty CVE set, license/provenance policy still applies.HOLGER_PROMOTE_DENY_LICENSES= comma-separated SPDX ids to deny.
fn evaluate(&self, ecosystem: &str, id: &ArtifactId) -> (Decision, Vec<String>) · src/promote/mod.rs:122
Evaluate one artifact (ecosystem + coords) against the real scanner. Returns the decision plus human-readable finding reasons (for the audit detail and the refusal message).
Module holger_server_lib::promote::PromoteError
fn code(&self) -> &'static holger_errcode::ErrCode · src/promote/mod.rs:182
The FROZEN holger-promote-<n> code for this failure — the machine half of the dual channel (Display below is the human half). A Ladder failure DELEGATES to the inner [stages::LadderError::code], so the specific rung verdict survives the wrapping instead of collapsing into one generic code.
Exhaustive on purpose: a new PromoteError variant without a code is a COMPILE error.
fn tagged(&self) -> String · src/promote/mod.rs:192
BOTH channels in one string.
Module holger_server_lib::promote::PromotionEngine
fn new(prod_root: impl Into<PathBuf>) -> Self · src/promote/mod.rs:327
A store rooted at prod_root (created on first seal). No policy gate — promotion is unconditional (the default path; configure one with with_gate to enforce). No job ledger — seals are not recorded (attach one with with_job_ledger).
fn with_required_property(mut self, key: String, value: String) -> Self · src/promote/mod.rs:343
Require a custom property on the source coordinate before a promotion is allowed (property-policy §). Additive: with none set (the default) promotion is unconditional on properties. Reads HOLGER_PROMOTE_REQUIRE_PROPERTY (key=value, or bare key) when built via [require_property_from_env].
fn required_property(&self) -> Option<&(String, String)> · src/promote/mod.rs:349
The configured property requirement, if any ((key, value)).
fn parse_property_requirement(raw: &str) -> Option<(String, String)> · src/promote/mod.rs:357
Parse HOLGER_PROMOTE_REQUIRE_PROPERTY into a (key, value) requirement — the env config surface, mirroring [PolicyGate::from_env]. key=value, or a bare key (any value). Absent/empty ⇒ None (no requirement). Pure over the supplied string so it is testable without touching the environment.
fn property_requirement_met(&self, props: &crate::properties::PropertyMap) -> bool · src/promote/mod.rs:367
Whether a props map satisfies this engine's property requirement. true when no requirement is configured (unconditional). The promote boundary passes the SOURCE coordinate's property map; the match rule is the shared properties::map_matches — so a signed=true requirement means exactly what the search axis / retention pin mean (no twin).
fn with_ladder(mut self, name: impl Into<String>, ladder: stages::StageLadder) -> Self · src/promote/mod.rs:381
Promote along a stage ladder (sparring → staging → release), recording every hop in an append-only lineage under <prod_root>/lineage/<name>.
Additive in the strict sense: with no ladder configured (the default) seal judges nothing extra, writes no lineage, and is byte-for-byte the promotion holger has always done. With one configured, an illegal hop is refused BEFORE any blob or ref is written — a rejected promotion leaves the store untouched.
fn ladder(&self) -> &stages::StageLadder · src/promote/mod.rs:389
The configured ladder (empty when there is no stage model).
fn current_stage(&self, id: &ArtifactId) -> Option<String> · src/promote/mod.rs:396
Where id currently sits on the ladder — the highest rung it has reached and not been withdrawn from. None with no ladder configured, or when the artifact has never been promoted.
fn stage_history(&self, id: &ArtifactId) -> Vec<stages::StageEntry> · src/promote/mod.rs:403
How id got where it is: every promotion and every withdrawal, oldest first.
fn withdraw(&self, id: &ArtifactId, stage: &str, actor: &str, at: i64, reason: &str,) -> Result<stages::StageEntry, PromoteError> · src/promote/mod.rs:417
Withdraw id from stage — the reversal.
Appends a Withdrawn entry to the lineage. The sealed blob and its ref are NOT touched: an immutable generation is never rewritten, so an artifact that already crossed an airgap does not become invalid because it was withdrawn on the other side. What changes is current_stage's answer — it falls back to the rung below.
fn gate_check_hop(&self, from_repo: &str, to_repo: &str, ecosystem: &str, id: &ArtifactId,) -> Result<(), PromoteError> · src/promote/mod.rs:439
The policy gate, scoped to the hop. With a ladder configured the gate runs only when the TARGET rung declares itself gated — rehearsal is usually ungated, release never is. With no ladder this is exactly gate_check.
fn with_gate(mut self, gate: PolicyGate) -> Self · src/promote/mod.rs:458
Attach a policy gate so promotions are gated (design §4). Additive: the gate only bites when configured, so existing callers that don't set one keep the unconditional behaviour.
fn with_job_ledger(mut self, store: Arc<jera::jobs::JobStore>) -> Self · src/promote/mod.rs:470
Route every seal through the shared jera job ledger (jera::jobs). Additive: with no ledger attached (the default) a promotion behaves exactly as before and writes no job row; with one attached each seal opens a promote job that transitions running → done (with the sealed content address as its result_ref) or running → failed (carrying the reason), so the ledger holds a record of the server's recent promotions (jera bounds it to the most recent jera::jobs::CAP; the permanent record is the audit log).
fn has_gate(&self) -> bool · src/promote/mod.rs:477
Whether a policy gate is configured (the boundary will scan before it seals).
fn has_job_ledger(&self) -> bool · src/promote/mod.rs:482
Whether a durable job ledger is attached (seals are recorded as jobs).
fn gate_check(&self, ecosystem: &str, id: &ArtifactId) -> Result<(), PromoteError> · src/promote/mod.rs:491
Run the policy gate for id in ecosystem BEFORE a seal. Fail-closed: with a gate at [Enforcement::Block] a Block verdict returns [PromoteError::PolicyBlocked]; Warn records but permits; no gate is a pass-through (Ok). The caller (the gRPC promote handler) invokes this at the boundary and audits the outcome.
fn is_promoted(&self, to_repo: &str, id: &ArtifactId) -> bool · src/promote/mod.rs:519
Whether id is already sealed into to_repo (immutable — a further seal would be refused).
fn seal(&self, from_repo: &str, to_repo: &str, id: &ArtifactId, data: &[u8], promoted_by: &str, promoted_at: i64,) -> Result<PromotionRecord, PromoteError> · src/promote/mod.rs:535
Seal data for id into the immutable PROD store under to_repo.
Content-addresses the bytes (blake3), writes the blob under its hash, then creates the ref exclusively — refusing to overwrite an already-sealed coordinate ([PromoteError::AlreadyPromoted]). Returns the audited record.
When a job ledger is attached (with_job_ledger) the whole seal runs as one tracked jera job: a running row is written before the storage transition and flipped to done (with the content address as result_ref) or failed (with the reason) when it settles. The storage transition itself is byte-for-byte the same either way, so this is purely additive observability over the existing seal.
fn fetch(&self, to_repo: &str, id: &ArtifactId) -> Result<Option<Vec<u8>>, PromoteError> · src/promote/mod.rs:668
Read a promoted artifact's bytes back, re-verifying the blob's blake3 against the sealed ref (integrity on read). None when not promoted; Err on IO or an integrity mismatch (a tampered blob).
Module holger_server_lib::promote::stages
Promotion stages — the ladder an artifact is promoted THROUGH.
holger already had the hard half:
PromotionEngine::sealmoves an artifact into an immutable, content-addressed PROD store by content address, not by rebuilding it, and the ref it writes IS the immutability guard./sparringis the release rehearsal; a green rehearsal becomes a sealed generation by a pointer move. That primitive is right and this module does not replace it.What was missing is the Artifactory-shaped model on top: named stages an artifact travels through —
sparring → staging → release— where each hop is recorded, queryable, gateable, and reversible, and where the ladder itself says which hops are legal.The model
A [
StageLadder] is an ordered list of [Stage]s. A stage is a NAME bound to a REPOSITORY — which is exactly how Artifactory does it and exactly how holger'sseal(from_repo, to_repo, …)already works, so the ladder is a lookup table over the mechanism that exists, not a second mechanism (L5).sparring staging release (rust-sparring) ──▶ (rust-staging) ──▶ (rust-release) rung 0 rung 1 rung 2 [gated] [gated]
- Legal hops — one rung up, by default. A hop that skips a rung, goes backwards, or names a stage that is not on the ladder is refused BEFORE anything is sealed ([
StageLadder::check_hop]). A ladder built withallow_skippingpermits jumps (still forward only) for shops that hotfix straight to release.- Gated per hop — a [
Stage] can declare that arriving at it runs the configured policy gate ([super::PolicyGate]). One scanner, per-rung enforcement: rehearsal is usually ungated, release never is.- Recorded — every hop appends one line to an append-only lineage file.
- Queryable — [`StageLog::current …
struct Stage { name: String, repo: String, gated: bool } · src/promote/stages.rs:71
One rung: a stage NAME bound to the REPOSITORY that holds artifacts at that stage.
enum LadderError { UnknownStage, NotForward, SkipsStages } · src/promote/stages.rs:99
Why a hop is not allowed. Every variant is refused before anything is sealed.
struct Hop { from: Stage, to: Stage } · src/promote/stages.rs:155
A legal hop, resolved against the ladder: the two rungs and whether arriving runs the policy gate.
struct StageLadder · src/promote/stages.rs:169
The ordered ladder of stages an artifact is promoted through.
enum StageOp { Promoted, Withdrawn } · src/promote/stages.rs:254
What happened to an artifact at a stage. Append-only: entries are added, never edited or removed.
struct StageEntry { op: StageOp, stage: String, from_stage: String, content_address: String, actor: String, at: i64, reason: String } · src/promote/stages.rs:265
One append-only line of an artifact's stage lineage.
struct StageLog · src/promote/stages.rs:291
The append-only stage lineage, on disk beside the promotion store.
One file per (ladder, artifact coordinate), JSON-lines. Append-only in the literal sense: every write is OpenOptions::append, nothing is ever rewritten, and a withdrawal is a new line rather than an edit — the same discipline as the audit log, for the same reason.
Module holger_server_lib::promote::stages::Hop
fn is_gated(&self) -> bool · src/promote/stages.rs:162
Does arriving at this hop's target run the configured policy gate?
Module holger_server_lib::promote::stages::LadderError
fn code(&self) -> &'static holger_errcode::ErrCode · src/promote/stages.rs:116
The FROZEN holger-promote-<n> code for this refusal — the machine half of the dual channel (Display below is the human half).
Exhaustive on purpose: a new LadderError variant without a code is a COMPILE error.
fn tagged(&self) -> String · src/promote/stages.rs:125
BOTH channels in one string: [holger-promote-5] 'dev' is not forward of 'prod'.
Module holger_server_lib::promote::stages::Stage
fn new(name: impl Into<String>, repo: impl Into<String>) -> Self · src/promote/stages.rs:87
An ungated rung.
fn gated(mut self) -> Self · src/promote/stages.rs:91
This rung runs the policy gate on arrival.
Module holger_server_lib::promote::stages::StageLadder
fn new(stages: Vec<Stage>) -> Self · src/promote/stages.rs:179
A ladder from rungs in order, bottom (rehearsal) first.
fn allow_skipping(mut self) -> Self · src/promote/stages.rs:184
Permit forward hops that skip rungs (the hotfix-straight-to-release shop).
fn stages(&self) -> &[Stage] · src/promote/stages.rs:190
The rungs, in order.
fn is_empty(&self) -> bool · src/promote/stages.rs:196
Is any ladder configured? An empty ladder means "no stage model" — every promotion is judged exactly as it was before this module existed.
fn position(&self, name: &str) -> Option<usize> · src/promote/stages.rs:201
The rung index of name, if it is on the ladder.
fn stage_of_repo(&self, repo: &str) -> Option<& Stage> · src/promote/stages.rs:207
The stage whose repository is repo. Stages are named by repo in the config, so this is how a seal(from_repo, to_repo, …) call is resolved onto the ladder.
fn stage(&self, name: &str) -> Option<& Stage> · src/promote/stages.rs:212
The stage called name.
fn check_hop(&self, from: &str, to: &str) -> Result<Hop, LadderError> · src/promote/stages.rs:217
Judge a hop between two stage names. Ok(Hop) for a legal one.
fn check_repo_hop(&self, from_repo: &str, to_repo: &str) -> Result<Hop, LadderError> · src/promote/stages.rs:235
Judge a hop expressed as repositories — the form seal already takes. Resolves both onto the ladder, then defers to check_hop.
Module holger_server_lib::promote::stages::StageLog
fn new(root: impl Into<PathBuf>) -> Self · src/promote/stages.rs:298
A lineage log rooted at root (created on first append). Conventionally <prod_root>/lineage.
fn record_promotion(&self, ladder: &str, id: &ArtifactId, hop: &Hop, content_address: &str, actor: &str, at: i64,) -> std::io::Result<StageEntry> · src/promote/stages.rs:325
Record that id arrived at hop.to from hop.from, carrying content_address. Called AFTER the seal succeeded — the record follows the fact.
fn withdraw(&self, ladder: &str, id: &ArtifactId, stage: &str, actor: &str, at: i64, reason: &str,) -> std::io::Result<StageEntry> · src/promote/stages.rs:354
Withdraw id from stage: append a Withdrawn entry.
Nothing is deleted and nothing sealed is rewritten — the blob and the ref are not touched, and the bytes still verify. What changes is current_stage's answer. An artifact carried across an airgap does not become invalid because it was withdrawn on the other side; it simply stops being the current release.
fn history(&self, ladder: &str, id: &ArtifactId) -> Vec<StageEntry> · src/promote/stages.rs:382
The full, ordered lineage of id on ladder — every promotion and every withdrawal, oldest first. Empty when the artifact has never been promoted.
A malformed line is skipped rather than failing the read: the log is append-only and a torn last write must not make the whole history unreadable (same tolerance as the Arrow-IPC audit replay).
fn current_stage(&self, ladder: &StageLadder, ladder_name: &str, id: &ArtifactId) -> Option<String> · src/promote/stages.rs:399
Where is this artifact now? The highest rung it has reached and not been withdrawn from, or None if it is not on the ladder at all.
Reduced from the append-only history, never from mutable state: replay the entries in order, note each stage's latest verdict, then take the highest rung still standing. Two people cannot disagree about "where is X" because there is only one record and one reduction of it.
fn is_withdrawn(&self, ladder_name: &str, id: &ArtifactId, stage: &str) -> bool · src/promote/stages.rs:416
Has id been withdrawn from stage (its latest verdict there is a withdrawal)?
Module holger_server_lib::properties
Custom artifact properties — user-defined key→value(s) metadata attached to an artifact coordinate
(repository, namespace/name/version), and the backing store thepropertysearch axis matches against (feature-complete-gaps §8, the last deferred search axis).This is the Nexus/Artifactory "properties" capability: tag
(rust-prod, serde 1.0.0)withenv=prod,team=core,reviewed=yes, then find every artifact carryingenv=prod. A key may hold multiple values (a set), soenv=prodandenv=stagingcan coexist underenv.Storage. A flat on-disk store rooted at
<data_root>/holger-props, laid outholger-props/<repo>/<coord>.props.json, each file a JSON object{ "<key>": ["<value>", …], … }. The repo name and the coordinate are each reduced to ONE path-safe segment by promote's [sanitize_repo] / [coord_key] (reused, not twinned — Law #5), so an untrusted coordinate off the wire can never write or read outsideroot. Writes are last-wins per key and staged through a temp file + rename so a concurrent reader never sees a half-written document. A missing coordinate reads as no properties, never an error.The store implements [
search::PropertyLookup], so the pure search engine can filter candidates by property without any knowledge of on-disk layout.
type PropertyMap · src/properties.rs:34
A key→values property map for one artifact coordinate. BTreeMap so the serialized JSON is deterministic (stable key order) — a re-write of the same properties is byte-identical.
struct PropertyStore · src/properties.rs:41
The on-disk store of custom artifact properties.
Layout under root: <repo>/<coord>.props.json. Both components are sanitized to exactly one path segment, so a path is always a direct child of root/<repo>/ — traversal is structurally impossible.
type SharedPropertyStore · src/properties.rs:47
Shared handle so every surface (gRPC state, HTTP door, CLI, in-process) reads and writes the one store.
fn map_matches(map: &PropertyMap, key: &str, value: &str) -> bool · src/properties.rs:136
Pure predicate: does the property map carry key (with any value when value is empty, else with value present among its values — exact, case-sensitive)? The single matching rule shared by the search axis, [PropertyStore::matches], and the retention/promotion/quarantine property gates — so a keep=true pin, a signed=true promotion requirement, and a quarantine=true serve block all mean exactly the same thing (Law #5, no twin).
fn parse_predicate(raw: &str) -> Option<(String, String)> · src/properties.rs:147
Parse a key=value (or bare key, = "any value") property predicate from a config/env string. key=value ⇒ (key, value); bare key ⇒ (key, ""); empty, whitespace, or an empty key (=value) ⇒ None. The single predicate parser shared by the promotion requirement + the quarantine gate (Law #5).
Module holger_server_lib::properties::PropertyStore
fn new(root: impl Into<PathBuf>) -> Self · src/properties.rs:53
A store rooted at root (directories are created on first write). The server roots it at <data_root>/holger-props; the CLI derives the same path, so all surfaces share one store.
fn get(&self, repo: &str, id: &ArtifactId) -> PropertyMap · src/properties.rs:67
All properties attached to (repo, id) — an empty map when none are set (a missing or unreadable document reads as no properties, never an error, so a search over a mixed store never sinks on one bad file).
fn set(&self, repo: &str, id: &ArtifactId, key: &str, values: Vec<String>,) -> std::io::Result<PropertyMap> · src/properties.rs:78
Set the values of key on (repo, id) (last-wins for that key, other keys untouched). An empty values REMOVES the key. Values are de-duplicated preserving order. Returns the resulting full property map. Atomic (write-temp-then-rename) so a concurrent get never sees a partial doc.
fn matches(&self, repo: &str, id: &ArtifactId, key: &str, value: &str) -> bool · src/properties.rs:125
Whether (repo, id) carries key (with any value when value is empty, else with value present among its values — exact, case-sensitive). This is the predicate the property search axis is built on.
Module holger_server_lib::provision
Provisioning — standing a registry up from a manifest.
holger could already provision two things well: its service (through
skidbladnir::service::{ServiceSpec, Lifecycle}+ensure_system_user, so the systemd unit, the dedicatedholgersystem user and the lifecycle verbs are the shared expert's, not hand-rolled) and one hardcoded repository pair (dev_pair_ron—/cache+/sparring, the registry nornir embeds).Everything between was manual: to stand up a registry with a real repo set you hand-wrote the RON graph. That is the gap this closes. A [
Manifest] is a declarative repo set; [Manifest::to_ron] renders the config graph holger already knows how to wire, and [Manifest::directories] names the storage roots to create.Not a second config format
The manifest renders the RON config graph that already exists — the same
exposed_endpoints/storage_endpoints/repositoriestripleread_ron_configparses andwire_holgerresolves. Nothing here interprets a repository; it only writes the declaration. The output is a plain file an operator can read, diff, commit and hand-edit afterwards, which is the point: provisioning produces the artifact you would otherwise have written, and then gets out of the way.
dev_pair_ronis now one call into thisThe dev pair was the first instance of exactly this shape, spelled out by hand. It is now [
Manifest::dev_pair], anddev_pair_ronrenders it — so there is ONE renderer (L5) and the pair cannot drift from the general path. Its output is asserted byte-identical to the string it produced before (dev_pair_ron_is_byte_identical_through_the_manifest), because nornir launchesholger start --config <that file>and a change there is a change to a shipped contract.
enum RepoKind { Hosted, Upstream, Mirror } · src/provision.rs:39
What a repository IS, in provisioning terms. Each variant renders to a different shape of the same RON repository node.
struct RepoSpec { name: String, format: String, kind: RepoKind } · src/provision.rs:55
One repository to provision.
struct Manifest { endpoint: String, grpc: String, http: String, data_dir: PathBuf, repos: Vec<RepoSpec> } · src/provision.rs:100
A declarative repo set to stand up: the endpoint pair, the data root, and the repositories.
pub(crate) fn bare_socket_addr(url: &str) -> &str · src/provision.rs:237
Reduce a URL-ish string to a bare host:port: strip any scheme:// prefix and any trailing /path. start parses ron_url / ron_http_url as bare SocketAddrs, so a --grpc https://host:port must be normalized or the generated config will not parse.
Module holger_server_lib::provision::Manifest
fn new(endpoint: impl Into<String>, grpc: impl Into<String>, http: impl Into<String>, data_dir: impl Into<PathBuf>,) -> Self · src/provision.rs:114
An empty manifest for data_dir, published on one endpoint.
fn with_repo(mut self, spec: RepoSpec) -> Self · src/provision.rs:130
Add a repository.
fn dev_pair(data_dir: &Path, grpc: &str, http: &str) -> Self · src/provision.rs:140
The dev release pair — the registry nornir embeds, expressed as a manifest: a crates-io upstream, a /cache mirror in front of it, and a /sparring rehearsal repo that also pulls through. This is the definition dev_pair_ron renders, so the pair and the general path are the same code.
fn directories(&self) -> Vec<PathBuf> · src/provision.rs:150
Every directory this manifest needs on disk — one storage root per stored repository. Create these before starting the server; nothing else is required (archives are created by the first write).
fn problems(&self) -> Vec<String> · src/provision.rs:161
Names that would collide once rendered — a repository declared twice, or a mirror naming an upstream that is not in the manifest. Returned rather than panicked so a caller can report every problem at once instead of one per run.
fn to_ron(&self) -> String · src/provision.rs:188
Render the RON config graph. The output is a plain file an operator reads, diffs, commits and hand-edits — read_ron_config parses exactly this.
Module holger_server_lib::provision::RepoSpec
fn hosted(name: impl Into<String>, format: impl Into<String>) -> Self · src/provision.rs:68
A hosted, writable repository.
fn upstream(name: impl Into<String>, format: impl Into<String>) -> Self · src/provision.rs:72
A read-only upstream (the remote a mirror pulls through to).
fn mirror(name: impl Into<String>, format: impl Into<String>, upstreams: impl IntoIterator<Item = impl Into<String>>,) -> Self · src/provision.rs:76
A pull-through cache in front of upstreams.
Module holger_server_lib::proxy
Proxy/group backend: chains a primary [
RepositoryBackendTrait] with an ordered upstream fallback list — the Nexus-style "proxy repo" over pull-through (e.g. a writable-znippy/cachefronting crates.io). Itself aRepositoryBackendTrait, so it slots transparently into FastRoutes wiring wherever a single backend is expected.Flow:
fetchtries primary then each upstream in declaration order, returning the first hit; an upstream hit is written THROUGH into the primary when it is writable, so the next fetch is a local hit.handle_http2_requestmirrors this but keys off status — primary wins unless it 404s, then the first non-404 upstream response is served (else the primary's 404). Everything else (put,list,archive_*,name/format) delegates to the primary alone; upstreams are read-only proxies.Gotcha: the write-through cache fill is strictly best-effort — a
putfailure is logged and swallowed, never surfaced, so a cache-fill error can never fail a read the caller already has bytes for.Gotcha: an upstream that ERRORS (connection refused, DNS failure, TLS error, a non-404 status) is logged, counted as
result="upstream_error", and SKIPPED — the walk continues to the next member, because "first hit wins" is the whole contract and one dead upstream must not blind the server to the reachable ones. Only when no upstream hit AND at least one errored is the failure surfaced, and that case deliberately does NOT populate the negative cache: the coordinate is unknown, not known-absent, and memoizing a transient network fault would keep answering 404 for the whole TTL after the upstream came back.
struct ProxyBackend · src/proxy.rs:52
Wraps a primary repository backend with an ordered upstream fallback chain — the Nexus-style "proxy / group" repo (chain repos in series).
fetch— tries primary, then each upstream in declaration order; returns the first hit found. Caching: when the primary is writable, an upstream hit is written through into the primary (best-effort) so the next fetch is a local hit — a pull-through cache (e.g. a writable-znippy/cacheover crates.io).handle_http2_request— returns the primary's response unless the status is 404, in which case it walks the upstream chain and returns the first non-404 response (or the primary's 404 if all upstreams also 404).put— always delegates to the primary; upstreams are read-only proxies.
Module holger_server_lib::proxy::ProxyBackend
fn new(primary: Arc<dyn RepositoryBackendTrait>, upstreams: Vec<Arc<dyn RepositoryBackendTrait>>,) -> Self · src/proxy.rs:69
A proxy with the negative cache disabled (neg_ttl == 0) — the original constructor, byte-for-byte the prior pull-through behaviour.
fn with_negative_cache_secs(mut self, secs: u64) -> Self · src/proxy.rs:84
Enable negative caching with a secs-second TTL (a secs of 0 leaves it disabled). Additive builder — existing callers that use new are unaffected.
Module holger_server_lib::raw
Generic / raw writable backend: a disk-backed, path-addressed bucket for arbitrary blobs — the Nexus/Artifactory "raw hosted repository" (the misc bucket for tarballs, installers, reports, firmware, anything that has no ecosystem protocol of its own). A client
PUTs bytes at an arbitrary path andGETs them back byte-identical; the path is the address.Deliberately the leanest writable backend: no index synthesis, no metadata generation, no ecosystem parsing — just a safe, content-verifiable loose-file store. Because it implements the full [
RepositoryBackendTrait] it drops into every existing seam unchanged: the checksum-deploy gate verifies uploads at the HTTP door, cross-reposearchfinds blobs by name/path/checksum, the retention/GC executor reclaims them (delete_artifactis the SHA-256-verified safe unlink, agreeing withholger-server-lib::sha256_hex), andreplicationcopies them content-id-idempotently.Coordinate ↔ path bijection (the one rule both entry points obey). The gRPC / in-process side addresses a blob by an [
ArtifactId]; the HTTP side addresses it by a raw path. They meet at one deterministic mapping:
id → relpath= the/-join of the non-empty parts of[namespace, name, version].listreports every stored file asArtifactId { namespace: None, name: <relpath>, version: "" }. soid_to_relpath(listed_id) == <relpath>round-trips exactly — which is why a listing fed back intofetch/delete_artifact(what retention, GC, replication and search all do) resolves to the very same file.Traversal-closed. Every path segment is validated ([
safe_relpath]): an empty segment,.,.., a backslash or a NUL is refused, so no id and no HTTP path can climb out ofbase_dir. (This mirrors the per-segment guard the sibling loose-file backends keep privately —sanitize_repoinpromotecollapses to a *s …
struct RawBackend · src/raw.rs:40
A writable, disk-backed, path-addressed blob store (the "raw hosted repo").
Module holger_server_lib::raw::RawBackend
fn new(name: String, base_dir: PathBuf) -> Self · src/raw.rs:78
Build a raw repo named name, storing blobs under base_dir.
Module holger_server_lib::replication
Repository REPLICATION — copy a repo's artifacts from a SOURCE holger store into a DESTINATION store, content-id idempotent (parity §I,
.nornir/replication-design.md).The engine is a pure function over two backend sets: hand it a list of
(source_backend, dest_backend)repo PAIRS and a [ReplicationOptions], and it walks each source'slist(),fetch()es the bytes, and — for every artifact the destination does not already hold by content id —put()s it into the writable destination. Its only I/O is the backend calls it is given, so it is unit-testable with in-memory backends and reused verbatim by both surfaces: theholger-server replicate --from --toCLI verb (two separate file-backed stores) and theReplicationService.ReplicategRPC RPC (two repos within one running server).Read-from-source / write-to-dest, never the reverse. The source is only ever
list()ed +fetch()ed; the destination is only everfetch()ed (to test presence) +put()(to copy). A source is never mutated.Idempotent by content id. The dedup key is the artifact's content digest (lowercase-hex sha256 of the bytes, the same digest the search engine matches). Before copying, the engine fetches the destination's bytes for the same coordinate: if they are already present AND hash to the same content id, the artifact is skipped. So a second replicate run over an unchanged source is a pure no-op (every artifact skipped, nothing written) — the property the executor test asserts. A coordinate whose destination content DIFFERS is not "present by content id", so it is (re-)copied, converging the destination onto the source.
Gated.
executedefaults tofalse— a dry run reports what would copy and writes nothing; an actual copy only happens under [ReplicationOptions::executing]. A destination backend that is not writable fails that pair's artifacts closed (reported, never silently dropped).
struct ReplicationOptions { execute: bool, limit: usize } · src/replication.rs:51
Knobs for a replication run.
enum Outcome { Copied, Skipped, WouldCopy, Failed } · src/replication.rs:80
What happened to a single source artifact under replication.
struct ReplicatedArtifact { source_repo: String, dest_repo: String, id: ArtifactId, content_id: String, size_bytes: i64, outcome: Outcome, detail: String } · src/replication.rs:94
One source artifact's replication result, tagged with both repo names + its content id.
struct ReplicationReport { items: Vec<ReplicatedArtifact>, truncated: bool } · src/replication.rs:110
The result of a [replicate_pairs] run: one row per source artifact examined, plus derived tallies.
struct RepoPair { source_name: String, source: Arc<dyn RepositoryBackendTrait>, dest_name: String, dest: Arc<dyn RepositoryBackendTrait> } · src/replication.rs:156
One source→destination repository pairing for a replication run. The source is read-only here; the destination is where artifacts land.
fn replicate_pairs(pairs: &[RepoPair], opts: &ReplicationOptions) -> ReplicationReport · src/replication.rs:168
Replicate every pairing in pairs, source→dest, content-id idempotent. Pure over the backends it is given (the only I/O is their list/fetch/put), so both the CLI and the gRPC RPC drive it unchanged. A source that cannot be listed contributes no rows for that pair (best-effort, logged); every artifact that is listed yields exactly one [ReplicatedArtifact] row.
Module holger_server_lib::replication::ReplicationOptions
fn preview() -> Self · src/replication.rs:68
A dry-run preview (writes nothing), unbounded.
fn executing() -> Self · src/replication.rs:72
An executing run (actually copies), unbounded.
Module holger_server_lib::replication::ReplicationReport
fn copied(&self) -> usize · src/replication.rs:121
Artifacts actually written into the destination.
fn skipped(&self) -> usize · src/replication.rs:125
Artifacts already present by content id (skipped).
fn would_copy(&self) -> usize · src/replication.rs:129
Artifacts a dry run would copy.
fn failed(&self) -> usize · src/replication.rs:133
Artifacts that could not be replicated.
fn examined(&self) -> usize · src/replication.rs:137
Total source artifacts examined.
fn is_complete(&self) -> bool · src/replication.rs:141
True when nothing failed — the replication is fully accounted for.
fn copied_bytes(&self) -> i64 · src/replication.rs:145
Sum of bytes actually copied (0 on a dry run).
Module holger_server_lib::repository
The
Repositoryconfig node and its backend factory — the seam between RON config and a liveRepositoryBackendTrait.A
Repositoryis parsed from RON as inertron_*fields; the wiring engine (lib::wire_holger) then calls [Repository::backend_from_config] in a second pass to materializebackend_repository, dispatching onron_repo_typeto the right per-ecosystem backend. The three rust storage flavors are the ones to keep straight:ron_writable_archive= live seal-able znippy registry (the /sparring + /cache primary),ron_archive_path= read-only drift snapshot,ron_data_dir= loose-file dev backend; the*-remotetypes instead build read-only crates.io/Nexus/Artifactory pull-through upstreams. Most ecosystems go through theznippy_backed!macro (archive required); npm/nuget/oci are hand-rolled soron_base_urlcan be threaded into client-facing URLs (packument tarballs, V3 service index).Gotcha:
wired_upstreams/wired_storage/wired_exposedare raw pointers filled in after the graph is built and pinned — they arenull/dangling until wiring completes and must never be followed before then.
struct Repository { ron_name: String, ron_repo_type: String, ron_upstreams: Vec<String>, ron_in: Option<RepositoryIO>, ron_out: Option<RepositoryIO>, ron_members: Vec<String>, ron_archive_path: Option<String>, ron_data_dir: Option<String>, ron_base_url: Option<String>, ron_writable_archive: Option<String>, ron_upstream_repo: Option<String>, ron_upstream_format: Option<String>, ron_handler: Option<String>, ron_upstream_auth: Option<UpstreamAuth>, ron_trusted_roots: Option<String>, ron_negative_cache_secs: Option<u64>, backend_repository: Option<Arc<dyn RepositoryBackendTrait>>, wired_upstreams: Vec<* const Repository>, wired_members: Vec<* const Repository> } · src/repository.rs:92
struct RepositoryIO { ron_storage_endpoint: String, ron_exposed_endpoint: String, wired_storage: * const StorageEndpoint, wired_exposed: * const ExposedEndpoint } · src/repository.rs:575
Module holger_server_lib::repository::Repository
fn backend_from_config(&mut self) -> anyhow::Result<()> · src/repository.rs:219
Materialize this repository's backend using only the handlers linked into this holger. The plugin directory is not read, because a single Repository does not own it — [Holger::instantiate_backends] builds one catalog for the whole config and passes it to [Repository::backend_from_config_with].
fn backend_from_config_with(&mut self, handlers: &holger_plugin_host::HandlerCatalog,) -> anyhow::Result<()> · src/repository.rs:223
Module holger_server_lib::retention
Retention / GC planner — the pure function that decides which artifacts a repository should keep and which it should reclaim.
This is the answer to the Artifactory/Nexus "retention policy" table stakes (
.nornir/feature-complete-gaps.md§4, the highest operability gap), shipped the way the parity design demands: a pure, deterministic function over artifact metadata — no I/O, no scheduler, no deletion. [plan_retention] takes a slice of [ArtifactRecord] plus a [RetentionPolicy] and returns a [RetentionPlan]: a keep/delete verdict per artifact, each annotated with the [RetentionReason]s that produced it, plus the reclaimable-byte total.Why a planner and not a deleter: holger's storage is immutable, blake3-verified znippy archives (retention at the archive-set level, never in-archive edits). The plan is the decision; carrying it out belongs to a jera job that drops whole superseded generations, and the decision is historized to skade — exactly the reuse note in the gap doc. Keeping the planner pure means it is trivially testable, re-runnable on a policy change, and identical whether it drives a
--dry-runCLI preview, a gRPC verb, or the eventual jera executor.Policy dimensions (all optional, all composable)
keep_last_n— keep the N newest versions per name, reclaim the rest.max_age_secs— reclaim anything older than this (needs acreated_at).keep_min_per_name— a floor: never drop below this many versions of a name, even when age would.max_total_bytes— a store-size quota: after the above, evict oldest-first until the kept set fits, honouring the floors.protect_latest_per_name— the single newest version of every name is never reclaimed (defaulttrue, the safety net).Ordering is deterministic end to end (group by
(namespace, name), newest-first within a group bycreated_atthen ve …
struct ArtifactRecord { id: ArtifactId, size_bytes: u64, created_at: Option<i64>, protected_by_property: bool } · src/retention.rs:48
One artifact considered for retention. Pure data — the planner never does I/O; callers gather these from a backend's list() (see Holger::plan_repo_retention) or synthesize them in tests/benches.
struct RetentionPolicy { keep_last_n: Option<usize>, max_age_secs: Option<i64>, keep_min_per_name: Option<usize>, max_total_bytes: Option<u64>, protect_latest_per_name: bool, protect_property: Option<(String, String)> } · src/retention.rs:98
A retention policy: which artifacts to keep. Every dimension is optional and they compose (an artifact is reclaimed if any rule selects it, unless a protection rule saves it). Deserializes straight from RON.
enum RetentionReason { ProtectedLatest, ProtectedByProperty, MinPerNameFloor, WithinKeepLastN, WithinMaxAge, NoPolicyMatched, SupersededByNewer, AgedOut, QuotaEvicted } · src/retention.rs:192
Why the planner kept or reclaimed a record. A kept record carries the protection/keep reasons; a reclaimed one carries the rule(s) that selected it.
struct RetentionDecision { record: ArtifactRecord, delete: bool, reasons: Vec<RetentionReason> } · src/retention.rs:232
The per-artifact verdict: keep or delete, plus the reason(s).
struct RetentionPlan { decisions: Vec<RetentionDecision>, kept_count: usize, delete_count: usize, kept_bytes: u64, reclaimed_bytes: u64, name_count: usize, evaluated_at: i64 } · src/retention.rs:245
A full retention plan: a verdict per input record plus the roll-up totals. The decisions are in a deterministic order (by (namespace, name), newest-first within a name).
struct GcOptions { dry_run: bool } · src/retention.rs:283
How to carry out a [RetentionPlan]. Dry-run by default — nothing is deleted unless dry_run is explicitly set to false. This is the gate: a preview costs nothing and drops nothing; an actual reclaim is an opt-in.
enum GcOutcome { Deleted, WouldDelete, Skipped, Failed } · src/retention.rs:309
What the executor did (or, in a dry-run, would do) with one artifact.
struct GcAction { id: ArtifactId, size_bytes: u64, content_id: Option<String>, outcome: GcOutcome, reasons: Vec<RetentionReason> } · src/retention.rs:338
The executor's per-artifact record — the plan's coordinate plus what became of it and the content id that was verified before any unlink.
struct GcReport { repository: String, dry_run: bool, actions: Vec<GcAction>, deleted_count: usize, reclaimed_bytes: u64, would_delete_count: usize, skipped_count: usize, failed_count: usize, evaluated_at: i64 } · src/retention.rs:353
The result of carrying out (or previewing) a [RetentionPlan] against one repository — the GC record historized to the warehouse/audit plane.
fn plan_retention(records: &[ArtifactRecord], policy: &RetentionPolicy, now: i64,) -> RetentionPlan · src/retention.rs:407
The pure planner. Groups records by (namespace, name), orders each group newest-first, applies the per-name rules (protect_latest, keep_min, keep_last_n, max_age), then the global max_total_bytes quota (oldest-first eviction, honouring the floors), and returns the [RetentionPlan].
Deterministic: the same (records, policy, now) always yields the same plan. Total function: an empty input yields an empty plan; a no-op policy keeps everything.
Module holger_server_lib::retention::ArtifactRecord
fn from_entry(entry: traits::ArtifactEntry) -> Self · src/retention.rs:73
Build a record from a list() entry with no known ingest time. size_bytes is clamped at zero (the proto carries an i64).
fn protected_by_property(mut self, protected: bool) -> Self · src/retention.rs:84
Mark this record as protected by a custom property (builder-style, used by the adapter after a PropertyStore lookup).
Module holger_server_lib::retention::GcOptions
fn dry_run() -> Self · src/retention.rs:297
The safe default: a dry-run that deletes nothing.
fn execute() -> Self · src/retention.rs:302
Opt in to an actual reclaim — the executor will unlink the plan's delete-set (each safely, content-id-verified).
Module holger_server_lib::retention::GcOutcome
fn as_str(&self) -> &'static str · src/retention.rs:325
Short stable slug for CLI/JSON output.
Module holger_server_lib::retention::RetentionPlan
fn to_delete(&self) -> impl Iterator<Item = &ArtifactRecord> · src/retention.rs:264
The records this plan would reclaim, in plan order.
fn to_keep(&self) -> impl Iterator<Item = &ArtifactRecord> · src/retention.rs:268
The records this plan would keep, in plan order.
Module holger_server_lib::retention::RetentionPolicy
fn new() -> Self · src/retention.rs:145
A policy that selects nothing for deletion (the identity plan) — the base for the builder methods below.
fn keep_last(mut self, n: usize) -> Self · src/retention.rs:149
Keep the N newest versions per name.
fn max_age(mut self, secs: i64) -> Self · src/retention.rs:154
Reclaim records older than secs.
fn keep_min(mut self, n: usize) -> Self · src/retention.rs:159
Never drop below n versions per name.
fn quota(mut self, bytes: u64) -> Self · src/retention.rs:164
Cap the kept set at bytes total (oldest-first eviction).
fn protect_latest(mut self, on: bool) -> Self · src/retention.rs:169
Toggle the "never delete the latest version of a name" safety net.
fn protect_property_kv(mut self, key: &str, value: &str) -> Self · src/retention.rs:176
Pin any artifact carrying the custom property (key, value) from reclamation (an empty value = "the key is present, any value"). The adapter resolves this against the PropertyStore.
fn is_noop(&self) -> bool · src/retention.rs:182
True when the policy would never select anything for deletion regardless of input (all bounds unset). The plan is then the pure keep-everything identity.
Module holger_server_lib::retention::RetentionReason
fn as_str(&self) -> &'static str · src/retention.rs:215
Short stable slug for CLI/JSON output.
Module holger_server_lib::sbom
SBOM hosting — attach + serve SBOM documents as first-class servable artifacts (parity §6/§11,
.nornir/sbom-hosting-design.md).Until now SBOMs were only generated (in nornir,
security.rs); holger could not host one. This module lets holger STORE a CycloneDX SBOM document attached to an artifact coordinate(repository, namespace/name/version)and serve it back byte-identical over three surfaces: theSbomServicegRPC RPCs, the read-onlyGET /-/sbomHTTP door, and theholger-server sbomCLI verb — the exact tri-surface shape the cross-repo search verb landed with.Model reuse (Law #5). The SBOM data model is NOT re-declared here — it is CONSUMED from modgunn, the single home of the nordisk SBOM schema. This module re-exports [
SbomComponent] / [SbomInput] / [SbomVuln] / [purl] / [to_cyclonedx] verbatim and adds exactly one thin seam, [build_cyclonedx], that serializes modgunn's emitted document to the bytes the store hosts. A producer that already holds a ready CycloneDX document attaches its raw bytes directly; a producer holding the structured model builds through modgunn here. Either way holger never forks the schema.Storage. A flat on-disk store rooted at
<data_root>/holger-sboms, laid outholger-sboms/<repo>/<coord>.sbom.json. The repo name and the coordinate are reduced to path-safe single segments by promote's [sanitize_repo] / [coord_key] (reused, not twinned), so an untrusted coordinate off the wire can never escape the store root (path-traversal closed). Attach is last-wins (an SBOM may be regenerated + re-attached); a missing coordinate fetches asNone(not-found, never an error).
fn build_cyclonedx(input: &SbomInput) -> Vec<u8> · src/sbom.rs:44
Serialize an SBOM (built from the shared modgunn model) into the bytes the store hosts: a CycloneDX 1.5 JSON document. This is holger's ONE SBOM serialization seam — it emits modgunn::sbom::to_cyclonedx, so the hosted schema is exactly modgunn's and can never drift. A structured producer builds its document through here; a producer holding a ready CycloneDX doc attaches its raw bytes.
struct SbomStore · src/sbom.rs:58
The on-disk store of hosted SBOM documents.
Layout under root:
<repo>/<coord>.sbom.json — the hosted SBOM document bytes (last-wins)
<repo> and <coord> are path-safe single segments (see module docs), so a crafted coordinate can never write or read outside root.
type SharedSbomStore · src/sbom.rs:247
A shared Arc<SbomStore> handle for wiring into the gRPC state + HTTP gateway.
Module holger_server_lib::sbom::SbomStore
fn new(root: impl Into<PathBuf>) -> Self · src/sbom.rs:66
A store rooted at root (directories are created on first attach). The server roots it at <data_root>/holger-sboms; the CLI derives the same path from the config's audit dir, so all surfaces share one store.
fn attach(&self, repo: &str, id: &ArtifactId, document: &[u8]) -> std::io::Result<String> · src/sbom.rs:84
Attach (host) an SBOM document for the coordinate (repo, id). Writes the raw bytes (last-wins: a re-attach overwrites), and returns the blake3 hex content address of the document. The write is staged to a sibling temp file and renamed into place so a concurrent fetch never observes a half-written document.
fn fetch(&self, repo: &str, id: &ArtifactId) -> std::io::Result<Option<Vec<u8>>> · src/sbom.rs:98
Fetch the hosted SBOM document for (repo, id), byte-identical to what was attached. Ok(None) when no SBOM is attached to that coordinate (a missing SBOM is not-found, never an I/O error).
fn has(&self, repo: &str, id: &ArtifactId) -> bool · src/sbom.rs:107
Whether an SBOM is attached to (repo, id) (a cheap existence probe).
Module holger_server_lib::search
Cross-repo artifact SEARCH — a thin, read-only aggregate over every backend's
list()+archive_files()(parity §H,.nornir/search-design.md).The engine is a pure function over a repo list: hand it
&[(name, Arc<dyn RepositoryBackendTrait>)](the FastRoutes table, or the whole config's repositories) and a [SearchQuery], and it returns [SearchResults]. No I/O of its own beyond calling the backends it is given, so it is unit-testable with in-memory mock backends and reused verbatim by all three surfaces: theSearchServicegRPC RPC, the read-onlyGET /-/searchHTTP door, and theholger-server searchCLI verb.Matching axes (all supplied criteria are AND-ed):
name— case-insensitive substring on the artifact name.namespace— case-insensitive substring on the namespace (Maven groupId / npm scope); an artifact with no namespace never matches a namespace query.version— EXACT match on the version (versions are exact tokens).checksum— lowercase-hex sha256 of the artifact CONTENT (case-insensitive). This is the one expensive axis: it fetches + hashes each candidate's bytes, so it only fires when supplied.path— case-insensitive substring on a RAW archive file path; an independent axis that yields [PathHit]s.A bare query (no criteria) lists every artifact, bounded by
limit. A path-only query yields only path hits.
const DEFAULT_SEARCH_LIMIT: usize · src/search.rs:33
Default cap on hits returned per kind when the query leaves limit at 0.
struct SearchQuery { name: Option<String>, namespace: Option<String>, version: Option<String>, checksum: Option<String>, path: Option<String>, properties: Vec<(String, String)>, repositories: Vec<String>, limit: usize } · src/search.rs:38
A cross-repo search. Every field is optional and they compose (AND). An all-empty query lists everything (bounded by limit).
trait PropertyLookup · src/search.rs:70
Resolves the custom properties attached to an artifact coordinate — the seam the property search axis matches against. Implemented by properties::PropertyStore; kept as a trait here so the engine stays pure and unit-testable with an in-memory lookup, never touching on-disk layout.
struct ArtifactHit { repository: String, id: ArtifactId, size_bytes: i64, content_type: String, checksum: Option<String> } · src/search.rs:124
One artifact matched by name / coordinate / checksum, tagged with its repo.
struct PathHit { repository: String, path: String } · src/search.rs:135
One raw archive path matched by the path axis, tagged with its repo.
struct SearchResults { artifacts: Vec<ArtifactHit>, paths: Vec<PathHit>, truncated: bool } · src/search.rs:142
The result of a [search_repos] run.
fn search_repos(repos: &[(String, Arc<dyn RepositoryBackendTrait>)], query: &SearchQuery,) -> SearchResults · src/search.rs:193
Run a cross-repo search. Pure over the repo list it is given — the only I/O is the backend list() / fetch() / archive_files() calls, so it is reused verbatim by the gRPC RPC, the HTTP door, and the CLI. A backend that errors is skipped (best-effort aggregate: one bad archive never sinks the whole search); the skip is logged.
fn search_repos_with_properties(repos: &[(String, Arc<dyn RepositoryBackendTrait>)], query: &SearchQuery, props: Option<& dyn PropertyLookup>,) -> SearchResults · src/search.rs:205
[search_repos] plus the custom-property axis. Identical for a query with no property filters; when query.properties is set, each candidate must also satisfy every (key, value) filter, resolved through props. With property filters set but props == None, no artifact can match them — the property axis fails closed (an unconfigured property store never yields false hits).
Module holger_server_lib::search::PropertyLookup
fn properties_of(&self, repo: &str, id: &ArtifactId) -> std::collections::BTreeMap<String, Vec<String>> · src/search.rs:72
The key→values map attached to (repo, id) (empty when none).
Module holger_server_lib::search::SearchQuery
fn is_empty(&self) -> bool · src/search.rs:96
A completely empty query (matches everything, no repo scoping, default cap).
Module holger_server_lib::security
Móðguðr (modgunn) security integration — Engine A verdicts for holger.
modgunn is the single home of the verdict logic (shared with nornir to break the dependency cycle). This module is only the holger↔modgunn adapter:
- map a holger artifact (a file inside a repository's znippy archive, or an explicit coord) → a [
modgunn::scan::Artifact];- run the lean
OsvScannerverdict (CVE / license / provenance) over a listing;- aggregate the per-artifact [
Verdict]s into a [ScanReport] the Tools#3 Security pane (UI) and theholger-server scanCLI verb both surface.Only modgunn's DEFAULT
scanfeature (Engine A) is pulled — never the heavyscip/warehouseengines. The advisory DB + policy here are a representative offline set for the demo + tests; a real deployment swaps in an imported OSV/GHSA snapshot via [modgunn::osv].
fn ecosystem_for(repo_type: &str) -> String · src/security.rs:32
The modgunn ecosystem label for a holger repository type / format string. Stable, lowercase; used consistently for BOTH the advisory DB and the artifacts so [Advisory::matches] keys line up. Falls back to the raw lower of an unknown string so a custom format still scans (license/provenance checks still apply even with no advisories for that ecosystem).
fn ecosystem_for_format(fmt: ArtifactFormat) -> String · src/security.rs:65
The modgunn ecosystem label for a repository backend's [ArtifactFormat] — the seam the promotion policy gate uses to key its scan (the source repo's format() is known directly, so no repo-type string round-trip is needed).
fn parse_coords(path: &str) -> Option<(String, String)> · src/security.rs:95
Parse (name, version) from an artifact filename/path across ecosystems.
Heuristic but real: takes the last path segment, strips a known archive extension, then splits at the first - that is immediately followed by a digit (the name↔version boundary that holds for cargo .crate, npm .tgz, Python wheels/sdists, Maven .jar/.pom, .gem, .nupkg, …). The version is the run up to the next - (so a wheel's -py3-none-any build tags are dropped). Returns None for metadata files or anything without a coord.
fn artifact_for(ecosystem: &str, path: &str, license: Option<String>, signed: bool,) -> Option<Artifact> · src/security.rs:136
Build a modgunn [Artifact] for a file in ecosystem. Provenance is taken from signed (a holger znippy archive that passed signature verification can pass true); the declared license is optional (filename listings carry none — None). None when the path has no parseable coords.
fn artifact_from_coords(ecosystem: &str, name: &str, version: &str, signed: bool) -> Artifact · src/security.rs:166
Build a modgunn [Artifact] from EXPLICIT coordinates (name/version) in ecosystem. This is the promotion path: the coords are known directly from the [traits::ArtifactId] crossing the boundary, so there is no filename to parse. signed sets provenance (a signature-verified znippy archive passes true). This is the REAL scan seam the promotion policy gate feeds — it uses the caller's configured scanner, never demo_*.
fn demo_advisory_db() -> AdvisoryDb · src/security.rs:188
A representative offline advisory DB for the demo + tests — a handful of well-known advisories across ecosystems so a real corpus surfaces a mix of Pass / Warn / Block. A real deployment replaces this with an imported OSV/ GHSA/RUSTSEC snapshot (modgunn::osv).
fn demo_policy() -> Policy · src/security.rs:234
The demo [Policy]: deny strong copyleft, allow anything else (an empty allow-list ⇒ no "unreviewed" noise), and treat an unstated license as Pass (filename listings carry no license, so unknown must NOT flood Warn). Provenance is Ignore for the demo (synthetic artifacts are unsigned).
fn demo_scanner() -> OsvScanner · src/security.rs:248
The default holger demo scanner (demo policy + demo advisory DB).
struct ScanReport { ecosystem: String, verdicts: Vec<Verdict>, pass: usize, warn: usize, block: usize, skipped: usize } · src/security.rs:256
The aggregated result of scanning a repository listing — the per-artifact [Verdict]s plus Pass/Warn/Block tallies and the overall (strongest) decision. Surfaced as state_json for the Tools#3 pane + asserted headlessly.
fn scan_listing<'a, I>(ecosystem: &str, files: I, scanner: &OsvScanner) -> ScanReport where I: IntoIterator<Item = &'a str>, · src/security.rs:319
Scan a repository files listing in ecosystem with scanner, folding the per-artifact verdicts into a [ScanReport]. Files without parseable coords (index/metadata) are counted as skipped, not scanned. The signature carries over to the modgunn verdict so the report is fully reproducible.
fn advisory_db_from_osv_file(path: &str) -> anyhow::Result<AdvisoryDb> · src/security.rs:350
Import an offline OSV / GHSA / RUSTSEC advisory snapshot from a single JSON file into an [AdvisoryDb] — the REAL feed the promotion policy gate scans against (the airgap-served advisory mirror the design names, §4 v2 / §13-10), replacing the demo_* stub on the enforcement path. Uses modgunn's osv importer (one advisory per vuln×affected-package). A range-only entry (no explicit versions) imports as "all versions" per the importer's contract.
fn promotion_policy(deny_licenses: Vec<String>) -> Policy · src/security.rs:362
A REAL (non-demo) promotion [Policy]: deny the given SPDX license ids, treat an unstated license as Pass (coord listings carry none, so unknown must not flood the gate), and ignore provenance (znippy signature verification is a separate gate). The CVE verdict is driven by the imported advisory DB.
Module holger_server_lib::security::ScanReport
fn decision(&self) -> Decision · src/security.rs:269
The overall decision — the strongest across all verdicts (Block > Warn > Pass); Pass for an empty/clean report.
fn state_json(&self) -> Value · src/security.rs:275
Observable state for the robot/headless tests: counts, overall decision, and the findings (id + severity + summary + coords) — never raw bytes.
Module holger_server_lib::storage
Storage endpoints: the backing-store side of the wiring graph.
A [
StorageEndpoint] is a RON-configured store a repository reads from and writes to — sibling node type toexposed::ExposedEndpoint, addressed byron_nameand resolved bywire_holger.ron_storage_typeselects the backend ("znippy"= immutable content-addressed archive atron_path,"rocksdb"= mutable KV) andron_pathis where it lives on disk.Gotcha: this is still a stub —
backend_from_configis a no-op that opens no store (the live backend hangs offRepository::backend_repository, not here).Unlike the exposed side (whose
wired_out_repositoriesFastRoute table is read on every request), a storage endpoint keeps no back-refs to its repositories: the wiring graph flows repo → storage (forward), and nothing here reads a reverse link. Reversestorage → reposedges lived here once as write-only*const Repositoryvectors that no consumer ever read — a dead unsafe pointer set — and were removed. If the UI lineage graph ever needs symmetric storage→repo edges, derive them from the repositories'ron_in/ron_outrather than reintroducing raw back-ref pointers.
struct StorageEndpoint { ron_name: String, ron_storage_type: String, ron_path: String } · src/storage/mod.rs:24
Module holger_server_lib::storage::StorageEndpoint
fn backend_from_config(&mut self) -> anyhow::Result<()> · src/storage/mod.rs:31
Module holger_server_lib::upstream
Remote-cache upstreams for the server request path — holger's TOP-LEVEL federation FEATURE.
An upstream is a read-only [
RepositoryBackendTrait] that fetches an artifact from an external registry (Artifactory, Nexus, Azure Artifacts, AWS CodeArtifact) so a writable primary can write-through-cache it viaProxyBackend. holger owns the orchestration — which repos proxy which upstreams, pull-through caching, dedup-on-store, and compression via znippy — but NOT the registry-client mechanism: the protocol clients (auth, list, fetch, mirror) live in Skidbladnir'sfederationmodule (the reusable "ship" that sails between the registry/cloud harbours).This whole module is gated behind the
federationfeature (holger'sfulltier). holger's minimal install (mini/embedded) does NOT compile it and does NOT link Skidbladnir or any registry-client dependency — the smallest holger stays clean by construction. See.nornir/holger-tiers.md(Deployment modes) andSkidbladnir/.nornir/upstream-registry-federation.md.
struct SkidbladnirUpstream · src/upstream/mod.rs:77
Adapter: wraps ONE Skidbladnir [UpstreamRegistry] harbour and exposes it as a holger [RepositoryBackendTrait] + [RemoteUpstream] so it drops straight into the existing ProxyBackend pull-through cache — unchanged. All registry I/O is delegated to the ship; this type only translates holger ↔ ship types and keeps holger's config-side auth for the RemoteUpstream::auth() contract.
fn as_backend(up: SkidbladnirUpstream) -> Arc<dyn RepositoryBackendTrait> · src/upstream/mod.rs:268
Convenience: box a [SkidbladnirUpstream] as an Arc<dyn RepositoryBackendTrait> for the wiring engine.
Module holger_server_lib::upstream::SkidbladnirUpstream
fn artifactory(name: impl Into<String>, base_url: impl Into<String>, repo: impl Into<String>, auth: UpstreamAuth, format: ArtifactFormat,) -> Self · src/upstream/mod.rs:89
A read-only proxy to an Artifactory repository (harbour: JFrog Artifactory).
fn nexus(name: impl Into<String>, base_url: impl Into<String>, repo: impl Into<String>, auth: UpstreamAuth, format: ArtifactFormat,) -> Self · src/upstream/mod.rs:107
A read-only proxy to a Nexus hosted/proxy repository (harbour: Sonatype Nexus).
fn azure(name: impl Into<String>, organization: impl Into<String>, repo: impl Into<String>, auth: UpstreamAuth, format: ArtifactFormat,) -> Self · src/upstream/mod.rs:127
A read-only proxy to an Azure Artifacts feed (harbour: Azure DevOps). base_url is the Azure DevOps organization name (e.g. contoso); repo is "{project}/{feed}" or just "{feed}" for an org-scoped feed.
fn aws(name: impl Into<String>, endpoint: impl Into<String>, auth: UpstreamAuth, format: ArtifactFormat,) -> Self · src/upstream/mod.rs:155
A read-only proxy to an AWS CodeArtifact repository (harbour: AWS CodeArtifact). base_url is the full repository endpoint returned by aws codeartifact get-repository-endpoint; the bearer authorization token (from get-authorization-token) is carried in auth ([UpstreamAuth::Bearer]).
fn mirror_into(&self, ids: &[ArtifactId], sink: &mut dyn FnMut(& ArtifactId, Vec<u8>) -> anyhow::Result<()>,) -> anyhow::Result<usize> · src/upstream/mod.rs:177
Mirror a set of artifact coordinates out of the harbour, handing each fetched artifact's bytes to sink (holger's orchestration writes them through into a znippy-backed primary). Returns the number mirrored. This is holger hiring the ship for a voyage.
Module holger_server_lib::upstream_rust
Read-only remote Cargo registry upstream (default: crates.io).
Used as a
ProxyBackendupstream so a writable/cacheprimary pulls + caches crates on demand (a transparent pull-through cache). HTTP is SYNC viaureq— runtime-agnostic, so it is safe both inside holger's async hyper handler (where blocking onreqwestwould panic) and in plain unit tests.
pub(crate) const MAX_DOWNLOAD_BYTES: u64 · src/upstream_rust.rs:20
Hard cap on a single crates.io-mirror download (bytes). A malicious/MITM upstream can otherwise omit/lie about Content-Length and stream an unbounded body into memory, OOMing the server (each fetch runs on a blocking thread). The crates.io mirror is part of holger's core (always compiled), independent of the federation tier — so the cap lives here, not in crate::upstream.
pub(crate) fn agent() -> &'static ureq::Agent · src/upstream_rust.rs:29
Shared ureq agent with bounded timeouts. A naked ureq::get(url).call() has no connect/read timeout, so a hung or slow upstream (a stalled mirror, a black-holed connection) blocks the calling thread forever. Each upstream fetch runs in spawn_blocking, so a handful of stuck calls would pin the blocking pool and starve unrelated repos. Bound connect + read + write so a dead upstream fails fast instead. One agent (connection pool) shared process- wide; built lazily.
struct RustRegistryUpstream · src/upstream_rust.rs:41
A read-only proxy to a remote sparse Cargo registry.
Module holger_server_lib::upstream_rust::RustRegistryUpstream
fn crates_io(name: impl Into<String>) -> Self · src/upstream_rust.rs:51
crates.io defaults: the sparse index + the static CDN.
fn new(name: impl Into<String>, index_base: impl Into<String>, dl_base: impl Into<String>,) -> Self · src/upstream_rust.rs:60
Point at an arbitrary sparse registry (for tests / private mirrors).
Crate holger-server-ui
[ui] facett/egui; thin RemoteHolger or in-process LocalHolger; gui/mini/mikro tiers guarded by tier_ladder.rs.
holger-server-ui — the holger operator/admin desktop UI (egui via facett).
The bin was holger-server-ui until 2026-08-01; it is now named for the component it drives (server/cli = holger builds the daemons it talks to). The PACKAGE is still holger-server-ui — it is published on crates.io.
Usage:
holger-server-ui [ENDPOINT] [--token <TOKEN>] [--ca <CA.pem>] [--cert <CERT.pem> --key <KEY.pem>]
ENDPOINT is the holger gRPC address (default http://127.0.0.1:50051). A bearer token may be supplied with --token <TOKEN> or the HOLGER_TOKEN environment variable (needed for write access on an auth-enabled server).
TLS / mTLS: pass --ca <CA.pem> to verify the server against a custom CA, and --cert <CERT.pem> --key <KEY.pem> to present a client certificate (mTLS — the CN → role auth path). When any of these are present the UI connects over TLS; otherwise it uses the plain (token-only) path. Use an https:// endpoint with TLS.
The UI connects with RemoteHolger; because the view-model is generic over HolgerObject, an embedded build could hand it a LocalHolger instead for direct in-process core calls (no network).
Module holger_server_ui::about
holger's identity for the shared About card — holger's branding/identity data for the constellation-wide JetBrains-style common shell (every suite app — nornir / dwarves / korp / holger — carries an About and a Tools menu).
holger no longer hand-rolls the About egui body: that twin was DELETED and the ONE render body now lives in the shared [
facett_about] widget (LAW #5), which renders a [nornir_appshell::Identity]. This module owns only holger's data — the embedded logo, the [identity] builder, and the headless oracle ([state_json]) — all egui-free, so the lean, display-free default build asserts holger's identity with no display, and theguibuild feeds [identity] tofacett_about::AboutCard.
const LOGO_PNG: & [u8] · src/about.rs:17
The holger+znippy hero logo PNG (the same asset the startup splash embeds), so the About card needs no runtime asset path. Carried into the [Identity] and decoded LFS-tolerantly by the shared widget under gui.
const LOGO_ALT: & str · src/about.rs:22
The accessible name on the About logo image — the AccessKit label a robot queries to prove the holger logo painted in the card. Stable (asserted by the robot test) and a genuine screen-reader description.
const WORDMARK: & str · src/about.rs:25
The wordmark heading shown under the logo (and the text identity a robot asserts).
const TAGLINE: & str · src/about.rs:28
The primary tagline.
const FRAMEWORK: & [& str] · src/about.rs:33
The shared common-framework components holger CONSUMES (never twins) — shown in the About card and reported to the oracle so the "built on the shared framework" claim is observable. Mirrors the suite-wide reuse law (L5).
const VERSION: & str · src/about.rs:42
The holger-server-ui version, presented in the About card + reported to the oracle so a robot asserts holger's identity carries a real version string.
const REPOSITORY: & str · src/about.rs:45
holger's SSH repository (shown on the card via [Identity::repository]).
fn identity() -> Identity · src/about.rs:51
Build holger's shared-shell [Identity] — the data the shared facett_about card renders and the [state_json] oracle dumps. holger maps its FRAMEWORK[] bullets onto [Identity::framework] ("built on the shared nordisk framework"); the embedded logo is carried whole (LFS-tolerant decode is the widget's concern, never a panic).
fn state_json(open: bool) -> serde_json::Value · src/about.rs:66
The headless oracle for the About card — dumped under the app's state_json.about. Delegates to the shared [Identity::state_json] (LAW #5), so holger's identity — wordmark, version, tagline, logo alt-text, has_logo, and the shared framework components it is built on — is asserted by a robot without needing pixels. open reports whether the popup is showing.
Module holger_server_ui::app
egui presentation for the holger UI, built from [facett] components.
One [
UiData] drives everything; each tab issues at most one blocking core call per click (the nornir-viz idiom). The repo list is rendered with afacett::table::Tablefacet; the rest is plain themed egui. Selection + every visible value live in [UiData]'s views, which exposestate_json()— so the same screen can be asserted headlessly.═══════════════════════════════════════════════════════════════════════════ RULE: DO NOT ADD ANOTHER
#[cfg(feature = "mini")]AT A USE SITE. ═══════════════════════════════════════════════════════════════════════════There are already 76 of them across this file (47),
app/scene.rs(23) andapp/command.rs(6), and they went 44 → 76 in a single commit. Every new pane forces a decision at every one of those sites, and the compiler says NOTHING when you miss one — you find out because the pane appears inholger-server-ui-mikro.The two axes are separate, and the mini tier used to have only one of them.
server/ui/Cargo.tomlsaidmini = ["gui"]— it enabledguiand removed NOTHING. Sameeframe,egui,server-lib,modgunn(→skade),world-connector,nornir-auth-client/online. The binaries measured on 2026-07-22 (debug), under the bin names of the day:holger-ui1 048 139 472 B,holger-ui-mikro1 050 137 592 B — the "lean" tier was 2 MB bigger. It trimmed the surface and linked the identical closure.✅ Fixed 2026-07-23. Every heavy dependency is optional now and hangs off the ONE pane that needs it (
about-card·signin·helix·tools·bench·warehouse-scan·demo), and each tier featur …
const HOLGER_UI_VERSION: & str · src/app.rs:145
The holger-server-ui version, shown on the startup splash + in state_json (so the robot can assert a version string is presented). Sourced from the crate's Cargo.toml at compile time.
pub(crate) fn functional_status(component: &str, check: &str, ok: bool, detail: &str) · src/app.rs:155
Introspection / emit marker — record one functional-status row for the nornir test matrix. Wraps nornir_testmatrix::functional_status behind the testmatrix feature (a compiled-out #[inline] no-op otherwise, with no nornir dep in a release build). Each Tools pane's run button emits one so nornir test --features testmatrix SEES each operator surface fire. Mirrors the reference emitter in korp-collectors/src/lib.rs.
const LOOKS: & [(& str, fn() -> LookTheme)] · src/app.rs:177
holger-server-ui's look roster. facett ships OS-specific presets (Windows + macOS, each light+dark) but they are visually interchangeable here, so holger collapses them to just dark and light, plus facett's non-OS device look. Each entry is (switcher label, facett Theme constructor); the index into this slice is the persisted look_preset.
fn look_names() -> Vec<String> · src/app.rs:202
fn default_look_preset() -> usize · src/app.rs:209
The default look for holger-server-ui: dark. facett's from_os resolves every OS to its dark variant, so the collapsed roster's first-launch default is dark. Pure → unit-testable with no window.
pub(crate) enum Tab { Status, Repos, Browse, Archive, Artifact, Graph, Helix, Lineage, Tools, Upload, Search, SignIn, Settings } · src/app.rs:214
pub(crate) const SHELL_PANE: & str · src/app.rs:257
The unique name of the shared-scene ROOT pane — a draggable horizontal master/detail Split: the registry rail (master) beside the tabbed workspace (detail). The Router reports this id as the scene's route, and a robot drags its divider via the render's separators rects.
It used to be the Tabs pane itself — a flat shell with no divider to grab. The tabs moved down to [WORKSPACE_PANE]; this name stays put so every existing route/deep-link that addressed holger-shell still resolves to the shell.
pub(crate) const RAIL_PANE: & str · src/app.rs:263
The registry rail — the master side of holger's master/detail shell, and a foldable DRAWER (it auto-folds crossing into a phone-class viewport and restores above it). Itself a draggable VERTICAL split of the repository roster over the selected repository's cross-tab context.
pub(crate) const RAIL_ROSTER_PANE: & str · src/app.rs:268
The rail's top half: the repository ROSTER (filter + selectable list). Picking a row here is the SAME select_repo every detail tab reads — this is the one master selection, not a rail-local copy.
pub(crate) const RAIL_SELECTION_PANE: & str · src/app.rs:273
The rail's bottom half: what the shell knows about the SELECTED repository across the whole workspace (loaded listing, archive, upload gate) — the detail the Repos table (a row per repo) cannot show.
pub(crate) const WORKSPACE_PANE: & str · src/app.rs:277
The unique name of the tabbed WORKSPACE pane (the detail side): the Tabs layout hosting holger's operator tabs. The [NavState] addresses the active tab by this id.
pub(crate) enum LineageMode { Graph3d, Helix } · src/app.rs:439
How the Lineage tab renders the live cache-fill graph: the rotating spatial FILL (facett-graph3d) or its temporal companion (facett-helix).
pub(crate) enum ToolsFn { HolgerDs, Migrate, Security, Deploy, Airgap, Demo } · src/app.rs:451
The four Tools-menu functions (a mirror of the holger CLI tools).
struct UploadOutcome { attempted: bool, ok: Option<bool>, bytes: Option<u64>, repo: Option<String>, stage: Option<& 'static str>, error: Option<String> } · src/app.rs:475
The structured outcome of the last upload attempt — the write's success or failure as READABLE DATA, not just the transient notice label. LAW #6: the real-write outcome (put_artifact) is the ONLY human-facing result of the Upload tab, so it is emitted field-by-field (ok / bytes / stage / error) and a robot asserts the VALUE, never the pixels or a substring of a free-text string.
attempted == false is the clean-slate state (no upload run yet); ok is None until an attempt lands, then Some(true) on a stored write or Some(false) on a failure whose stage ("read" = the local file read, "put" = the server write) and error are both populated (LAW #2 — the failure path is an asserted value).
struct HolgerUiApp · src/app.rs:508
The holger UI application: a [UiData] handle + the tab's input fields.
Module holger_server_ui::app::HolgerUiApp
fn new(mut data: UiData) -> Self · src/app.rs:693
Build the app over a connected data layer and prime the first views.
fn enable_demo(&mut self, beat_ms: u64) · src/app.rs:819
🎬 Arm demo mode: install the hands-free guided-tour walk and skip the splash so the tour starts immediately. beat_ms is the pause between beats. The live render loop then applies one due beat per frame off the egui clock.
fn demo_drive_to(&mut self, elapsed_ms: u64) -> Vec<String> · src/app.rs:828
🎬 The headless-test hook: apply every beat due at elapsed_ms against the real app hooks (deterministic, no egui clock). Returns the labels applied. The live demo_tick is the same logic fed by frame time.
pub(crate) fn demo_running(&self) -> bool · src/app.rs:853
true when the hands-free tour is armed. Only the demo (embedded) build has one to arm, so every other tier answers a constant false — which lets the panes that ask (Helix's data signature, the render loop) stay one code path instead of growing a #[cfg] at each question.
fn demo_active(&self) -> bool · src/app.rs:867
true when demo mode is armed (for the live repaint loop). The read-only tiers compile no demo, so it is constantly false there.
fn set_session_client_identity(&mut self, cert: Option<std::path::PathBuf>, key: Option<std::path::PathBuf>,) · src/app.rs:920
Record the session's mTLS client identity (the --cert/--key the UI connected with), so Tool #5 Airgap can reuse it for an auth=mtls push.
fn airgap_set_dry_run(&mut self, dry_run: bool) · src/app.rs:931
Toggle the Tool #5 Airgap dry-run flag (test/automation hook for driving the validate() happy/red path without the egui checkbox).
fn set_artifact_query(&mut self, repository: &str, name: &str, version: &str) · src/app.rs:938
Set the Artifact-tab query fields (test hook: the tab's text edits live in an egui Grid and aren't individually label-addressable for TypeInto, so a deterministic setter drives the same state the operator would type).
fn set_save_path(&mut self, path: &str) · src/app.rs:949
Set the Artifact-tab ⬇ save destination (the download path box). A test hook: the box prefills to the user's download dir per fetch, so a headless drive pins it to a deterministic (writable or deliberately-bad) path and then clicks the real ⬇ Save artifact button, whose outcome lands on state_json["artifact"]["saved_to" / "save_error"].
fn select_repo(&mut self, i: usize) · src/app.rs:957
Select repository row i — the exact path the Repos-tab row click runs. A test hook because the repo name is rendered BOTH as the selectable row and in the facett capability Table, so a label-addressed click is ambiguous; this drives the same selection deterministically.
fn look_theme(&self) -> LookTheme · src/app.rs:968
The active look's [LookTheme].
fn look_name(&self) -> &'static str · src/app.rs:974
The active look's holger label (dark / light / device) — what the switcher shows and state_json reports, not facett's internal theme name.
fn set_look_preset(&mut self, idx: usize) -> String · src/app.rs:979
Switch the active look (clamped); returns the new look's label.
fn apply_look(&self, ctx: &egui::Context) · src/app.rs:987
Apply the active preset to ctx — installs the full egui Style and publishes the derived legacy palette so the facett Table facets follow (COH-1). Re-applying takes effect next frame, no restart.
fn look_state_json(&self) -> Value · src/app.rs:995
The look state the operator sees, as observable JSON for headless robot tests: the active preset name, the full preset roster, and the resolved legacy palette (the colours every facett view actually paints with). This is what proves "the active preset + a coherent palette" without a screen.
fn state_json(&self) -> Value · src/app.rs:1077
The whole app's observable state — what a robot driver asserts on ("see what the user sees as data", the nornir Observable::state_json contract). Active tab, static mode, selected tool, splash/graph state, the repo count behind the views, and the active look.
fn toggle_about(&mut self) -> bool · src/app.rs:1217
Toggle the About popup (test/automation hook + the Help · About command path). Returns the new open state.
fn run_demo_tool(&mut self) · src/app.rs:1227
Dispatch the Tool #6 demo appliance — the same hook the pane's "Start demo appliance…" button runs: records the demo container bring-up as a jera job (and boots it live under the containers feature). Test/automation hook so a robot drives the exact dispatch a click would, then asserts the observable job id.
fn set_upload_path(&mut self, path: &str) · src/app.rs:1810
Robot/test hook: set the upload source file path (the Upload form's "file path" box), so a headless drive can point the upload at a real (or deliberately missing) file and then click the real upload button. The write's outcome lands on state_json["upload"] — see [UploadOutcome].
fn set_upload_coord(&mut self, name: &str, version: &str) · src/app.rs:1818
Robot/test hook: set the artifact name + version the Upload form writes under (the id's coordinate). The target repo comes from the Repos selection (select_repo); the source bytes from set_upload_path.
fn select_graph_node(&mut self, idx: usize) · src/app.rs:2535
Feed the graph a node selection (the same effect a click in the 3D view has) — lights that node's subgraph and dims the rest. Used by keyboard / programmatic selection and by the robot-UI test. No-op if no graph is built or the index is out of range.
fn draw_ui(&mut self, ui: &mut egui::Ui) · src/app.rs:3235
Robot/harness entry: render the whole app into a Ui (no eframe Frame), so egui_kittest / nornir-robotui can drive the real app: Harness::builder().build_ui_state(|ui, app| app.draw_ui(ui), app).
This is now the SAME call the production [eframe::App::ui] impl makes — render(ui), straight into the root Ui. It used to wrap the app in a CentralPanel that production does not have, so the robot suite was driving a subtly different widget tree (an extra panel node, and its frame margins) from the app a user actually sees. Taking a Ui also drops the deprecated top-level CentralPanel::show.
fn skip_splash(&mut self) · src/app.rs:3240
Skip the startup splash so headless/robot tests start on the tabs.
Module holger_server_ui::app::Tab
pub(crate) fn slot_id(self) -> &'static str · src/app.rs:349
The stable scene-slot id (also the state_json.tab string) for this tab.
pub(crate) fn label(self) -> &'static str · src/app.rs:369
The human header label shown on the scene's TabView button (the AccessKit label a robot clicks). Stable — asserted by the robot suite.
pub(crate) fn index(self) -> usize · src/app.rs:388
This tab's index into [TAB_ORDER] (the scene's active-tab child index).
pub(crate) fn from_index(i: usize) -> Tab · src/app.rs:392
The tab at scene-slot index i (clamped to [Tab::Status]).
pub(crate) fn help_text(self) -> &'static str · src/app.rs:399
A one-paragraph plain-language explanation of what this tab is for — shown behind the small ⓘ button at the top of each tab body. Mini-only (the affordance that reads it, scene::tab_info, is gated to the mini tier).
Module holger_server_ui::app::UploadOutcome
fn state_json(&self) -> Value · src/app.rs:495
The outcome as observable data (LAW #6): every field a robot asserts.
Module holger_server_ui::app::command
Command palette + menu dispatch for the holger UI — the
facett::menucommand set (goto:/tool:/repo:/action:/look:) and the single interpreter that routes a picked id through the SAME hooks a click runs. Split out of [super] (app.rs) to keep that module focused on rendering; the impl block here is a child module ofapp, so it still reachesHolgerUiApp's private state directly.
Module holger_server_ui::app::command::HolgerUiApp
pub(super) fn build_commands(&self) -> Vec<Command> · src/app/command.rs:54
Build the palette command set for the current state: every tab (goto:*), every operator tool (tool:*), one command per repository (repo:*, built at runtime — the reason this needs facett's owned-String command ids), a few actions (action:*), and the look presets (look:*). The ids are stable + headless-addressable, so run_command fires the same paths a click would. Rebuilt each frame (cheap; a flat Vec).
pub(super) fn dispatch_command(&mut self, id: &str) · src/app/command.rs:137
Dispatch a palette command id through the SAME hooks a click runs (tab switch, tool select, repo select→Browse, refresh, look). Unknown ids are ignored. This is the one place palette ids are interpreted — run_command (headless) and the live palette.ui pick both route here.
fn run_command(&mut self, id: &str) · src/app/command.rs:173
Headless hook: fire a palette command by id (CLI / robot parity with a live pick), recording it on the palette's invoked observable and running its effect — so a test drives the exact path Ctrl-K → Enter would.
fn apply_repo_row_nav(&mut self, i: usize, action: &str) · src/app/command.rs:182
Run a per-repo right-click navigation action (row:browse / row:scan / row:archive): select row i, then open the matching tab (Scan → Tools/ Security). row:copy is handled at the call site (it needs the egui clipboard). Extracted so a headless test drives the same path a pick runs.
Module holger_server_ui::app::graph
The dependency-graph view types for the holger UI — the neon node-kind palette ([
GraphNodeKind]), the repo→kind classifier ([repo_node_kind]), and the Móðguðr verdict-graph renderer ([render_security_graph]). Split out ofapp; theHolgerUiAppmethods that BUILD theGraph3D(build_graph/graph_tab/graph_state_json) stay inappand reach these assuper::graphitems.
pub(super) fn render_security_graph(graph: &DepGraphData, ui: &mut Ui) · src/app/graph.rs:19
Render the Móðguðr dependency-graph view: every scanned component as a node coloured by verdict (block=red, warn=yellow, pass=green) via facett's DepGraphView, with warehouse-sourced edges when present — else the coloured node set with an honest "no resolved graph for this archive" note. A free function (not a method) so it borrows only the graph data, leaving the Security pane's other self field borrows intact.
pub(super) enum GraphNodeKind { Server, Repository, Artifact, Upstream, ReadOnly, Quarantined } · src/app/graph.rs:69
A node kind in the holger 3D graph — drives the HOLGER NEON PALETTE (§"holger wiring" in graph3d-orgasmic.md). Each kind maps to a neon colour; the facett Graph3D then layers glow/shockwave/dim over it under a Full-effects theme (DecorPolicy::Full). The kind is also surfaced in state_json so the robot can assert the graph as DATA, not pixels.
pub(super) fn repo_node_kind(r: &RepoRow) -> GraphNodeKind · src/app/graph.rs:122
Classify a repository row into a [GraphNodeKind] from the data the UI already exposes (no new gRPC call): quarantine/drift markers in the name or type, then proxy/upstream/cache type, then the read-only flag, else a plain repository.
Module holger_server_ui::app::graph::GraphNodeKind
pub(super) fn color(self) -> egui::Color32 · src/app/graph.rs:89
The neon base colour for this kind. Under DecorPolicy::Full the facett kernel re-shades with its neon gradient + glow; under Reduced/device these kind-colours are what the operator sees (so the mapping is real, not cosmetic).
pub(super) fn label(self) -> &'static str · src/app/graph.rs:107
A stable string label for state_json (asserted by the robot).
Module holger_server_ui::app::look_tests
Robot-UI cell for the unified facett look & feel (the "all consumers, holger too" migration). Runs only under
--features gui(whereappcompiles). It drives the real holger-server-ui app's look surface headlessly and asserts its observable state — the active preset + a coherent palette — and that switching the preset actually re-themes every facett view via the published legacy palette (COH-1). No display, no GPU.LAW: inject real input + assert real output. We inject a concrete preset index (a "click" on the look switcher) and assert the rendered palette changes accordingly and that egui picked up the new style — never just "didn't panic".
Module holger_server_ui::app::scene
The holger shell, described DECLARATIVELY. The whole top-level UI — a grouped menubar over a tabbed pane tree — is expressed as one [
facett_app::scene::Scene] value and rendered through facett-app's OWN primitives (Router/TabView/facett_menu). Nothing is forked: the scene DRIVES the existing<tab>_tabrenderers through a [SceneHost], returning each view-model'sstate_json().
- Describe — [
holger_scene] builds the shell (menubar groups + a draggable master/detail split of the registry rail over aPane::tabsof one foldable body perTAB_ORDERentry, each body itself either a DRAGGABLE master/detail split or an honest single pane — see [body_split]) with the Rust builder; the identical value round-trips through RON byte-stably.- Host — [
SceneHostImpl] maps eachContent::Slot(id)to the matching<tab>_<half>(ui)method and folds its view-modelstate_json()into the scene state. MenuAction::Commands route straight into the existingdispatch_command.- Render — [
HolgerUiApp::render_scene] calls [facett_app::scene::render] from the app's draw, keepingself.tabauthoritative (so every existing command / demo / robot hook still drives the tab).- Observe — [
HolgerUiApp::scene_state_json] folds the pane tree + fold / active-tab state + the last render facts into the app'sstate_json.holger IS a master/detail app, and the shell now says so. This module's older doc argued the shell was "a flat, single-window tabbed workspace … draggable splits / drawers are deliberately unused — there is no honest master/detail or peripheral-rail surface to hang them on." That was wrong about its own app. holger is an artifact REGISTRY BROWSER: one selected repository (
repos.selected) is the master, and Browse / Archive / Artifact / Upload / Lineage are all details OF that selection — five tab …
pub(super) const ROOT: & str · src/app/scene.rs:87
The ROOT pane's unique name — the draggable master/detail split (rail | workspace) the Router reports as the scene's route. Aliases the app's [SHELL_PANE] so the pane id a robot addresses (/scene/panes/holger-shell) has exactly ONE spelling across the shell and its tests.
pub(super) const WORKSPACE: & str · src/app/scene.rs:92
The tabbed WORKSPACE pane's name — the addressable handle whose live active_tab index the renderer reads and a tab-header click mutates. Aliases the app's [WORKSPACE_PANE].
pub(super) const RAIL: & str · src/app/scene.rs:95
The registry RAIL pane's name (the foldable drawer holding the roster + selection).
pub(super) fn tab_index(tab: Tab) -> usize · src/app/scene.rs:130
The index of a [Tab] in the strip (its active_tab value). Delegates to the app's [Tab::index] so the scene and state_json can never disagree.
pub(super) fn tab_from_index(i: usize) -> Tab · src/app/scene.rs:135
The [Tab] at strip index i (inverse of [tab_index]).
pub(super) fn split_bodies() -> Vec<String> · src/app/scene.rs:307
Every tab body that DECOMPOSED into a draggable master/detail split, by pane name — the divider roster the internal-split robot cells drag. Derived from [body_split], so a body silently downgraded back to one pane drops out of here.
pub(super) fn single_pane_bodies() -> Vec<String> · src/app/scene.rs:318
The tabs that are HONESTLY one pane (no master/detail to hang a divider on) — the complement of [split_bodies], published so the coverage test asserts the decision explicitly rather than by omission.
pub(super) fn holger_scene(repos: &[RepoRow]) -> Scene · src/app/scene.rs:363
Build the holger shell as a declarative [Scene] (the Rust-builder form). The pane tree is CONSTANT (one leaf per [TAB_ORDER] entry) so a persisted NavState stays valid frame-to-frame; only the Repositories submenu is data-driven (one Action::Command("repo:<name>") per reported repository), so this is rebuilt each frame from the current repo roster.
holger-shell ── Split(H, 0.22, DRAGGABLE) [phone → Tabs(active 1) under 760pt]
├─ holger-rail ── Split(V, 0.60, DRAGGABLE) (Foldable 🗄, drawer@760) "Registry"
│ ├─ rail_roster_pane (Foldable 📚) slot:rail-roster — the MASTER selection
│ └─ rail_selection_pane (Foldable 🔎) slot:rail-selection — its cross-tab context
└─ holger-workspace ── Tabs(active 0) "Workspace"
├─ status_pane (Foldable 🩺) slot:status — HONESTLY one pane
├─ repos_pane ── Split(H, 0.62, DRAGGABLE)
│ ├─ repos_main "Repos · grid" slot:repos-main
│ └─ repos_inspector "Repos · actions" slot:repos-inspector
├─ browse_pane ── Split(H, 0.66, DRAGGABLE)
│ ├─ browse_main "Browse · artifacts" slot:browse-main
│ └─ browse_inspector "Browse · source" slot:browse-inspector
├─ archive_pane ── Split(H, 0.64, DRAGGABLE)
│ ├─ archive_main "Archive · files" slot:archive-main
│ └─ archive_inspector"Archive · stats" slot:archive-inspector
├─ artifact_pane ── Split(H, 0.48, DRAGGABLE) query │ metadata
├─ graph_pane ── Split(H, 0.74, DRAGGABLE) canvas │ inspector
├─ helix_pane ── Split(H, 0.74, DRAGGABLE) canvas │ inspector
├─ lineage_pane ── Split(H, 0.74, DRAGGABLE) canvas │ feed
├─ tools_pane ── Split(H, 0.26, DRAGGABLE) functions │ output
├─ upload_pane ── Split(H, 0.58, DRAGGABLE) form │ target
…
pub(super) fn foldable_panes(scene: &Scene) -> Vec<String> · src/app/scene.rs:530
The names of every foldable pane in a scene (introspection for state_json and the coverage tests).
A thin delegate to facett-app's OWN [Scene::foldable_panes] — holger used to re-walk the tree here, which is exactly the twin L5 forbids. The shared crate is the one place that knows what "foldable" means.
pub(super) fn draggable_splits(scene: &Scene) -> Vec<String> · src/app/scene.rs:538
The names of every pane whose layout is a draggable (resizable) split — the divider roster the robot suite drags and state_json reports. A split that loses resizable: true drops out of this list, which is exactly what the coverage test asserts against. Delegates to facett-app's own [Scene::draggable_splits] (L5).
pub(super) fn drawer_panes(scene: &Scene) -> Vec<String> · src/app/scene.rs:543
The names of every DRAWER pane (auto-folds crossing into a phone-class width).
Module holger_server_ui::app::scene::HolgerUiApp
pub(super) fn render_scene(&mut self, ui: &mut egui::Ui) -> Value · src/app/scene.rs:901
Render the shell through the declarative [Scene] — the menubar + the tabbed pane tree — DRIVING the existing tab renderers via [SceneHostImpl]. Called from [HolgerUiApp::render] in place of the old hand-wired menu-bar + tab strip + match self.tab central panel.
self.tab stays the authoritative tab (so every command / demo / robot hook keeps driving it): it is pushed into the scene's active_tab before the render and, if a tab-header click moved it this frame, read back afterwards. The static-mode Upload guard is re-applied on the way out. Returns the render state_json (cached into self.last_render).
pub(super) fn scene_state_json(&self) -> Value · src/app/scene.rs:936
The declarative shell as observable data (folded into the app's state_json): the pane tree, which panes are foldable, the live fold + active-tab state, and the last render's facts (device class, pane rects, per-slot state).
fn toggle_scene_fold(&mut self, pane: &str) -> bool · src/app/scene.rs:986
Robot/test hook: fold or unfold a scene pane by name (the same toggle a fold-header click runs). Returns the new folded state. Observable in state_json["scene"]["folded"].
fn toggle_scene_minimize(&mut self, pane: &str) -> bool · src/app/scene.rs:994
Robot/test/command hook: minimise a scene pane — collapse it to its chrome title bar and hand its space back to its siblings. The same toggle the bar's − control runs. Returns the new minimised state; observable in state_json["scene"]["minimized"], and in the pane's shrunken render alloc.
fn toggle_scene_maximize(&mut self, pane: &str) -> bool · src/app/scene.rs:1002
Robot/test/command hook: maximise a scene pane — give it the whole shell area, shrinking every non-ancestor sibling to zero. At most one pane holds it. The same toggle the bar's 🗖 control runs. Returns the new maximised state; observable in state_json["scene"]["maximized"].
fn restore_scene_pane(&mut self, pane: &str) -> bool · src/app/scene.rs:1009
Robot/test/command hook: restore a scene pane — drop its minimise AND its maximise. The split ratios were never overwritten, so the layout returns exactly as it was. Returns true if anything changed.
fn restore_scene_layout(&mut self) -> bool · src/app/scene.rs:1015
Robot/test/command hook: restore every pane (the "reset layout" one-liner). Returns true if anything changed.
Module holger_server_ui::data
Transport-agnostic view-model for the holger UI.
Everything the UI renders is produced here by calling the core [
HolgerObject] API. That trait is satisfied byLocalHolger(direct in-process rust calls into holger core) orRemoteHolger(gRPC) — the UI does not care which. There is no egui in this module: every view is plain data with astate_json()method, so the UI's state can be asserted headlessly (the nordisk "see what the user sees as data" law).The single [
UiData] handle owns a private current-thread tokio runtime and runs exactly one core call per UI action (the nornir-viz idiom), so an eguiupdateloop can stay synchronous.
struct ArchiveView { repository: String, files: Vec<String>, file_count: u64, total_uncompressed_bytes: u64, archive_path: String, error: Option<String> } · src/data.rs:40
Archive browse view — backs the Archive tab (ArchiveService). Holds the raw file paths inside the znippy archive backing repository plus its stats (file count, total uncompressed bytes, archive/repo name). error carries a core/transport failure from either the archive_info or list_archive_files call that fed it.
struct RepoRow { name: String, repo_type: String, writable: bool, has_archive: bool } · src/data.rs:60
One repository row in the picker/sidebar — mirrors [RepositoryInfo].
writable is exactly the flag the Upload control reads; has_archive gates the Archive tab.
struct StatusView { status: String, version: String, uptime_seconds: i64, loaded: bool, error: Option<String> } · src/data.rs:80
Status / About view — backs the Status tab (Health).
struct ProfileView { profile: String, read_only: bool, label: String, writable_repo_count: i32, loaded: bool, error: Option<String> } · src/data.rs:100
Server-profile view — server truth backing the static "silent running" chrome (AdminService.ServerProfile). The UI drives static_mode and the badge off this, not off a local toggle.
struct ReposView { repos: Vec<RepoRow>, selected: Option<usize>, error: Option<String> } · src/data.rs:134
Repo picker view — backs the Repos tab (ListRepositories).
struct ArtifactView { repository: String, namespace: Option<String>, name: String, version: String, size_bytes: u64, content_type: String, found: bool, error: Option<String>, properties: Vec<PropertyKv>, sbom: Option<SbomView>, sbom_error: Option<String>, saved_to: Option<String>, save_error: Option<String>, data: Vec<u8> } · src/data.rs:164
Artifact fetch view — backs the Artifact tab (FetchArtifact).
The raw bytes are kept on the view but never serialized into state_json() (only size_bytes), so headless snapshots stay small and don't leak payloads.
struct SbomView { size_bytes: i64, content_address: String, preview: String } · src/data.rs:194
The hosted SBOM document's observable summary (bytes are NOT serialized — only size, content address, and a short preview for the inspector).
struct SearchView { query: String, artifacts: Vec<SearchArtifactHit>, paths: Vec<SearchPathHit>, truncated: bool, searched: bool, error: Option<String> } · src/data.rs:210
Cross-repo search view — backs the Search tab (SearchService.Search).
struct BrowseRow { namespace: Option<String>, name: String, version: String, size_bytes: i64, content_type: String } · src/data.rs:232
One artifact row in a repository listing — mirrors [traits::ArtifactEntry] (the proto ArtifactEntry), with the id flattened into the columns the Browse table renders.
struct BrowseView { repository: String, entries: Vec<BrowseRow>, next_page_token: String, error: Option<String> } · src/data.rs:244
Browse view — backs the Browse tab (ListArtifacts). Holds one page of entries for the active repository plus the opaque next_page_token the core returned (empty when the listing is exhausted).
struct SecFinding { name: String, version: String, decision: String, ids: Vec<String>, summary: String } · src/data.rs:261
One scanned artifact's verdict row in the Security pane — plain, serializable data lifted from a modgunn::core::Verdict so this view compiles in the default (non-gui) build with no modgunn types leaking in.
struct SecurityView { repository: String, ecosystem: String, scanned: usize, skipped: usize, pass: usize, warn: usize, block: usize, decision: String, findings: Vec<SecFinding>, loaded: bool, source: String, error: Option<String> } · src/data.rs:277
Security scan view — backs the Tools#3 (Móðguðr) pane. Holds the aggregate tallies + the overall decision for the scanned repository plus the per-artifact findings (clean artifacts are tallied but not listed). error carries a core/transport failure from the listing call that fed it.
struct DepGraphNode { name: String, version: String, decision: String, ids: Vec<String> } · src/data.rs:304
One node in the Móðguðr dependency-graph view: a scanned artifact with its verdict decision and any advisory ids (the graph node's sublabel).
struct DepGraphData { repository: String, nodes: Vec<DepGraphNode>, edges: Vec<(usize, usize)>, has_edges: bool, pass: usize, warn: usize, block: usize, loaded: bool, error: Option<String> } · src/data.rs:320
The dependency-graph view backing the Móðguðr tool's graph mode: every scanned artifact as a node coloured by verdict (block=red, warn=yellow, pass=green) — unlike [SecurityView::findings], which lists only the non-clean ones. Inter-dependency edges are sourced from the shared warehouse WHEN it has a captured snapshot for this subject (has_edges); a served archive usually has none, so the view shows the coloured node set with an honest "no resolved graph" note. state_json lets the robot assert the colour tallies + edge count.
struct UiData { status: StatusView, profile: ProfileView, repos: ReposView, artifact: ArtifactView, browse: BrowseView, archive: ArchiveView, search: SearchView, endpoint: String, security: SecurityView, dep_graph: DepGraphData, airgap: holger_airgap::PopulateArgs, airgap_run: Option<AirgapRun>, holger_ds: DsConfig, migrate: MigrateConfig, deploy: DeployConfig, demo_appliance: Option<crate::infra::DemoDispatch>, lineage: LineageView } · src/data.rs:373
The UI's data layer: a holger handle + a private current-thread runtime so the async core calls can be driven synchronously (one blocking call per UI action — the nornir-viz idiom).
Module holger_server_ui::data::ArchiveView
fn state_json(&self) -> Value · src/data.rs:50
Module holger_server_ui::data::ArtifactView
fn state_json(&self) -> Value · src/data.rs:203
Module holger_server_ui::data::BrowseView
fn state_json(&self) -> Value · src/data.rs:252
Module holger_server_ui::data::DepGraphData
fn state_json(&self) -> Value · src/data.rs:336
Module holger_server_ui::data::ProfileView
fn state_json(&self) -> Value · src/data.rs:127
Module holger_server_ui::data::ReposView
fn state_json(&self) -> Value · src/data.rs:142
fn selected_repo(&self) -> Option<& RepoRow> · src/data.rs:147
The currently selected repo row, if any.
fn upload_enabled(&self) -> bool · src/data.rs:153
Whether the Upload control should be enabled: a writable repo is selected. Exactly the RepositoryInfo.writable gate from the UI brief.
Module holger_server_ui::data::SearchView
fn state_json(&self) -> Value · src/data.rs:223
Module holger_server_ui::data::SecurityView
fn state_json(&self) -> Value · src/data.rs:296
Module holger_server_ui::data::StatusView
fn state_json(&self) -> Value · src/data.rs:91
Module holger_server_ui::data::UiData
fn new(holger: Arc<dyn HolgerObject>) -> anyhow::Result<Self> · src/data.rs:416
Build a data layer around any HolgerObject (embedded or remote).
fn with_runtime(holger: Arc<dyn HolgerObject>, rt: tokio::runtime::Runtime) -> Self · src/data.rs:448
Build a data layer from a handle plus a caller-provided runtime. Use this when the handle's transport (e.g. a tonic channel) must be created on the same runtime that later drives its calls — the single-runtime tonic idiom. [UiData::new] is the common case (it makes its own current-thread runtime).
fn refresh_status(&mut self) · src/data.rs:478
Refresh the Status tab from health().
fn refresh_profile(&mut self) · src/data.rs:503
Refresh the server profile from server_profile() (server truth for the static / "silent running" mode). On error the view is marked unloaded and the error is surfaced, leaving the prior values in place.
fn refresh_repos(&mut self) · src/data.rs:529
Refresh the repo picker from list_repositories(). Selection is preserved by name across refreshes; a first load selects the first repo.
fn select_repo(&mut self, idx: usize) · src/data.rs:548
Select a repo by index (a click handler / headless-test entry point).
fn fetch_artifact(&mut self, repository: &str, id: ArtifactId) · src/data.rs:557
Fetch an artifact into the Artifact tab. A clean NOT_FOUND is rendered as found = false (not an error); transport/server failures land in error.
fn fetch_sbom_current(&mut self) · src/data.rs:587
Fetch the hosted SBOM for the artifact currently in the view (on demand — a document can be large, so it is not loaded with the fetch). None (nothing attached) clears the pane; errors land in sbom_error.
fn save_artifact(&mut self, path: &str) · src/data.rs:618
Save the fetched artifact's bytes to path (the ⬇ download). The bytes are already in memory from the fetch — this is a plain local write; the outcome lands on saved_to / save_error (both observable).
fn refresh_search(&mut self, name_query: &str) · src/data.rs:634
Run a cross-repo search on the name axis (SearchService.Search) into the Search view. An empty query clears the view instead of asking the server to enumerate everything.
fn refresh_browse(&mut self, repository: &str, name_filter: Option<String>) · src/data.rs:675
Refresh the Browse tab from list_artifacts() for repository, optionally narrowed by a name_filter substring — loads the FIRST page (up to [Self::BROWSE_PAGE_SIZE]). The continuation token is kept on the view; call [UiData::load_more_browse] to append the next page. A core error lands in error.
fn load_more_browse(&mut self) · src/data.rs:707
Append the next page of the current Browse listing using the stored continuation token. No-op when the listing is exhausted (empty token).
fn refresh_archive(&mut self, repository: &str, prefix: Option<String>) · src/data.rs:742
Refresh the Archive tab for repository: load the archive stats (archive_info) and the raw file paths (list_archive_files, optionally narrowed by a path prefix). Two core calls feed one view; an error from either lands in error (and leaves the rest at its default). The repository is always recorded so the UI shows which repo it tried.
fn put_artifact(&self, repository: &str, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> · src/data.rs:766
Upload bytes to a (writable) repo. The core error is returned verbatim so the UI can map it to an affordance (PERMISSION_DENIED → "read-only", …).
fn run_security_scan(&mut self, repository: &str, repo_type: &str) · src/data.rs:778
Run a Móðguðr (modgunn) security scan over repository (whose ecosystem is repo_type): list the backing archive over the core API, map each file to a modgunn::scan::Artifact, and fold the per-artifact verdicts into the [SecurityView]. One core call (list_archive_files); the verdict itself is pure + offline. Gated to where server-lib (hence modgunn) is present: the gui build and the test build (dev-dependency) — the default transport-only build doesn't link modgunn.
fn run_holger_ds(&mut self) · src/data.rs:882
Tool #1 — actually RUN the holger-ds artifact-delivery benchmark at the selected scale, over the package protocol (Holger's native HTTP door), and fold the throughput (MB/s · ops/s) into [DsConfig::result]. Reuses the exact bench-scenarios path the CLI verb + the head-to-head comparison use: build a synthetic crate corpus → stand up the real HTTP gateway (or drive an EXTERNAL target base URL) → gatling-style parallel GET pool.
Inline + CI-safe: corpus budget + bombard duration are clamped per scale so micro completes in ~1s. The gatling pool blocks OS threads and the gateway tokio::spawns its accept loop, so this drives a dedicated multi-thread runtime (not the data layer's current-thread one). Gated to where bench-scenarios/server-lib are linked (the gui build + the test build).
fn run_demo(&mut self) · src/data.rs:959
Tool #6 — dispatch the demo appliance container THROUGH the shared job manager jera: [crate::infra::dispatch_demo] declares the pinned demo registry ContainerSpec, records its bring-up as a durable [jera::jobs] BOOT_CONTAINER job (returning the job id), and — with holger-server-ui's containers feature — boots it live through jera→draupnir. The result (job id + spec + status) lands in [Self::demo_appliance] for the pane + state_json. The ledger lives under HOLGER_DATA/holger-server-ui-jobs when set, else a temp dir (daemon-free default).
fn run_migrate(&mut self) · src/data.rs:975
Tool #2 — actually RUN the Migrate ("suck"): build the agent connector for the chosen source (Nexus / Artifactory), list its repositories (optionally filtered), and dump them into the output znippy archive via agent_lib::operations::dump_all_repos_to_znippy. The connector's HTTP work is driven on the data layer's runtime. A missing URL or an unreachable registry is surfaced as a CLEAR result error — never a silent no-op. Gated to where agent-lib is linked (the gui build + the test build).
fn run_airgap(&mut self) · src/data.rs:1040
Tool #5 — produce a real Airgap populate plan/result. Validates the typed [PopulateArgs] first (so a missing mTLS cert/key surfaces a CLEAR message, never a crash); a --dry-run returns the pure-Rust plan only, while a real run delegates to the distribution/airgap/populate driver (located by walking up from the cwd) and captures its log + exit status. The outcome lands in [UiData::airgap_run].
fn run_deploy(&mut self, repository: &str, target: holger_airgap::Target) · src/data.rs:1101
Tool #4 — actually RUN the Deploy (Skíðblaðnir): read the selected repo's sealed znippy bundle over the core API (archive_info — a real gRPC / in-process call, the same one the Archive tab uses) to confirm there is an artifact to deploy and capture its size, then produce a real airgap-engine deploy plan (push-only carry of the already-sealed bundle to target). The outcome lands in [UiData::deploy]. An empty / missing archive surfaces a CLEAR message — never a silent no-op (this is the "** being implemented" no-op-button fix). The actual carry/push half is the inherently-external airgap driver (Tool #5's execute path); here the deploy is planned so it can run with no creds against a test target, mirroring the airgap dry-run default.
fn airgap_state_json(&self) -> Value · src/data.rs:1148
The combined Airgap state — the typed args plus the last run result — for state_json (the Tools#5 pane as DATA).
fn state_json(&self) -> Value · src/data.rs:1177
One combined snapshot of every view — the headless $APP_STATE a robot test asserts against.
fn connect_remote(endpoint: &str) -> anyhow::Result<Self> · src/data.rs:1212
Connect to a remote holger gRPC endpoint (no credentials).
fn connect_remote_with_token(endpoint: &str, token: Option<& str>) -> anyhow::Result<Self> · src/data.rs:1224
Connect to a remote holger gRPC endpoint, optionally with an OIDC bearer token injected on every request (needed for write access on an auth-enabled server).
The data layer's private runtime owns the channel — so the connect and every later call share one current-thread runtime (the single-runtime tonic idiom the nornir viz uses; avoids a channel outliving the runtime that spawned it).
fn connect_remote_with_tls(endpoint: &str, ca: Option<Vec<u8>>, client_identity: Option<(Vec<u8>, Vec<u8>)>, token: Option<& str>,) -> anyhow::Result<Self> · src/data.rs:1251
Connect to a remote holger gRPC endpoint over TLS (and mTLS when a client_identity cert+key is supplied), optionally with a bearer token.
Same single-runtime pattern as [UiData::connect_remote_with_token]: the data layer's private current-thread runtime owns the TLS channel, so the connect and every later call share one runtime.
NOTE: only the client TLS path is built here; there is no end-to-end mTLS integration test (it needs generated certs + a TLS server — a manual / mannequin step). This path is verified to compile only.
Module holger_server_ui::data::lineage
Live artifact-lineage view-model — the proxy-cache-filling facet.
holger is a pull-through proxy/cache. As it caches and serves artifacts, this view-model maintains a live lineage graph of what the cache holds:
- nodes — every cached artifact (grouped/clustered by repository + [
Ecosystem]), plus the repository that requested it and the upstream it was proxied from,- edges — the lineage: artifact → the repo that requested it ([
EdgeKind::RequestedBy]) and artifact → its upstream/proxied source ([EdgeKind::ProxiedFrom]),- events — a time-ordered list of arrivals (the "when did the cache fill" temporal view the helix winds along a coil).
The feed is transport-agnostic: the model consumes a plain [
LineageEvent] and knows nothing about how it was produced. Two producers are supported:
- live — [
LineageView::connect] attaches anmpsc::Receiver<LineageEvent>; each [LineageView::pump] drains newly-arrived events and grows the graph incrementally (nodes pop in as the cache fills),- replay — [
LineageView::replay] folds an iterator of events (e.g. the whole history) into the same state.The seam onto holger's real data feed — the append-only Arrow-IPC audit log (
server_lib::audit) — lives in the gated [audit_tap] submodule: it maps anAuditEvent→ [LineageEvent], providesreplay_audit_dirover a segment directory, and a [ChannelAuditLog] tee that a running holger can install as itsArc<dyn AuditLog>so every fetch/put streams straight into a connectedLineageView.There is no egui here: like every view in [
crate::data], the whole state is observable asstate_jsonfor headless robot tests.
enum Ecosystem { Maven, Pypi, Rust, Npm, Nuget, Go, Gem, Deb, Rpm, Helm, Docker, Conda, Composer, Other } · src/data/lineage.rs:47
The package ecosystem an artifact belongs to — the cluster key the 3D fill groups nodes by and the lane the helix winds each arrival onto. Derived from the repository name (and, weakly, the artifact shape) since holger's audit record carries no explicit format column.
enum LineageAction { Download, Upload, Delete, List, Promote, Other } · src/data/lineage.rs:180
The operation an arrival describes — a transport-agnostic mirror of holger's audit::AuditAction.
struct LineageEvent { ts_nanos: i64, ident: String, action: LineageAction, repo: String, artifact: String, source_ip: String, status: u16, bytes: u64, upstream: Option<String> } · src/data/lineage.rs:209
One cache-fill event the [LineageView] consumes — the transport-agnostic input the model maps into a node + edges + a time event. It carries everything an audit::AuditEvent does, plus an optional explicit upstream (a real feed can supply the proxied origin; when None the model derives one from the ecosystem).
enum NodeKind { Artifact, Repo, Upstream } · src/data/lineage.rs:268
What a lineage-graph node represents.
enum EdgeKind { RequestedBy, ProxiedFrom } · src/data/lineage.rs:280
The lineage an edge encodes.
struct LineageNode { id: String, label: String, kind: NodeKind, ecosystem: Ecosystem, repo: String, cached_at_nanos: i64, size: u64, upstream: Option<String>, hits: u32 } · src/data/lineage.rs:289
One node in the live lineage graph.
struct LineageEdge { from: String, to: String, kind: EdgeKind } · src/data/lineage.rs:313
One directed lineage edge (by node id, so it survives incremental growth).
struct TimeEvent { ts_nanos: i64, artifact: String, ecosystem: Ecosystem, repo: String, action: LineageAction } · src/data/lineage.rs:322
One time-ordered arrival — the temporal event the helix winds onto a per- ecosystem coil.
struct LineageView { nodes: Vec<LineageNode>, edges: Vec<LineageEdge>, events: Vec<TimeEvent>, total_events: u64, total_bytes: u64 } · src/data/lineage.rs:336
The live lineage graph state — nodes (deduplicated), edges (deduplicated), and a time-ordered event list, grown by ingest from either a live channel (connect + pump) or a replay (replay). Every field is observable via state_json.
fn channel() -> (Sender<LineageEvent>, Receiver<LineageEvent>) · src/data/lineage.rs:364
Convenience: a fresh unbounded lineage feed channel. The Sender half is handed to a producer (e.g. a [ChannelAuditLog] tee installed on a running holger, or a demo driver); the Receiver is handed to [LineageView::connect].
fn demo_events() -> Vec<LineageEvent> · src/data/lineage.rs:534
A self-contained demo backlog: a realistic multi-ecosystem cache-fill sequence (rust/pypi/maven/npm/go arrivals with a couple of repeat hits) the gui layer drips one-per-frame through the LIVE channel so the graph visibly fills and rotates when no real audit feed is wired. Also handy as a test fixture.
Module holger_server_ui::data::lineage::Ecosystem
fn classify(repo: &str, artifact: &str) -> Ecosystem · src/data/lineage.rs:71
Classify a repository (and artifact) into an ecosystem by substring match on the repo name — the ecosystem is not an audit column, so it is recovered from the repo the request targeted (the same repo→backend mapping holger wires per znippy-{maven,python,rust,…} crate), falling back to the artifact shape and finally [Ecosystem::Other].
fn label(self) -> &'static str · src/data/lineage.rs:113
Stable lowercase label — the helix lane name and the cluster key surfaced in state_json.
fn upstream_host(self) -> Option<& 'static str> · src/data/lineage.rs:137
The public upstream host a pull-through proxy of this ecosystem fills from. Used to synthesize the proxied-from source when the audit record does not carry an explicit upstream (holger's audit log records the local fetch/put, not the origin — see the module docs). None for ecosystems with no single canonical public registry.
fn color(self) -> [u8; 3] · src/data/lineage.rs:156
A neon RGB base colour for this ecosystem cluster (the egui-free palette the gui layer lifts into a Color32).
Module holger_server_ui::data::lineage::LineageAction
fn as_str(self) -> &'static str · src/data/lineage.rs:191
Stable lowercase wire string.
Module holger_server_ui::data::lineage::LineageEvent
fn new(ts_nanos: i64, ident: impl Into<String>, action: LineageAction, repo: impl Into<String>, artifact: impl Into<String>, source_ip: impl Into<String>, status: u16, bytes: u64,) -> Self · src/data/lineage.rs:235
Build an arrival with no explicit upstream (the common case — the audit log records the local fetch/put, and the origin is derived from the ecosystem).
fn with_upstream(mut self, upstream: impl Into<String>) -> Self · src/data/lineage.rs:259
Attach an explicit proxied-from origin (builder style).
Module holger_server_ui::data::lineage::LineageView
fn new() -> Self · src/data/lineage.rs:370
A fresh, empty lineage view with no feed attached.
fn connect(&mut self, rx: Receiver<LineageEvent>) · src/data/lineage.rs:375
Attach a live feed. Subsequent pump calls drain it.
fn is_live(&self) -> bool · src/data/lineage.rs:380
true once a live feed is attached.
fn pump(&mut self) -> usize · src/data/lineage.rs:387
Drain all currently-available events from the live feed and fold each into the graph. Returns how many were ingested. Non-blocking; safe to call every frame. A disconnected sender is tolerated (the feed simply goes quiet).
fn replay(&mut self, events: impl IntoIterator<Item = LineageEvent>) · src/data/lineage.rs:401
Fold a batch (e.g. the whole audit history) into the graph — the replay-from-log mode.
fn artifact_count(&self) -> usize · src/data/lineage.rs:408
Number of distinct cached artifact nodes (excludes repo/upstream nodes).
fn ingest(&mut self, ev: LineageEvent) · src/data/lineage.rs:416
Fold one arrival into the live graph: upsert the artifact node (clustered by ecosystem), the requesting repo node and the proxied-from upstream node, wire the two lineage edges, and append the time event. An arrival with no specific artifact (a bare listing) is counted but adds no node.
fn state_json(&self) -> Value · src/data/lineage.rs:525
The whole lineage state as observable JSON (the headless robot contract).
Module holger_server_ui::data::lineage::audit_tap
struct ChannelAuditLog · src/data/lineage.rs:629
An [AuditLog] that tees every recorded event into a lineage feed channel — the live tap. Install it (or a fan-out wrapper) as a running holger's Arc<dyn AuditLog> and hand the paired Receiver to [LineageView::connect]; every fetch/put then streams live into the graph. record never fails (a disconnected receiver is dropped silently), matching holger's fire-and-forget audit contract.
Module holger_server_ui::data::lineage::audit_tap::ChannelAuditLog
fn new(tx: Sender<LineageEvent>) -> Self · src/data/lineage.rs:635
Wrap a lineage-feed sender (the Sender half of [super::channel]).
Module holger_server_ui::data::lineage::audit_tap::LineageView
fn replay_audit_dir(&mut self, dir: impl AsRef<Path>) -> anyhow::Result<usize> · src/data/lineage.rs:612
Replay-from-log mode over holger's audit stream: read every append-only Arrow-IPC segment under dir (oldest-first) and fold each recorded fetch/put into the lineage graph. Returns the number of events replayed. A missing directory replays as empty.
Module holger_server_ui::data::tools
Operator-tool config + result types for the holger UI data layer (Tools#1–#5): holger-ds delivery bench, migrate, airgap, and deploy (Skíðblaðnir). Each is a plain serializable struct with a
state_jsonso the robot asserts the pane as DATA; the RUNNERS that populate them live onUiDatain the parent. Split out ofdata;use super::*inherits the parent's imports (Serialize / Value / …).
struct DsResult { ran: bool, scale: String, target: String, crates: usize, files: usize, ops: u64, bytes: u64, mbs: f64, ops_per_sec: f64, status: String, error: Option<String> } · src/data/tools.rs:14
The result of one holger-ds artifact-delivery run, measured over the package protocol (never in-process): file/crate counts plus the throughput the gatling pool actually pulled off the wire. Surfaced in the Tools#1 pane and state_json so the robot asserts a REAL benchmark ran (not a no-op).
struct DsConfig { scale: String, target: String, status: String, result: Option<DsResult> } · src/data/tools.rs:36
Tool #1 holger-ds configuration — the demo-plan scale slider + target. Drives the artifact-delivery benchmark: scale (micro|small|large), and an optional target base URL to measure an EXTERNAL holger/nexus/artifactory over the real package protocol (empty ⇒ stand up a local holger gateway).
struct MigrateRun { ran: bool, source: String, url: String, output: String, repositories: usize, status: String, error: Option<String> } · src/data/tools.rs:79
The result of one Migrate ("suck") run via the holger agent connector: how many upstream repositories were dumped into the znippy archive (or a clear error). Surfaced in the Tools#2 pane + state_json so the robot asserts the connector was actually invoked (not a label-only stub).
struct MigrateConfig { source: String, url: String, output: String, repository: String, status: String, result: Option<MigrateRun> } · src/data/tools.rs:96
Tool #2 Migrate ("suck") configuration — pull a Nexus / Artifactory registry into one or many znippy archives via the holger agent (the single home of the sucking). source is "nexus" or "artifactory"; url the registry base; output the .znippy path; repository an optional single-repo filter.
struct AirgapRun { ran: bool, dry_run: bool, plan: String, log: String, exit_code: i32, success: bool, status: String, error: Option<String> } · src/data/tools.rs:148
The result of one Tool #5 Airgap populate run. A --dry-run produces the plan only (no shell); a real run delegates to the distribution/airgap/populate driver and captures its log + exit status. Surfaced in the Tools#5 pane + state_json so the robot asserts a real plan/result (never a crash/no-op).
struct DeployRun { ran: bool, repository: String, target: String, file_count: u64, bytes: u64, archive_path: String, plan: String, status: String, error: Option<String> } · src/data/tools.rs:168
The result of one Tool #4 Deploy (Skíðblaðnir) run. Reads the selected repo's sealed znippy bundle over the core API (archive_info) — the real artifact to deploy — and folds in an airgap-engine push-only deploy plan for the chosen test target. Surfaced in the Tools#4 pane + state_json so the robot asserts a real deploy ran (never a no-op / "being implemented" stub).
struct DeployConfig { target: holger_airgap::Target, status: String, result: Option<DeployRun> } · src/data/tools.rs:190
Tool #4 Deploy (Skíðblaðnir) configuration — pick the test target the selected repo's sealed znippy bundle is deployed to. target reuses the airgap engine's [holger_airgap::Target] so the deploy rides the documented airgap path.
Module holger_server_ui::data::tools::DeployConfig
fn state_json(&self) -> Value · src/data/tools.rs:210
Module holger_server_ui::data::tools::DsConfig
fn state_json(&self) -> Value · src/data/tools.rs:59
fn cli_command(&self) -> String · src/data/tools.rs:65
The equivalent holger-server holger-ds CLI invocation — the plan the Tools#1 pane shows (and a robot asserts).
Module holger_server_ui::data::tools::MigrateConfig
fn state_json(&self) -> Value · src/data/tools.rs:122
fn cli_command(&self) -> String · src/data/tools.rs:129
The equivalent holger-agent invocation — the migrate plan the Tools#2 pane shows (and a robot asserts). Uses --<source>-url so it stays in lockstep with the agent CLI flags.
Module holger_server_ui::demo
🎬 Demo mode — a self-running guided tour of holger-server-ui.
Two halves:
[
fill_demo_store] FILLS a dev znippy store with REAL packages pulled from crates.io via the existing agent/world connector ([world_connector::WorldConnector]) — bounded (a curated ~12-package set), SSRF-guarded by the connector itself, and offline-safe: any download that fails (no network) falls back to a small bundled synthetic-crate fixture, so the demo always opens a populated server. Packages are written into two writable [RustRepoZnippy] archives so the repo roster + 3D graph have real rows/nodes to show.[
DemoWalk] is the scripted walk: a deterministic, time-INJECTED list of beats that drive the realHolgerUiAppthrough its views — the repo roster, Browse/Archive/Artifact, the 3D dependency Graph (with node selection so the neon glow/shockwave/dim lights up), every Tools pane (incl. the Móðguðr security verdict graph), and look re-themes — with a pause between beats so the egui animations (spinning logo, Graph3D, ice decor, look transitions) actually render.FC-7: the walk never reads a wall clock — [
DemoWalk::due] takes the elapsed-ms as an argument. The live app feeds it egui's frame time (ui.input(|i| i.time)), a headless test feeds synthetic values, so the tour is fully deterministic.
struct DemoPackage { repo: String, name: String, version: String, bytes: usize, real: bool } · src/demo.rs:61
One package that landed in the demo store.
struct DemoFill { repos: Vec<String>, packages: Vec<DemoPackage>, online: bool, routes: Vec<(String, Arc<dyn RepositoryBackendTrait>)> } · src/demo.rs:73
The result of filling the demo store: the in-process holger handle the UI runs against, plus a report of what landed (and whether the network was reached).
fn fill_demo_store(store_dir: &Path, allow_network: bool) -> Result<DemoFill> · src/demo.rs:142
FILL a dev znippy store with the curated package set. Writes two on-disk writable znippy archives under store_dir and returns the wired routes + a report. allow_network gates the real crates.io pull — false (or any download failure) uses the bundled synthetic fixture, so the demo is fully offline-safe. Pure of egui; reusable from the CLI or a test.
pub(crate) enum DemoAction { Goto, SelectRepo, SelectGraphNode, SelectTool, SecurityGraph, Look } · src/demo.rs:207
One scripted action the demo applies to the live app — each maps to the exact pub hook a human click would run (select_repo, select_graph_node, set_look_preset, or a tab/tool switch).
pub(crate) struct DemoStep { at_ms: u64, label: String, action: DemoAction } · src/demo.rs:218
One beat of the tour: at at_ms, apply action and log label.
struct DemoWalk · src/demo.rs:227
The scripted-tour driver — pure + time-injected (FC-7). Holds the beats and a cursor; emits each due beat once. The app wraps it with egui frame time; a test feeds synthetic elapsed-ms.
Module holger_server_ui::demo::DemoFill
fn real_count(&self) -> usize · src/demo.rs:86
How many packages were pulled for real vs. faked offline.
fn local_holger(&self) -> server_lib::LocalHolger · src/demo.rs:92
Build the in-process LocalHolger the demo UI runs against (no gRPC, no network) — wraps the filled writable repos in a FastRoutes route table.
Module holger_server_ui::demo::DemoWalk
fn tour(beat_ms: u64) -> Self · src/demo.rs:238
The default showcase tour: walk every holger-server-ui view — the repo roster (with row selections), Browse/Archive/Artifact, the 3D dependency Graph (selecting nodes so the glow/shockwave plays), every Tools pane (incl. the Móðguðr security verdict graph), Upload, and two look re-themes — returning home. beat_ms is the pause between beats so each transition's motion renders.
fn len(&self) -> usize · src/demo.rs:268
Total beats.
fn is_empty(&self) -> bool · src/demo.rs:271
fn is_done(&self) -> bool · src/demo.rs:275
true once every beat in this pass has been emitted.
fn duration_ms(&self) -> u64 · src/demo.rs:279
The at_ms of the last beat — one pass's run length.
fn rewind(&mut self) · src/demo.rs:283
Reset the cursor for a looped restart on a rebased clock.
pub(crate) fn tabs(&self) -> Vec<Tab> · src/demo.rs:290
The distinct tabs the tour visits, in first-seen order — for the headless coverage assertion.
pub(crate) fn due(&mut self, elapsed_ms: u64) -> Option<DemoStep> · src/demo.rs:305
FC-7, time injected. Return the beat due at elapsed_ms (advancing the cursor), or None if nothing is due yet / the pass is done. At most one beat per call.
Module holger_server_ui::infra
Tools · Demo appliance — a jera-backed demo container for the holger UI.
The shared "common framework" the whole nordisk suite adopts (nornir / dwarves / korp / holger) gives every app a Tools menu whose demo entries start containers THROUGH the shared job manager
jera— never a bespokethread::spawnor a hand-rolled docker call (LAW 5, reuse-first). korp'sinfra.rsis the reference: a container is declared as a plain [jera::container::ContainerSpec] (which maps onto draupnir's ONE OCI engine viaContainerSpec::to_draupnir) and its bring-up is recorded as a durable job in the shared [jera::jobs] ledger — the exact seam holger-server already routes promote/GC through.holger's demo stands up a small, pinned upstream artifact registry (
docker.io/library/registry:2.8.3) — the natural demo for an artifact server: once up, holger can proxy / migrate from it. Declaring + dispatching the spec is testable in the lean default build (draupnir's spec types + the jera ledger always compile; the dispatch opens a real redb job row with no daemon); the LIVE boot is opt-in behind holger-server-ui'scontainersfeature (→jera/container-oci→draupnir/backend-oci).
const DEMO_REGISTRY_VERSION: & str · src/infra.rs:33
The demo registry version this UI pins (mirrors korp's pinned FalkorDB/Spark versions). Never latest — a re-launch must be reproducible, not silently drifted by an upstream retag.
const DEMO_IMAGE_DEFAULT: & str · src/infra.rs:38
The default demo image, pinned to [DEMO_REGISTRY_VERSION]: the CNCF distribution registry — a real upstream artifact store holger can proxy / migrate from. Env-overridable via HOLGER_DEMO_IMAGE.
const DEMO_PORT: u16 · src/infra.rs:41
The registry HTTP port — published host↔container 1:1.
const DEMO_NAME: & str · src/infra.rs:45
The stable managed-container name for the demo registry (draupnir prefixes draupnir-). A stable name makes teardown / idempotent re-launch addressable.
const DEMO_WORKSPACE: & str · src/infra.rs:48
The workspace label the demo job is recorded under in the jera ledger.
fn demo_image() -> String · src/infra.rs:51
The demo image to run — HOLGER_DEMO_IMAGE else [DEMO_IMAGE_DEFAULT].
fn demo_url() -> String · src/infra.rs:56
The demo registry client URL (http://127.0.0.1:5000).
fn demo_registry_spec() -> ContainerSpec · src/infra.rs:69
The demo registry container spec as a [jera::container::ContainerSpec]: the pinned image, the stable name, and the published :5000. Starting it goes THROUGH jera→draupnir ([start_via_jera] / [dispatch_demo]); no bespoke docker call. Engine defaults for the remaining knobs (net = default NAT, no mounts); ..Default::default() keeps this forward-compatible as jera grows the spec.
struct DemoDispatch { job_id: String, image: String, name: String, ports: Vec<u16>, url: String, status: String, booted: bool, error: Option<String> } · src/infra.rs:90
The outcome of a demo dispatch: the jera job id the bring-up was recorded under, the container spec's image / name / ports, and the terminal status. This is what the Tools#6 pane shows AND what the robot asserts in state_json — proof the demo went THROUGH the shared job manager (a non-empty job_id), not a bespoke spawn.
fn state_json() -> Value · src/infra.rs:132
A robot/headless oracle for the declared demo (before any dispatch): the spec's image · name · ports · url · pinned version. Lets a test assert the demo container is declared + pinned without a live daemon.
fn dispatch_demo(jobs_root: &Path) -> DemoDispatch · src/infra.rs:159
Dispatch the demo container through the shared job manager jera: open a durable [jera::jobs] ledger at jobs_root, start a kind::BOOT_CONTAINER job carrying the [demo_registry_spec] (image / name / ports), capture its job id, then bring the container up THROUGH jera→draupnir when the live engine is compiled in (containers feature) — else record it as a planned dispatched job (the default laptop/CI build has no daemon). Either way the ledger holds a row a reader (the nornir jobs list / a Jobs panel) can enumerate — exactly like holger-server's promote/GC jobs.
This is the load-bearing "no thread::spawn, no bespoke docker" seam: the demo's bring-up is a tracked job, not an ad-hoc spawn.
fn start_via_jera(spec: &ContainerSpec) -> Result<(), String> · src/infra.rs:229
Start the demo container through jera (→ draupnir's ONE OCI engine), reusing the shared runtime rather than any bespoke docker call (LAW 5). Detached bring-up (the boot proc is dropped; draupnir's start is detached). Only compiled with holger-server-ui's containers feature.
Module holger_server_ui::infra::DemoDispatch
fn state_json(&self) -> Value · src/infra.rs:114
The observable state a robot asserts — carries the job id (proof it went through jera), the container spec, and the terminal status.
Module holger_server_ui::login_seam
HolgerLoginSeam— holger-server-ui's host implementation of the shared [facett_auth_ui::LoginSeam], wiring the SHAREDnornir-auth-client(edda) RFC 8628 device-authorization grant behind the presentationalLoginPanethe operator Sign-in tab mounts. This is the SAME sign-in the constellation reuses (korp/holger/dwarves) — holger never hand-rolls a device-code loop (LAW #5).Layering (important)
The pane is auth-library-agnostic + presentational: it only renders the states this seam surfaces. holger owns the wiring here. The [
facett_auth_ui::LoginSeam] methods are SYNC, but the device primitives are async — so each call bridges through a private current-threadtokioruntime (block_on), the same idiom korp'ssrc/login.rsuses. That is IO, not CPU parallelism: norayon/thread::spawn/tokio::spawnof compute (ROOT LAW #0).Security invariant (RED-when-broken)
No method exposes a token/secret VALUE —
current_identityandpollsurface only the subject (identity) string, andcredential_namesonly the NAMES of the held credentials. So a secret value can never reach the pane or itsstate_json; the Sign-in robot test asserts no sentinel token appears in the observable state.holger-server-ui has NO vault, so the acquired token lives in the client's shipped default 0600 [
FileTokenStore] at~/.holger/auth-token.json— a same-host, owner-only convenience store (the same shape as korp's~/.korp/auth-token.json).
const DEFAULT_DEVICE_AUTH_URL: & str · src/login_seam.rs:38
Default device-authorization endpoint ($HOLGER_OIDC_DEVICE_AUTH_URL).
const DEFAULT_TOKEN_URL: & str · src/login_seam.rs:41
Default token endpoint ($HOLGER_OIDC_TOKEN_URL).
const DEFAULT_CLIENT_ID: & str · src/login_seam.rs:52
Default public client id for the operator UI ($HOLGER_OIDC_CLIENT_ID). A PUBLIC client id (not a secret) — the device-code grant carries no client secret.
⚠ Deliberately still holger-ui, though the package was renamed to holger-server-ui on 2026-08-01. This string is not our name to change: it is the id the client is REGISTERED under at the IdP, and renaming it here would make every existing Keycloak/Entra/Okta registration reject the device-code request with invalid_client. Change it only together with the IdP registration (or per deployment via $HOLGER_OIDC_CLIENT_ID).
const DEFAULT_SCOPE: & str · src/login_seam.rs:55
Default requested scope ($HOLGER_OIDC_SCOPE). offline_access is what makes the IdP return a refresh token so the session can be silently refreshed.
const APP: & str · src/login_seam.rs:59
The per-app store name — holger's default 0600 file lives at ~/.holger/auth-token.json.
fn login_config_from_env() -> LoginConfig · src/login_seam.rs:70
Build a [LoginConfig] from the HOLGER_OIDC_* env vars, falling back to the Keycloak-dev-shaped placeholders above (mirrors korp's KORP_OIDC_*):
| env | meaning | default |
HOLGER_OIDC_DEVICE_AUTH_URL |
RFC 8628 device-authorization endpoint | [DEFAULT_DEVICE_AUTH_URL] |
HOLGER_OIDC_TOKEN_URL |
token endpoint (poll + refresh) | [DEFAULT_TOKEN_URL] |
HOLGER_OIDC_CLIENT_ID |
public client id | [DEFAULT_CLIENT_ID] |
HOLGER_OIDC_SCOPE |
requested scope | [DEFAULT_SCOPE] |
fn subject_of(access_token: &str) -> Option<String> · src/login_seam.rs:90
Best-effort extract of a human identity from an access token for DISPLAY only. Decodes the JWT payload (NO signature check — that is the SERVER's job on presentation) and prefers preferred_username → email → sub. None for an opaque (non-JWT) token or a payload without those claims. Mirrors korp's subject_of (the shared client has no display decoder — this stays presentational).
struct HolgerLoginSeam · src/login_seam.rs:130
holger-server-ui's host [LoginSeam]: the RFC 8628 device-code grant over the shared [nornir-auth-client], persisting the acquired token in the default 0600 file store. Holds the in-flight DeviceCode between begin_device_code and poll.
Module holger_server_ui::login_seam::HolgerLoginSeam
fn from_env() -> Self · src/login_seam.rs:140
Build the seam from the HOLGER_OIDC_* env config + the default ~/.holger/auth-token.json file store (the operator UI's live sign-in).
fn with_config_and_store(cfg: LoginConfig, store: FileTokenStore) -> Self · src/login_seam.rs:150
Build over an explicit config + store — the seam a test points at a stub IdP
- a temp-dir token file (no real network, no
~/.holgerwrite).
Module holger_server_ui::robot_remote
robot-remote— MOUNT holger-server-ui as a WIRE-REMOTE robot-drive target.This flips holger from a probe-only fleet member to a really-driven one: edda's [
fleet] harness can already drive an egui member's mountedCmdJsonServerover a byte stream (the fat leg,UnixEguiWire), but the honest gap was per-app — "each app must MOUNT aCmdJsonServerover its egui UI on a socket". holger-server-ui mounts it here, exactly the way dwarves-demo did (robot_remote.rs+serve_robot_remote).What this is (REUSE, no second drive path)
- The command vocabulary is the ONE robotui
Action/Assert/Target/Stepset carried bycmd_json— the SAME the co-located kittest robot suite (tests/robot_ui_app.rs) drives through a [RobotSession]. Nothing new.- The host is a [
RobotSession] over holger-server-ui's OWNegui_kittestshell (the SAMEHarness::build_state(|ctx, a| a.draw_ui(ui), …)shape the robot tests build — REUSE, not a twin), and the app's observable state ISHolgerUiApp::state_json(no twinned oracle).- The transport is an [
IoChannel] over a realAF_UNIX[UnixListener]; edda's reusableUnixEguiWiredials the exact socket [serve_robot_remote] binds.Member socket name
The fleet dials
<dir>/holger.sock— the socket BASENAME is the member name (holger). [serve_robot_remote] binds whatever path the--robot-remote <socket>flag hands it; the fleet is the side that chooses theholger.sockbasename.Concurrency (ROOT LAW #0)
The server is plain BLOCKING I/O pumped single-threaded — it spawns NOTHING. The mount runs on the process's own thread (the CLI hands
mainover to it); a driver is served sequentially via [CmdJsonServer::serve] until it hangs up (EOF).Scope
Feature-gated behind
robot-remoteand reached ONLY via the--robot-remote <socket>CLI flag, so the SHIPPED default binary never mounts an …
fn serve_robot_remote(path: &Path) -> io::Result<()> · src/robot_remote.rs:156
MOUNT the CmdJsonServer on a Unix socket at path and serve drivers on THIS (the app's own) blocking thread — NO thread::spawn (ROOT LAW #0). One driver at a time, sequentially: the fleet opens a fresh CmdJsonClient per tab drive, so serve blocks answering that driver until it hangs up (EOF), then the next connection is accepted. A per-connection I/O error is logged, not fatal. Never returns except on an accept error.
The fleet dials <dir>/holger.sock; the caller passes that path here.
Module holger_server_ui::scenarios
robot-remote— the per-tab robot scenario library for holger.The North-Star autotest fleet drives each app member WIRE-REMOTE over
cmd_json. Until now the fleet'sRobotDriveMemberhad no per-tab script for holger, so it fell back to a bare liveness probe (ping + read_state). This module closes that gap — mirroring dwarves'dwarves-demo/src/scenarios.rs: it records a REALScenarioper holger operator tab — SELECT the tab, perform the meaningful interactions it supports, and ASSERT over the tab's OWNstate_jsonthat the expected state changed.REUSE (no second vocabulary, no second oracle)
- The scenarios are built from the ONE robotui vocabulary (
Action/Assert/Step/Scenario) the fleet already carries — the SAMEStepwire form aCmdJsonClientdrives. Nothing new is invented.- Every assertion reads the tab's EXISTING
state_jsonoracle (the shell's/tabactive-tab id + each pane's own folded sub-state under its own key), or the harness'es ownAssert::Drew— no twinned per-tab oracle. holger's active-tab value is the tab's stableslot_id(a lower-cased id), while the tab-bar button a robot CLICKS renders the tab's displaylabel(), so each scenario carries both (SELECT clicks the label and asserts theslot_idmoved).Consumable as DATA
[
holger_tab_scenarios] returns aVec<Scenario>(serde-serializable to the SAMEStepwire form the fleet uses under thewirefeature), so the fleet'sRobotDriveMembercan LOAD holger's scenarios by member name (<dir>/holger.scenarios.json) and drive them over the socketserve_robot_remotemounts. [holger_tab_scenario_library] carries the same data plus per-tab m …
struct HolgerTabScenario { tab: & 'static str, label: & 'static str, probe_only: bool, scenario: Scenario } · src/scenarios.rs:45
One holger operator tab's recorded scenario plus its metadata.
scenario.suite carries the tab's slot_id, so a consumer that only wants the wire data can take [Self::scenario] and still know which tab it drives.
fn holger_tab_scenario_library() -> Vec<HolgerTabScenario> · src/scenarios.rs:100
The recorded per-tab scenario library — one [HolgerTabScenario] per registered holger operator tab, in tab-bar order (the shell's TAB_ORDER). The holger-side drive test drives every scenario here against the LIVE mounted shell, so a removed/renamed tab or a stale oracle goes RED there.
fn holger_tab_scenarios() -> Vec<Scenario> · src/scenarios.rs:251
The recorded scenarios as pure wire DATA — one [Scenario] per holger operator tab, each suite carrying the tab's slot_id. Serde-serializable to the SAME Step wire form the fleet's CmdJsonClient drives, so the fleet's RobotDriveMember can LOAD holger's per-tab scenarios by member name and drive them over the mounted socket.
Crate holger-traits
[contracts] RepositoryBackendTrait, ConnectorTrait, HolgerObject, ArtifactId/Format, znippy format-version pin. A znippy API change breaks the whole workspace via [patch.crates-io].
Foundational contracts and shared types for holger — the seam every backend, upstream, and connector implements, plus the coordinate/format vocabulary they all speak. This crate deliberately sits at the bottom of the dep graph and stays thin: it depends on znippy-common (for the source-of-truth type ids) but NOT on the sibling znippy-plugin-* crates.
The three server-side seams:
- [
RepositoryBackendTrait] — a repo that serves an ecosystem's native index from a read-only znippy archive (or disk);has_archive/archive_*default to "no archive" so file-backed and writable repos inherit them unchanged. - [
RemoteUpstream] — a read-orientedRepositoryBackendTraitthat pull-through- caches from Nexus/Artifactory/holger/crates.io; drops intoProxyBackendwherefetch()is the read and a writable primary write-through-caches the hit. Its HTTP is impl-side and SYNC (ureq), neverreqwest— the server request path runs inside the async hyper handler, where a blocked-onreqwestpanics. - [
HolgerObject] — the transport-agnostic facade (in-process engine vs gRPC client, oneArc<dyn>handle), the Rust mirror ofholger.proto.
Agent-side, [ConnectorTrait] moves assets between a Source and Target; its [UpstreamAuth] cousin here is the server-side auth enum, distinct from the agent's ConnectorAuth.
GOTCHA: znippy is the single source of truth for [ArtifactFormat::znippy_type_id] discriminants — most are derived by calling the znippy handler's type_id() directly so they can't drift, but Pip (2) and Maven3 (3) stay hardcoded literals because their native handlers live in the plugin crates this crate won't depend on. The znippy_type_ids_match_* tests guard those two.
Module holger_traits
struct ArtifactId { namespace: Option<String>, name: String, version: String } · src/lib.rs:45
enum ArtifactFormat { Maven3, Pip, Rust, Go, Nuget, Npm, Gem, Deb, Rpm, Helm, Docker, Conda, Composer, Znippy, Raw } · src/lib.rs:53
struct FormatHome { format: ArtifactFormat, config_key: & 'static str, aliases: & 'static [& 'static str], crate_dir: & 'static str, package: & 'static str, also: Option<(& 'static str, &'static str)> } · src/lib.rs:216
Where one [ArtifactFormat]'s backend lives, plus every other name the same capability is called by. See [ArtifactFormat::home] for why this is needed.
const ALL_ARTIFACT_FORMATS: & [ArtifactFormat] · src/lib.rs:244
Every format holger knows, in declaration order — so a caller can enumerate the capability set instead of hand-maintaining a second list.
enum StorageType { Znippy, Rocksdb } · src/lib.rs:264
enum RepositoryType { Rust, Pip, Maven3, Go, Nuget, Npm, Gem, Deb, Rpm, Helm, Docker, Conda, Composer, Znippy, Raw } · src/lib.rs:271
struct ArchiveInfo { file_count: u64, total_uncompressed_bytes: u64, archive_path: String } · src/lib.rs:316
Stats for the znippy archive backing a repository (mirror of the proto ArchiveInfoResponse). archive_path is the repository/archive name — a human-readable handle, not a filesystem path. Defaults to "no archive" (a backend that isn't archive-backed reports zero files / zero bytes).
trait RepositoryBackendTrait · src/lib.rs:323
enum UpstreamAuth { None, Basic, Bearer, ApiKey, Mtls } · src/lib.rs:438
How a remote-cache upstream authenticates with its registry. Promoted into traits as the single source of truth shared by every upstream impl (Nexus = Basic, Artifactory = Basic/Bearer/API-key, holger = Bearer).
Distinct from the agent-side ConnectorAuth (async/reqwest, agent-only), which keeps its own OIDC-token-fetch semantics; this enum is the server-side upstream contract.
struct ArtifactMeta { size: Option<u64>, sha256: Option<String>, content_type: Option<String> } · src/lib.rs:457
Size + checksum(s) + content-type for an upstream artifact. Lets the proxy verify a fetched body before write-through caching and answer index/metadata probes without downloading the body.
trait RemoteUpstream · src/lib.rs:467
A read-oriented remote-cache upstream: a [RepositoryBackendTrait] that knows its upstream base URL + auth and can cheaply probe existence/metadata. Plugs into ProxyBackend unchanged — fetch() is the read, a writable primary caches the hit.
fn upstream_path(fmt: ArtifactFormat, id: &ArtifactId) -> String · src/lib.rs:490
Map an artifact coordinate to the upstream layout path for fmt. This is the per-ecosystem translation Nexus/Artifactory share (maven2/npm/pypi/… layouts relative to the repository root). Joined onto the registry-specific content base ({base}/repository/{repo}/… for Nexus, {base}/artifactory/{repo}/… for Artifactory) by the impl.
struct RemoteRepository { name: String, format: String, repo_type: String, url: Option<String> } · src/lib.rs:514
struct RemoteAsset { path: String, download_url: String, content_type: Option<String>, size: Option<u64> } · src/lib.rs:522
trait ConnectorTrait · src/lib.rs:530
struct RepositoryInfo { name: String, repo_type: String, writable: bool, has_archive: bool } · src/lib.rs:561
Summary of one configured repository (mirror of the proto RepositoryInfo).
struct ArtifactEntry { id: ArtifactId, size_bytes: i64, content_type: String } · src/lib.rs:570
One artifact in a repository listing (mirror of the proto ArtifactEntry).
struct Health { status: String, version: String, uptime_seconds: i64 } · src/lib.rs:578
Server health (mirror of the proto HealthResponse).
struct ServerProfile { profile: String, read_only: bool, label: String, writable_repo_count: i32 } · src/lib.rs:591
Server operating profile (mirror of the proto ServerProfileResponse).
This is server truth: whether the server accepts writes at all, derived from the live route table (or an explicit sealed-mode override). The UI drives its static "silent running" chrome off this rather than a local toggle.
struct SearchQuery { name: String, namespace: String, version: String, path: String, repositories: Vec<String>, limit: i32 } · src/lib.rs:618
A cross-repo search query (mirror of the proto SearchRequest). Every axis is optional (empty = unconstrained); axes AND-compose on the server.
struct SearchArtifactHit { repository: String, id: ArtifactId, size_bytes: i64, content_type: String } · src/lib.rs:636
One artifact matched by a search, tagged with the repo it was found in (mirror of the proto ArtifactHit).
struct SearchPathHit { repository: String, path: String } · src/lib.rs:645
One raw archive path matched by the path axis (mirror of the proto PathHit).
struct SearchResults { artifacts: Vec<SearchArtifactHit>, paths: Vec<SearchPathHit>, truncated: bool } · src/lib.rs:652
A search result page (mirror of the proto SearchResponse).
struct SbomDoc { document: Vec<u8>, size_bytes: i64, content_address: String } · src/lib.rs:662
A hosted SBOM document for one artifact coordinate (mirror of the proto FetchSbomResponse).
struct PropertyKv { key: String, values: Vec<String> } · src/lib.rs:673
One custom property on an artifact coordinate: a key with its ordered values (mirror of the proto PropertyEntry).
trait HolgerObject · src/lib.rs:681
Transport-agnostic Holger facade. Callable from the CLI or a UI backend over either transport (direct in-process or gRPC) behind the same handle.
Module holger_traits::ArtifactFormat
fn znippy_type_id(&self) -> i8 · src/lib.rs:88
Canonical znippy pkg_type / DenseUnion discriminant for this format.
znippy is the single source of truth for these numbers (see znippy-common's native + skeleton handlers). Every variant whose handler is reachable from znippy-common calls that handler's [ArchiveTypePlugin::type_id] directly — including Rust, whose native cargo_native::CargoPlugin lives in znippy-common — so the two can never drift (the test below asserts the equality as data). Only Pip (2) and Maven3 (3) remain literals: their native handlers live in the sibling znippy-plugin-python / znippy-plugin-maven crates, which this foundational traits crate deliberately does NOT depend on (keeping the base dep graph thin). The znippy_type_ids_match_* tests assert those two literals still equal znippy's published discriminants.
fn from_format_str(s: &str) -> Option<Self> · src/lib.rs:119
Parse a Nexus/Artifactory/CLI format string into an ArtifactFormat.
fn home(&self) -> FormatHome · src/lib.rs:173
Where this format's backend actually LIVES. The answer to "I know the capability by one name — which crate implements it?"
A capability in holger routinely carries FOUR different names, and the one in the docs is usually not the one in the source tree. PyPI is the worst case, and it has already caused two wrong answers and a nearly-started 677-LOC re-implementation of code that was sitting right there:
| where | the name it uses |
| README / design docs / the UI | PyPI |
ron_repo_type an operator writes |
pip |
| this enum + the backend struct | Pip / PipRepoZnippy |
| the crate + its directory | znippy-python / holger-znippy-python-repository |
So grep -ri pypi server/ returns nothing from the implementation, and grep -ri python finds only a directory. from_format_str already maps the spoken aliases onto the variant, but it stops there — it never says where the code is. That last hop is the one that was missing, so this is it.
crate_dir is relative to server/repository/, or empty for a format whose backend is not a separate repository crate (Raw lives in server/lib/src/raw.rs). also names a SECOND backend for the same format where one exists (a file-backed peer of the znippy-backed one).
The mapping is asserted against the real directory layout by every_format_names_a_crate_that_exists, so a renamed crate breaks the build instead of quietly re-opening the trap.
Module holger_traits::ConnectorTrait
fn name(&self) -> &str · src/lib.rs:531
async fn list_repositories(&self) -> anyhow::Result<Vec<RemoteRepository>> · src/lib.rs:533
async fn list_assets(&self, repository: &str) -> anyhow::Result<Vec<RemoteAsset>> · src/lib.rs:535
async fn download_asset(&self, asset: &RemoteAsset) -> anyhow::Result<Vec<u8>> · src/lib.rs:537
async fn upload_asset(&self, repository: &str, path: &str, data: &[u8],) -> anyhow::Result<()> · src/lib.rs:539
Module holger_traits::FormatHome
fn all_names(&self) -> Vec<& 'static str> · src/lib.rs:237
Every name this capability answers to — the config key first, then the aliases. Handy for a "did you mean?" or a docs table.
Module holger_traits::HolgerObject
async fn fetch(&self, repository: &str, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> · src/lib.rs:683
Fetch an artifact by id from a named repository.
async fn put(&self, repository: &str, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> · src/lib.rs:686
Store an artifact (write-enabled repositories only).
async fn list_repositories(&self) -> anyhow::Result<Vec<RepositoryInfo>> · src/lib.rs:689
List all configured repositories.
async fn list_artifacts(&self, repository: &str, name_filter: Option<String>, limit: u32, page_token: Option<String>,) -> anyhow::Result<(Vec<ArtifactEntry>, String)> · src/lib.rs:699
List artifacts in a repository, optionally filtered by a name_filter substring, capped at limit, paged via an opaque page_token. Returns the page of entries plus the next_page_token (empty when exhausted).
The default implementation returns an empty page, so transports that do not (yet) surface a listing — e.g. the in-process [HolgerObject]s whose server returns empty today — inherit it unchanged. Override it where the transport carries a real ListArtifacts call (the gRPC remote does).
async fn list_archive_files(&self, repository: &str, prefix: Option<String>,) -> anyhow::Result<Vec<String>> · src/lib.rs:718
List the raw file paths inside the znippy archive backing repository, optionally filtered to those starting with prefix. These are archive-raw paths (format-agnostic), not parsed artifact ids.
The default returns an empty list, so transports that don't (yet) surface archive browsing inherit it unchanged. Override it where the transport carries a real ListArchiveFiles call (the gRPC remote and in-process local both do).
async fn archive_info(&self, repository: &str) -> anyhow::Result<ArchiveInfo> · src/lib.rs:729
Stats for the znippy archive backing repository (file count + total uncompressed bytes + name). Default: the empty [ArchiveInfo].
async fn health(&self) -> anyhow::Result<Health> · src/lib.rs:735
Server health / version / uptime.
async fn server_profile(&self) -> anyhow::Result<ServerProfile> · src/lib.rs:743
Server operating profile (static/read-only vs dynamic/writable).
The default returns a dynamic (writable) profile so transports that don't report one inherit it unchanged. Override it where the transport can derive real server truth (the gRPC remote via AdminService. ServerProfile, the in-process local from its route table).
async fn search(&self, query: &SearchQuery) -> anyhow::Result<SearchResults> · src/lib.rs:752
Cross-repo artifact search (SearchService.Search) — an open read.
The default returns the empty result set, so transports that don't (yet) surface search inherit it unchanged. Override it where the transport carries the real RPC (the gRPC remote does).
async fn fetch_sbom(&self, repository: &str, id: &ArtifactId,) -> anyhow::Result<Option<SbomDoc>> · src/lib.rs:759
Fetch the hosted SBOM document for a coordinate (SbomService.FetchSbom) — an open read; Ok(None) when none is attached. Default: none.
async fn get_properties(&self, repository: &str, id: &ArtifactId,) -> anyhow::Result<Vec<PropertyKv>> · src/lib.rs:771
Read all custom properties on a coordinate (PropertyService.GetProperties) — an open read; empty when none. Default: empty.
Module holger_traits::RemoteUpstream
fn base_url(&self) -> &str · src/lib.rs:469
The upstream base, e.g. https://nexus.corp or https://jfrog.corp.
fn auth(&self) -> &UpstreamAuth · src/lib.rs:472
Credentials attached to upstream requests.
fn exists(&self, id: &ArtifactId) -> anyhow::Result<bool> · src/lib.rs:476
Cheap existence probe. Default: a fetch that discards the body — impls should override with a HEAD where the registry supports one.
fn metadata(&self, id: &ArtifactId) -> anyhow::Result<Option<ArtifactMeta>> · src/lib.rs:482
Size + checksum + content-type, used to verify before write-through caching. Ok(None) when the upstream has no such artifact.
Module holger_traits::RepositoryBackendTrait
fn name(&self) -> &str · src/lib.rs:324
fn format(&self) -> ArtifactFormat · src/lib.rs:325
fn is_writable(&self) -> bool · src/lib.rs:326
fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>> · src/lib.rs:328
fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> · src/lib.rs:329
fn archive_files(&self, prefix: Option<& str>) -> anyhow::Result<Vec<String>> · src/lib.rs:335
List the raw file paths inside the backing znippy archive, optionally filtered to those starting with prefix. These are archive-raw paths (format-agnostic), not parsed artifact ids. Default: empty (a backend with no archive — or that can't enumerate one — returns nothing).
fn archive_info(&self) -> anyhow::Result<ArchiveInfo> · src/lib.rs:342
Stats for the backing znippy archive (file count + total uncompressed bytes + name). Default: the empty [ArchiveInfo] (no archive).
fn list(&self, name_filter: Option<& str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> · src/lib.rs:350
List artifacts this backend holds, newest/any order, optionally filtered by a substring of the artifact name, capped at limit. Default: empty (a backend that can't enumerate returns nothing — the contract stays valid, the listing just stays empty).
fn has_archive(&self) -> bool · src/lib.rs:358
Whether this backend is backed by a znippy archive. Defaults to false (file-backed / writable repos carry no archive). Znippy-backed repos override this to return true when their archive reader is loaded.
fn delete_artifact(&self, id: &ArtifactId, expected_content_id: &str) -> anyhow::Result<()> · src/lib.rs:379
Reclaim one artifact's bytes from this backend — the write side of the retention/GC executor (the planner in holger-server-lib::retention decides what; this carries the decision out for a single coordinate).
Safe by contract: expected_content_id is the lowercase-hex content digest the caller (executor) computed for the bytes it intends to drop; an implementation MUST recompute the digest of what is actually stored and refuse the unlink (returning Err) if it does not match — so a store that changed under the plan (a re-upload, a swap, a path collision) is never deleted blind. A missing artifact is an error (nothing to reclaim), never a silent success.
Default: Err — holger's backing store is immutable znippy archives, which have no in-place delete (retention there is a whole-generation archive-set drop, out of band). Only the writable, loose-file backends override this. Immutable/read-only backends fail closed here rather than pretend to reclaim.
fn handle_http2_request(&self, method: &str, suburl: &str, body: &[u8],) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)> · src/lib.rs:392
fn coordinate_for_path(&self, suburl: &str) -> Option<ArtifactId> · src/lib.rs:407
The artifact coordinate a raw HTTP suburl addresses, when the backend can map its own path scheme back to a coordinate (the inverse of the path it serves from). Used by the serve-time quarantine gate to look up an artifact's properties before the bytes are served over HTTP. Default None — a backend whose native protocol paths don't map cleanly to a single coordinate (npm packuments, Maven metadata, …) is simply not HTTP-gated by coordinate (the gRPC fetch_artifact path, which carries an explicit coordinate, always is). suburl includes the leading /<repo>/….
Module holger_traits::RepositoryType
fn endpoint_name(&self) -> &'static str · src/lib.rs:290
Module holger_traits::znippy_format
Reader-side znippy format-version gate — holger's own pin on the on-disk archive format it is willing to parse.
Why this exists (the defect it closes)
A znippy archive records the on-disk format version it was written with, in the Arrow schema metadata of every sub-index (
znippy_format_version). Holger is an artifact registry: an archive can arrive from anywhere — an agent across the airgap, a USB stick, a pull-through cache, a clientPUT. If a newer znippy wrote it, an older reader parsing it is reading a layout it does not know: at best a silent mis-parse, at worst an arrow downcast panic or a wild allocation inside the request handler..nornir/artifactory-parity-design.mdrecords the observed consequence — "a newer-written archive crashes an older reader and takes the server down" — and rates it P0. A registry any client can kill by uploading is not a registry.The contract
- The archive's recorded version is read before any of its rows are parsed.
- A version greater than [
MAX_SUPPORTED_ZNIPPY_FORMAT], or a recorded version that is not a plain number, is refused with an [ArchiveFormatError] — a typed, request-facing error that names the archive, what it recorded, and what this build reads.- A version equal to or lower than the pin, and an archive with no recorded version (legacy / pre-version archives), read exactly as before — this gate is additive and refuses nothing that works today.
- Anything that stops us determining the version (unreadable file, corrupt manifest, a panic inside the index reader) is not this gate's verdict: it returns "undetermined" and the normal open path reports its own error as before. The gate never converts an existing failure mode into a new one.
Deliberately independent of znippy's own check
znippy-commongrew an internalcheck_format_versionon its read path, and that is welcome — but holger must not depend on it. holger pins znippy …
const MAX_SUPPORTED_ZNIPPY_FORMAT: u32 · src/znippy_format.rs:61
The highest on-disk znippy archive format version this holger build parses.
Bump this only together with a reader that actually understands the new layout. It is deliberately a holger-side constant rather than a re-export of znippy_common::index::ZNIPPY_FORMAT_VERSION: the pin must be a statement about what holger can read, and it must keep compiling (and refusing) when holger is built against a znippy that has no such constant.
const ZNIPPY_FORMAT_VERSION_KEY: & str · src/znippy_format.rs:65
Arrow schema-metadata key under which a znippy archive records its on-disk format version. Matches znippy_common::index::FORMAT_VERSION_KEY.
struct ArchiveFormatError { path: String, recorded: String, max_supported: u32 } · src/znippy_format.rs:74
A znippy archive this build refuses to read: it records a format version that is newer than [MAX_SUPPORTED_ZNIPPY_FORMAT], or one that is not a number.
Carried as a typed error (not a bare string) so a request handler can downcast_ref it and answer the request — 503 with this message — instead of letting a mis-parse loose in the process.
fn recorded_format_version(path: &Path) -> Option<String> · src/znippy_format.rs:105
The archive's recorded format version, verbatim, or None when it cannot be determined (no recorded version, unreadable file, corrupt manifest, or a panic inside the index reader — all of which the normal open path reports itself).
Reads the manifest plus the Arrow stream schema of the first non-reserved sub-index; no archive rows and no blob bytes are touched. The whole read runs under [std::panic::catch_unwind] because path is attacker-controlled input and the point of this function is that hostile bytes must not escape it.
fn ensure_supported_format(path: &Path) -> Result<(), ArchiveFormatError> · src/znippy_format.rs:152
Refuse the archive at path if it records a format version this build cannot read. Ok(()) for a supported version, an unrecorded version, and for anything that cannot be determined (see [recorded_format_version]) — the gate only ever adds a refusal, it never masks an existing error.
struct FormatGate · src/znippy_format.rs:179
[ensure_supported_format] with the verdict memoised against the archive's identity on disk — its (length, mtime).
The writable dev-mode backends re-open their .znippy on every read (a put invalidates any cached view), so the gate sits on a hot path. A file whose length and mtime are unchanged is the same file, and its recorded format version cannot have changed; the memo turns the steady-state cost into one stat. A file that has changed — including one swapped underneath a running server — is re-gated.
Module holger_traits::znippy_format::FormatGate
fn new() -> Self · src/znippy_format.rs:186
fn check(&self, path: &Path) -> Result<(), ArchiveFormatError> · src/znippy_format.rs:192
Gate path, skipping the read when it is byte-identical (by (len, mtime)) to the last archive that passed.
Crate holger-update-bundle
Holger Secure Update Gateway — network-free signed update-bundle format (holger.update-bundle/v1), two-person approval + release-signature trust model, and offline verifier. No outbound networking by construction.
A network-free library defining the signed holger.update-bundle/v1 format, its two-person approval + release-signature trust model, and a fully offline verifier. It is reusable by the authoring CLI (holger-bundle), the standalone verifier (holger-verify), the offline server import path, and backup/scrub tooling. It has no server, UI, connector, or outbound-networking dependency by construction (the zero-egress link-audit enforces this).
- Container. A bundle is a znippy archive (no new binary format is invented) holding a fixed member layout:
manifest.json(envelope),core.json(the immutable candidate),payload/,evidence/,approvals/, andsignatures/. - Digests. Every payload and evidence member carries a SHA-256 and a BLAKE3 digest, recorded in
core.jsonand again in the release manifest. - Candidate digest.
core.jsonis written once;candidate_digest = sha256(core.json bytes). Every approval binds this digest. - Approvals. Each approval is a detached ed25519 signature over the exact approval-statement bytes. The approver's role is authorized by the signer certificate fingerprint against the trust policy — never by the role string in the statement.
- Two-person rule. The policy requires a minimum number of distinct approver certificates and specific roles, each satisfied by a distinct certificate; one certificate cannot satisfy two required roles. The release signer must be distinct from the approvers.
- Release manifest.
signatures/release-manifest.jsonrecords{path,size,sha256,blake3}for every member except itself,release.sig, andrelease.crt. The release signature (ed25519) covers the manifest's exact bytes, so it transitively covers every payload, evidence file,core.json, and every approval statement/signature/certificate. **A single changed byte in any covered …
Module holger_update_bundle::api
File-level convenience: load a
.holger.znippy, verify it, and (for extract) materialize its members into a fresh directory.extractalways verifies first; there is no production path that writes payloads without verification.
fn verify_bundle_file(path: &Path, policy: &TrustPolicy, target: &TargetSpec, local_state: Option<& AcceptedState>,) -> Result<VerifyReport> · src/api.rs:19
Load + verify a .holger.znippy bundle file, entirely offline.
fn extract_bundle_file(path: &Path, policy: &TrustPolicy, target: &TargetSpec, dest: &Path,) -> Result<VerifyReport> · src/api.rs:33
Verify, then materialize the verified members into dest. dest must be absent or an empty directory. Members are staged into a sibling temp directory and the whole tree is atomically renamed into place — verify-before-extract, with no partial tree ever visible under the final path.
fn write_members_to_empty_dir(members: &MemberMap, dest: &Path) -> Result<()> · src/api.rs:46
Write a verified member map into a fresh directory at dest, atomically.
Module holger_update_bundle::approval
Signed approval statements. Each approval is a detached ed25519 signature over the exact stored bytes of an approval statement that binds the candidate digest. Authorization of the claimed role is decided by the signer certificate fingerprint against the trust policy — never by the role string a signer writes into the statement.
enum Decision { Approve, Reject } · src/approval.rs:13
An approval decision. Only Approve authorizes; anything else is non-authorizing.
struct ApprovalStatement { candidate_digest: String, approval_role: String, signer_certificate_fingerprint: String, signer_subject: String, approved_at: DateTime<Utc>, decision: Decision, optional_comment: Option<String>, signature_algorithm: String, signature_path: String } · src/approval.rs:24
A signed approval statement (stored as approvals/<role>.<fingerprint>.approval.json).
The bytes of this struct (serialized once) are the exact message the detached signature covers. optional_comment is metadata only and is never part of authorization logic.
Module holger_update_bundle::approval::ApprovalStatement
fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> · src/approval.rs:45
The exact bytes signed / verified for this approval statement.
fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> · src/approval.rs:49
fn is_approve(&self) -> bool · src/approval.rs:54
True if this is an authorizing (approve) decision.
Module holger_update_bundle::bin::holger_bundle
holger-bundle— author, approve, and finalize holger update bundles.Workflow:
preparea candidate directory, collect twoapprovesignatures from distinct roles, thenfinalizeinto a signed.holger.znippy. Private keys are read only to sign and never copied into the bundle or printed.
Module holger_update_bundle::bin::holger_verify
holger-verify— the standalone, zero-egress offline verifier + extractor for holger update bundles. A verification failure exits non-zero. There is no--skip-verification:extractalways verifies before writing.
Module holger_update_bundle::container
The bundle container: a znippy archive holding the member layout. No new binary archive format is invented — znippy is used purely as an offline container.
All verification logic operates over an in-memory [
MemberMap] (path → exact bytes), so the security core is testable without znippy and, per the threat model, a bundle is verified against the same in-memory bytes it is later extracted from — there is no re-read window between verify and extract.
type MemberMap · src/container.rs:21
A bundle as a map of member path → exact bytes. BTreeMap keeps a deterministic order (matters for the release manifest and for reproducible packing).
fn pack_bundle(members: &MemberMap, out: &Path) -> Result<()> · src/container.rs:35
Write members to a .holger.znippy archive at out atomically: pack into a temp sibling, then rename into place. A partial archive never appears under the requested final filename; the temp file is removed on failure.
fn load_bundle(path: &Path) -> Result<MemberMap> · src/container.rs:69
Load and validate every member from a .holger.znippy archive. Enforces the hostile-input limits (member count, per-member size, aggregate uncompressed size with checked arithmetic) and path safety (traversal / duplicate / case-fold) before returning the member map.
fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> · src/container.rs:134
Atomically write bytes to path (temp sibling + rename), creating parents.
Module holger_update_bundle::core
core.json— the immutable release candidate (holger.update-bundle/v1).
core.jsonis written to disk once; every downstream digest and signature is computed over those exact stored bytes. The struct here is only used to produce those bytes (at prepare time) and to read fields during verify — never to recompute the candidate digest by reserialization.
struct BundleCore { schema_version: String, bundle_id: String, sequence: u64, created_at: DateTime<Utc>, expires_at: Option<DateTime<Utc>>, producer: String, supplier: String, product: String, release_version: String, supported_hardware_models: Vec<String>, supported_hardware_revisions: Vec<String>, source_provenance: SourceProvenance, payloads: Vec<PayloadEntry>, evidence: Vec<EvidenceEntry>, policy_result: PolicyResult, supersedes: Vec<String>, withdraws: Vec<String> } · src/core.rs:15
The immutable release candidate.
struct SourceProvenance { source_kind: String, source_uri: Option<String>, source_repository: Option<String>, source_asset_path: Option<String>, fetched_at: Option<DateTime<Utc>>, reported_size: Option<u64>, vendor_checksum: Option<String>, vendor_signature_status: Option<String>, tls_origin: Option<String>, transfer_receipt_digest: Option<String> } · src/core.rs:48
Where the artifact came from, recorded so every byte is traceable to its source.
struct PayloadEntry { relative_path: String, media_type: String, size_bytes: u64, sha256: String, blake3: String } · src/core.rs:73
A payload artifact (firmware / installer / archive) carried in payload/.
enum EvidenceKind { SourceReceipt, Sbom, Vex, Scan, Advisory, Exception, Other } · src/core.rs:84
The kind of an evidence document.
struct EvidenceEntry { relative_path: String, media_type: String, size_bytes: u64, sha256: String, blake3: String, evidence_kind: EvidenceKind } · src/core.rs:96
An evidence document carried in evidence/.
struct PolicyResult { policy_digest: String, advisory_snapshot_digest: Option<String>, verdict: String, non_production: bool } · src/core.rs:107
The recorded policy verdict + the exact advisory/policy snapshot it was made against.
Module holger_update_bundle::core::BundleCore
fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> · src/core.rs:128
Serialize to the exact bytes that become core.json. Deterministic (serde_json preserves struct field order + Vec order).
fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> · src/core.rs:133
Parse from stored core.json bytes.
fn schema_ok(&self) -> bool · src/core.rs:138
True if the schema version is the one this crate understands.
fn has_evidence_kind(&self, kind: EvidenceKind) -> bool · src/core.rs:143
True if at least one evidence entry of kind is present.
Module holger_update_bundle::digest
Content digests. Every bundle member carries both a SHA-256 and a BLAKE3 digest; verification recomputes both over the exact stored bytes.
Self-contained (
sha2+blake3) — the bundle's digests are a closed domain (they are compared only against other digests stored in the same bundle), so this crate does not couple to the wider workspace hash primitive.
fn hex_lower(bytes: &[u8]) -> String · src/digest.rs:12
Lowercase hex encoding of a byte slice.
fn hex_decode(s: &str) -> Option<Vec<u8>> · src/digest.rs:24
Decode a lowercase/uppercase hex string to bytes. Returns None on any non-hex character or odd length.
fn sha256_hex(bytes: &[u8]) -> String · src/digest.rs:45
SHA-256 of bytes as 64 lowercase hex characters.
fn blake3_hex(bytes: &[u8]) -> String · src/digest.rs:50
BLAKE3 of bytes as 64 lowercase hex characters.
struct Digests { size_bytes: u64, sha256: String, blake3: String } · src/digest.rs:56
The dual digest + size recorded for every payload and evidence member.
Module holger_update_bundle::digest::Digests
fn of(bytes: &[u8]) -> Self · src/digest.rs:64
Compute the dual digest of bytes.
fn matches(&self, bytes: &[u8]) -> bool · src/digest.rs:73
True when both digests and the size match bytes exactly.
Module holger_update_bundle::error
Typed errors for the update-bundle trust model.
Every variant carries a safe, path-free message suitable to surface to a caller: policy verdicts ("signature invalid", "sequence rollback 7 < 9", "untrusted signer") are meant to be shown, while raw IO / parse detail is collapsed into a short opaque string (the underlying error goes to the caller's log, never a filesystem path or certificate internal into a client channel).
type Result · src/error.rs:12
Result alias for the bundle core.
enum BundleError { UnsupportedSchema, UnsupportedPolicySchema, UnsafePath, LimitExceeded, MissingMember, UnreferencedMember, DigestMismatch, CandidateDigestMismatch, EnvelopeMismatch, MissingReleaseSignature, InvalidReleaseSignature, InvalidApprovalSignature, UntrustedSigner, UntrustedReleaseSigner, TwoPersonRule, MissingRole, Expired, TooOld, MissingExpiry, MissingEvidence, TargetMismatch, SequenceRollback, SequenceReplay, BundleIdReuse, Certificate, Policy, Malformed, Container, Io, Serde } · src/error.rs:17
A bundle-processing error. Messages are safe to display; they never embed a local filesystem path, certificate PEM/DN, or a policy secret.
Module holger_update_bundle::error::BundleError
fn code(&self) -> &'static ErrCode · src/error.rs:126
The FROZEN holger-update-<n> code for this variant — the machine half of the dual channel (the #[error(..)] string above is the human half).
The match is exhaustive on purpose: adding a BundleError variant without allocating it a code is a COMPILE error, not a runtime surprise. That is what keeps the registry honest as the trust model grows.
See .nornir/holger-error-codes.md and the holger-errcode crate.
fn tagged(&self) -> String · src/error.rs:167
BOTH channels in one string: [holger-update-22] sequence rollback 7 < 9.
This is what an operator sees and what an audit row records — the code makes the verdict machine-checkable, the prose keeps it readable. Use this at every site that surfaces a BundleError to a human, a log, or a client.
pub(crate) fn io(context: &str, e: std::io::Error) -> Self · src/error.rs:171
Module holger_update_bundle::error::errcode_tests
The Secure Update Gateway's trust verdicts are the errors that MOST need to be machine-checkable: an operator must be able to tell "your signature is invalid" from "this bundle is a replay" from "the disk is full", and an automated importer must branch on that without parsing English.
These assert the CODE, never a substring of the prose.
Module holger_update_bundle::finalize
Approval and finalization.
add_approvalwrites a detached-signed approval statement bound to the candidate digest.finalizeverifies every approval against the trust policy, enforces the two-person rule, then builds and signs the release manifest — and self-verifies the result so a bundle that would not verify is never emitted.
fn candidate_digest(members: &MemberMap) -> Result<String> · src/finalize.rs:44
SHA-256 of the exact core.json bytes — the candidate digest.
fn add_approval(members: &mut MemberMap, role: &str, signer_cert_der: &[u8], signer_key_pkcs8: &[u8], comment: Option<String>, approved_at: DateTime<Utc>,) -> Result<ApprovalRecord> · src/finalize.rs:54
Add a signed approval to a candidate member map. Signs the exact approval statement bytes with the approver's ed25519 key (PKCS#8); the private key never enters the bundle and never appears in any output.
fn finalize(members: &mut MemberMap, policy: &TrustPolicy, release_cert_der: &[u8], release_key_pkcs8: &[u8], now: DateTime<Utc>,) -> Result<VerifyReport> · src/finalize.rs:97
Verify approvals + policy, then build and sign the release manifest, mutating members into a finalized bundle. Returns the verification report of the finalized bundle (finalize refuses to emit anything that does not verify).
Module holger_update_bundle::manifest
The release manifest and envelope.
signatures/release-manifest.jsonrecords{path, size, sha256, blake3}for every member except itself,release.sig, andrelease.crt. The release signature covers this manifest's exact bytes, so it transitively covers every payload, evidence file,core.json,manifest.json, and every approval statement/signature/certificate. Any changed byte in any covered member changes a digest here and fails verification before the signature is even checked.
struct MemberDigest { relative_path: String, size_bytes: u64, sha256: String, blake3: String } · src/manifest.rs:22
A single member's digest record.
struct ReleaseManifest { schema_version: String, bundle_id: String, candidate_digest: String, members: Vec<MemberDigest> } · src/manifest.rs:31
signatures/release-manifest.json.
fn is_self_referential(path: &str) -> bool · src/manifest.rs:42
The members that the release manifest does NOT (and cannot) cover: itself and the release signature + certificate.
struct EnvelopeManifest { schema_version: String, bundle_id: String, sequence: u64, supplier: String, product: String, release_version: String, created_at: DateTime<Utc>, member_count: usize, core_digest: String } · src/manifest.rs:129
manifest.json — a small human/inspect-friendly envelope, itself covered by the release manifest.
Module holger_update_bundle::manifest::EnvelopeManifest
fn to_json_bytes(&self) -> Result<Vec<u8>> · src/manifest.rs:143
fn from_json_bytes(bytes: &[u8]) -> Result<Self> · src/manifest.rs:147
Module holger_update_bundle::manifest::ReleaseManifest
fn build(members: &MemberMap, bundle_id: &str, candidate_digest: &str) -> Self · src/manifest.rs:48
Build the manifest over every member except the self-referential three.
fn to_json_bytes(&self) -> Result<Vec<u8>> · src/manifest.rs:68
fn from_json_bytes(bytes: &[u8]) -> Result<Self> · src/manifest.rs:72
fn index(&self) -> BTreeMap<& str, &MemberDigest> · src/manifest.rs:77
Index the members by path for O(1) lookup.
fn verify_members(&self, members: &MemberMap) -> Result<()> · src/manifest.rs:87
Verify every covered member's bytes against this manifest, and that there are no extra unreferenced members and no missing referenced members. Any changed byte in any covered member is caught here.
Module holger_update_bundle::pathsafe
Member-path safety. Every bundle is hostile input; a member path must be a relative, forward-slash, traversal-free path with no ambiguity that could escape an extraction root or collide on a case-insensitive filesystem.
This mirrors the server's
raw::safe_relpathcomponent rules (empty /./../ backslash / NUL) and adds what none of the existing helpers cover: duplicate-normalized-path and case-folding-collision detection across the set of members (both are manifest-forgery signals). Symlink / hard-link members cannot occur — a znippy archive stores file bytes, not link entries — and extraction always creates the parent directories itself into a fresh empty directory, so no pre-existing symlink is ever followed.
fn validate_member_path(path: &str) -> Result<()> · src/pathsafe.rs:19
Validate a single member path in isolation. Rejects absolute paths, .., empty components, backslashes, and NUL bytes.
struct PathGuard · src/pathsafe.rs:52
Accumulates the set of accepted member paths and rejects duplicates and case-folding collisions across the whole bundle.
Module holger_update_bundle::pathsafe::PathGuard
fn new() -> Self · src/pathsafe.rs:58
fn check(&mut self, path: &str) -> Result<()> · src/pathsafe.rs:65
Validate path and register it. Returns an error if the path is unsafe, a byte-identical duplicate, or collides case-insensitively with a member already seen.
Module holger_update_bundle::policy
holger.bundle-trust-policy/v1— the offline trust policy a verifier enforces.The policy is a RON document. Role authorization is decided strictly by signer certificate fingerprint (SHA-256 of the cert DER), never by a role string a signer writes into its own statement.
trusted_roots(when non-empty) requires each signer/release certificate to chain to one of the supplied roots; when empty, the policy operates in certificate-pinning mode (a signer is trusted iff its fingerprint is listed inapproved_signers/trusted_release_signers).
struct ApprovedSigner { fingerprint: String, subject: Option<String>, roles: Vec<String> } · src/policy.rs:18
A signer authorized to exercise one or more roles.
struct TrustPolicy { schema_version: String, trusted_roots: Vec<String>, approved_signers: Vec<ApprovedSigner>, required_approval_roles: Vec<String>, minimum_distinct_approvers: usize, trusted_release_signers: Vec<String>, release_signer_must_be_distinct: bool, maximum_bundle_age_secs: Option<u64>, require_expiry: bool, require_sbom: bool, require_vex: bool, require_scan: bool, allowed_products: Vec<String>, allowed_models: Vec<String> } · src/policy.rs:30
The versioned trust policy.
Module holger_update_bundle::policy::TrustPolicy
fn from_ron(text: &str) -> Result<Self> · src/policy.rs:90
Parse a policy from RON text.
fn to_ron(&self) -> Result<String> · src/policy.rs:103
Serialize to pretty RON.
fn root_certs_der(&self) -> Result<Vec<Vec<u8>>> · src/policy.rs:109
Decode the trusted-root certificates from hex DER.
fn signer(&self, fingerprint: &str) -> Option<& ApprovedSigner> · src/policy.rs:121
Look up the approved-signer record for a fingerprint.
fn authorizes_role(&self, fingerprint: &str, role: &str) -> bool · src/policy.rs:128
True if fingerprint is authorized for role.
fn is_release_signer(&self, fingerprint: &str) -> bool · src/policy.rs:135
True if fingerprint is a permitted release signer.
Module holger_update_bundle::prepare
Candidate preparation: assemble
core.json, the envelope, and the payload + evidence members. Pure and in-memory; disk helpers wrap it for the CLI.
struct PayloadInput { relative_path: String, bytes: Vec<u8>, media_type: String } · src/prepare.rs:20
A payload to include: path relative to payload/, its bytes, and a media type.
struct EvidenceInput { relative_path: String, bytes: Vec<u8>, media_type: String, kind: EvidenceKind } · src/prepare.rs:28
An evidence document to include: path relative to evidence/, bytes, media type, and kind.
struct Preparation { bundle_id: String, sequence: u64, created_at: DateTime<Utc>, expires_at: Option<DateTime<Utc>>, producer: String, supplier: String, product: String, release_version: String, supported_hardware_models: Vec<String>, supported_hardware_revisions: Vec<String>, source_provenance: SourceProvenance, policy_result: PolicyResult, supersedes: Vec<String>, withdraws: Vec<String>, payloads: Vec<PayloadInput>, evidence: Vec<EvidenceInput> } · src/prepare.rs:36
The inputs to a candidate bundle.
struct Candidate { members: MemberMap, candidate_digest: String } · src/prepare.rs:56
A prepared candidate: the members and the candidate digest over core.json.
fn write_members_dir(members: &MemberMap, dir: &Path) -> Result<()> · src/prepare.rs:155
Write a member map to a candidate directory (each member at its relative path).
fn read_members_dir(dir: &Path) -> Result<MemberMap> · src/prepare.rs:168
Read a candidate directory into a member map, enforcing path safety.
Module holger_update_bundle::prepare::Preparation
fn build(mut self) -> Result<Candidate> · src/prepare.rs:64
Build the candidate member map + candidate digest. Rejects unsafe payload / evidence paths and evidence/payload collisions.
Module holger_update_bundle::receipt
Machine-readable receipts emitted by finalize and import. Receipts never carry a private key, token, password, or secret file content — only identities, digests, and counts.
struct FinalizationReceipt { bundle_id: String, sequence: u64, supplier: String, product: String, release_version: String, candidate_digest: String, release_signer_fingerprint: String, approvals: Vec<ApprovalRecord>, finalized_at: DateTime<Utc>, bundle_sha256: String, bundle_size_bytes: u64 } · src/receipt.rs:12
Emitted by holger-bundle finalize.
struct ImportReceipt { bundle_id: String, sequence: u64, supplier: String, product: String, release_version: String, candidate_digest: String, release_signer_fingerprint: String, imported_at: DateTime<Utc>, outcome: String, bundle_sha256: String } · src/receipt.rs:29
Emitted by holger-server bundle import.
Module holger_update_bundle::rollback
Anti-rollback state, per
(supplier, product).Each accepted bundle leaves one marker file
<NNNN>.acceptedwritten withcreate_new(true)— race-safe (two importers, one wins withAlreadyExists) and crash-safe (a torn marker is simply re-created). The reduced in-memory view ([AcceptedState]) is the highest accepted sequence plus, per sequence, the(bundle_id, core_digest)that was accepted — enough to reject a downgrade, a same-sequence-different-bytes replay, and a bundle-id reuse.
struct AcceptedEntry { sequence: u64, bundle_id: String, core_digest: String, accepted_at: DateTime<Utc> } · src/rollback.rs:23
One accepted bundle's recorded identity.
struct AcceptedState · src/rollback.rs:32
The reduced accepted state for one (supplier, product).
enum Admissibility { New, IdempotentReimport } · src/rollback.rs:38
Whether a bundle may be admitted, and if so whether it is a no-op re-import.
struct RollbackStore · src/rollback.rs:112
On-disk store of accepted-sequence markers.
Module holger_update_bundle::rollback::AcceptedState
fn empty() -> Self · src/rollback.rs:46
fn from_entries(entries: Vec<AcceptedEntry>) -> Self · src/rollback.rs:50
fn highest_accepted_sequence(&self) -> Option<u64> · src/rollback.rs:58
fn check_admissible(&self, core: &BundleCore, candidate_digest: &str,) -> Result<Admissibility> · src/rollback.rs:65
Decide whether core (with the given candidate digest) may be admitted. Rejects: a lower sequence; a reused sequence with different bytes; a reused bundle id with different bytes.
Module holger_update_bundle::rollback::RollbackStore
fn open(root: impl AsRef<Path>) -> Self · src/rollback.rs:118
Open (do not create) a store rooted at root.
fn load(&self, supplier: &str, product: &str) -> Result<AcceptedState> · src/rollback.rs:131
Load the accepted state for a (supplier, product).
fn record(&self, supplier: &str, product: &str, entry: &AcceptedEntry) -> Result<()> · src/rollback.rs:153
Record an accepted bundle. Race-safe via create_new; a second record of an identical (sequence, bundle_id, core_digest) is an idempotent no-op, but a conflicting record at the same sequence is refused.
Module holger_update_bundle::schema
Schema identifiers, well-known member paths, and hostile-input resource limits.
const BUNDLE_SCHEMA: & str · src/schema.rs:4
The bundle schema this crate produces and accepts.
const POLICY_SCHEMA: & str · src/schema.rs:7
The trust-policy schema this crate produces and accepts.
const BUNDLE_EXTENSION: & str · src/schema.rs:22
The recommended file extension for a finalized bundle.
const MAX_MEMBERS: usize · src/schema.rs:27
Maximum number of members in a bundle archive.
const MAX_MEMBER_BYTES: u64 · src/schema.rs:30
Maximum uncompressed size of a single member (decompression-bomb guard, per member).
const MAX_TOTAL_BYTES: u64 · src/schema.rs:33
Maximum aggregate uncompressed size across all members (bomb guard, total).
const MAX_CONTROL_BYTES: u64 · src/schema.rs:36
Maximum size of a JSON control document (core.json / manifest.json / release-manifest.json).
const MAX_APPROVALS: usize · src/schema.rs:39
Maximum number of approval statements considered.
Module holger_update_bundle::signer
Signature and certificate primitives for the bundle trust model.
Signatures are standard ed25519 over exact stored bytes — no new algorithm and no proprietary format. Signer identity is an X.509 certificate (DER); the fingerprint is
sha256(cert DER)and the subject is the certificate subject. The ed25519 public key is taken from the certificate's SPKI. Chain verification (leaf signed by a trusted root) usesx509-parser's ring-backedverify_signature— no OpenSSL.
struct SignerCert · src/signer.rs:19
A parsed signer certificate.
fn sign_pkcs8(pkcs8_der: &[u8], msg: &[u8]) -> Result<Vec<u8>> · src/signer.rs:84
Sign msg with an ed25519 private key supplied as PKCS#8 DER. Returns the 64-byte detached signature. The private key never leaves this function.
fn chains_to_root(leaf: &SignerCert, roots: &[SignerCert]) -> bool · src/signer.rs:97
True if leaf chains to one of roots: either it IS a trusted root (fingerprint match, i.e. a pinned self-signed anchor) or it is directly signed by a trusted root. An empty roots set means certificate-pinning mode (no chain requirement) — the caller enforces fingerprint authorization separately.
Module holger_update_bundle::signer::SignerCert
fn parse(der: &[u8]) -> Result<Self> · src/signer.rs:29
Parse an X.509 certificate (DER). The certificate must carry a 32-byte ed25519 public key.
fn fingerprint(&self) -> &str · src/signer.rs:51
SHA-256 of the certificate DER (lowercase hex) — the identity used by the policy.
fn subject(&self) -> &str · src/signer.rs:56
The certificate subject.
fn der(&self) -> &[u8] · src/signer.rs:61
The certificate DER bytes.
fn verify(&self, msg: &[u8], sig: &[u8]) -> Result<()> · src/signer.rs:66
Verify a detached ed25519 signature over msg under this certificate's key.
Module holger_update_bundle::tests
Adversarial + round-trip unit tests for the bundle trust model.
Module holger_update_bundle::tests::approvals
Approval-policy adversarial tests. These drive the approval + two-person logic directly (before finalize) to isolate it from the release-manifest coverage.
Module holger_update_bundle::tests::integrity
Bundle-integrity adversarial tests: tamper detection, structural checks, expiry, and chain-to-root.
Module holger_update_bundle::tests::roundtrip
Real znippy container round-trip: pack → load → verify → extract, plus a tampered-archive check.
Module holger_update_bundle::tests::targeting
Targeting + anti-rollback tests.
Module holger_update_bundle::tests::testkit
Test-only PKI + bundle builders. Uses rcgen (a dev-dependency) to mint ed25519 signer certificates in-process — never a committed private key.
struct TestSigner { cert_der: Vec<u8>, key_pkcs8: Vec<u8>, fingerprint: String } · src/tests/testkit.rs:15
struct TestCa { cert: rcgen::Certificate, key: KeyPair, der: Vec<u8>, fingerprint: String } · src/tests/testkit.rs:22
fn self_signed(cn: &str) -> TestSigner · src/tests/testkit.rs:30
A self-signed ed25519 signer (certificate-pinning mode).
fn ca(cn: &str) -> TestCa · src/tests/testkit.rs:44
A self-signed ed25519 CA.
fn issue_leaf(cn: &str, ca: &TestCa) -> TestSigner · src/tests/testkit.rs:60
A leaf ed25519 certificate issued by ca.
fn pinned_policy(reviewer: &TestSigner, owner: &TestSigner, release: &TestSigner,) -> TrustPolicy · src/tests/testkit.rs:75
A pinned-fingerprint critical policy: 2 distinct approvers (security-reviewer + system-owner), a distinct trusted release signer, SBOM required, expiry required.
struct BuiltBundle { members: MemberMap, policy: TrustPolicy, reviewer: TestSigner, owner: TestSigner, release: TestSigner, candidate_digest: String, bundle_id: String, now: DateTime<Utc> } · src/tests/testkit.rs:110
fn valid_bundle_seq(sequence: u64, bundle_id: &str) -> BuiltBundle · src/tests/testkit.rs:122
Build a valid, two-approval, release-signed bundle (pinned mode) at sequence.
fn valid_bundle() -> BuiltBundle · src/tests/testkit.rs:170
fn base_preparation(sequence: u64, bundle_id: &str, now: DateTime<Utc>) -> Preparation · src/tests/testkit.rs:174
fn hex(bytes: &[u8]) -> String · src/tests/testkit.rs:229
Hex-encode a byte slice (used to embed a CA DER into a trust policy).
Module holger_update_bundle::verify
The offline verification pipeline. Operates over an in-memory [
MemberMap] so it is fully testable without a container and so a bundle is verified against the exact bytes it will be extracted from.Order of checks (fail-closed, most-fundamental first):
- enumerate members, path-safety, count/size limits (done at load time)
- release-manifest present; every member's dual digest matches it; no extra / missing members
- release signature valid over the manifest bytes; release signer authorized + chains to a trusted root
- candidate digest = sha256(core.json) matches the manifest; parse core
- envelope (manifest.json) consistent with core
- every approval signature valid over exact statement bytes; signer authorized for the claimed role by fingerprint; chains to a trusted root
- two-person rule: distinct approvers, required roles each by a distinct cert, release signer distinct
- expiry / maximum age
- required evidence present
- target product / model / revision
- sequence validity against local accepted state
struct TargetSpec { product: Option<String>, model: Option<String>, revision: Option<String> } · src/verify.rs:40
The target the bundle is being verified against (all optional; None = any).
struct ApprovalRecord { role: String, fingerprint: String, subject: String } · src/verify.rs:48
A recorded, verified approval.
struct VerifyReport { bundle_id: String, sequence: u64, supplier: String, product: String, release_version: String, candidate_digest: String, release_signer_fingerprint: String, release_signer_subject: String, approvals: Vec<ApprovalRecord>, non_production_evidence: bool, checks: Vec<String> } · src/verify.rs:56
The result of a successful verification.
pub(crate) fn member<'a>(members: &'a MemberMap, path: &str) -> Result<& 'a Vec<u8>> · src/verify.rs:72
pub(crate) fn collect_roots(policy: &TrustPolicy) -> Result<Vec<SignerCert>> · src/verify.rs:90
Parse the trusted-root certificates from the policy.
pub(crate) fn verify_approvals(members: &MemberMap, policy: &TrustPolicy, candidate_digest: &str, roots: &[SignerCert],) -> Result<Vec<ApprovalRecord>> · src/verify.rs:101
Verify every approval statement's signature over its exact stored bytes, bind each to candidate_digest, and authorize the claimed role strictly by the signer certificate fingerprint against policy. Returns the approving records.
pub(crate) fn enforce_two_person(policy: &TrustPolicy, approvals: &[ApprovalRecord], release_fp: &str,) -> Result<()> · src/verify.rs:170
Enforce distinct-approver count, required-role coverage by distinct certificates, and release-signer distinctness. One certificate cannot satisfy two required roles.
fn verify_member_map(members: &MemberMap, policy: &TrustPolicy, target: &TargetSpec, local_state: Option<& AcceptedState>, now: DateTime<Utc>,) -> Result<VerifyReport> · src/verify.rs:249
Verify a bundle held entirely in memory. now is passed explicitly for deterministic testing; the file-level wrapper uses the wall clock.
Crate holger-world-connector
[connectors] External registries (crates.io/PyPI); SSRF host allowlist; list_assets bails (lockfile-driven).
World connector — the agent's internet-side Source for public registries (crates.io + PyPI, the latter under [pip]), implementing [ConnectorTrait].
Unlike Nexus/Holger connectors there is no browsable listing: list_assets deliberately errors. The real entry points are the free functions — [parse_lockfile] (Cargo.lock) / pip::parse_* produce [Package]s, then [packages_to_assets] / pip_packages_to_assets turn them into [RemoteAsset]s whose download_url points at the upstream CDN. You resolve a lockfile, then hand each asset to download_asset to fetch bytes bound for a znippy archive.
Gotcha: download URLs come from untrusted upstream JSON, so this is an SSRF surface. Two guards, both load-bearing — [is_allowed_download_url] pins https + an allow-listed host (H4), and the client sets redirect::Policy::none() because the allowlist only vets the initial URL, so a compromised allow-listed host must not be able to 3xx into an internal target. Bodies are read through [read_capped] to bound agent memory (M6).
Module holger_world_connector
struct WorldConnector · src/lib.rs:32
fn is_allowed_download_url(url: &str) -> bool · src/lib.rs:76
Validates that url is an https URL on an allow-listed host. Returns the reason on rejection so callers can surface a useful error.
struct Package { name: String, version: String, source: Option<String>, checksum: Option<String>, dependencies: Option<Vec<String>> } · src/lib.rs:112
fn parse_lockfile(path: &Path) -> Result<Vec<Package>> · src/lib.rs:142
Parse a Cargo.lock file and return all packages from crates.io
fn packages_to_assets(packages: &[Package]) -> Vec<RemoteAsset> · src/lib.rs:205
Helper: convert parsed packages into RemoteAssets for downloading
Module holger_world_connector::Package
fn crates_io_url(&self) -> Option<String> · src/lib.rs:121
fn archive_path(&self) -> String · src/lib.rs:136
Module holger_world_connector::WorldConnector
fn new() -> Result<Self> · src/lib.rs:91
Module holger_world_connector::pip
PyPI/pip half of the
worldconnector: turn a Python dependency manifest intoRemoteAssets the agent can pull through into a znippy archive.Two front doors, both yielding
Vec<PipPackage>:parse_requirements_txt(pinnedname==version, files empty — extras and env-markers stripped) andparse_uv_lockfile(registry packages only, sdist+wheel URLs already inline). Requirements packages are then filled in byresolve_with_indexagainst aPypiIndex(the/pypi/{name}/{ver}/jsonAPI viaPypiJsonClient), which only hits the network for packages whosefilesare still empty — so lockfile entries skip the API entirely.pip_packages_to_assetsis the final hop intotransfer_to_znippy, laying each file atpackages/{normalized_name}/{filename}.Gotcha:
normalize_name(PEP 503: lowercase,_/.→-) governs only the archive directory; the filename is preserved verbatim, soPillowlands underpackages/pillow/Pillow-10.0.0.tar.gz. The PyPI JSON body is capped atMAX_PYPI_JSON_BYTES(M6) so a hostile index can't OOM the agent.
enum PipError { Io, Parse, PypiApi } · src/pip.rs:25
struct PipPackage { name: String, version: String, files: Vec<PipFile> } · src/pip.rs:66
struct PipFile { filename: String, url: String, hash: Option<String> } · src/pip.rs:73
trait PypiIndex · src/pip.rs:96
struct PypiJsonClient · src/pip.rs:131
fn parse_requirements_txt(path: impl AsRef<std::path::Path>,) -> Result<Vec<PipPackage>, PipError> · src/pip.rs:267
Parses a requirements.txt with pinned versions (name==version). Returns packages with empty files — call resolve_with_index to populate.
fn parse_uv_lockfile(path: impl AsRef<std::path::Path>,) -> Result<Vec<PipPackage>, PipError> · src/pip.rs:332
Parses a uv.lock file and returns packages with resolved download URLs.
async fn resolve_with_index(packages: Vec<PipPackage>, index: &dyn PypiIndex,) -> Result<Vec<PipPackage>, PipError> · src/pip.rs:395
Populates files for packages using the given PyPI index.
fn pip_packages_to_assets(packages: &[PipPackage]) -> Vec<RemoteAsset> · src/pip.rs:418
Converts resolved pip packages to generic assets for transfer_to_znippy.
Module holger_world_connector::pip::PipFile
fn archive_path(&self, package_name: &str) -> String · src/pip.rs:81
Archive path within a znippy archive: packages/{normalized_name}/{filename}.
Module holger_world_connector::pip::PypiIndex
async fn fetch_files(&self, name: &str, version: &str,) -> Result<Vec<PipFile>, PipError> · src/pip.rs:98
Fetches list of files for a package+version from the PyPI JSON API.
Module holger_world_connector::pip::PypiJsonClient
fn new() -> Self · src/pip.rs:137
fn with_base_url(base_url: impl Into<String>) -> Self · src/pip.rs:141
Crate holger-znippy-build-thing-repository
The znippy-build-thing repository backend for holger — serves a forge's BUILD HISTORY out of a .znippy archive: one build-provenance attestation per built artifact, addressed by the artifact's own sha256, plus the derived recipe→artifacts lookup a CVE sweep reads. Read-only, pure path mapping, no shared types with the writer.
The build-history repository backend — holger serving a forge's znippy-build-thing package.
A build thing is a published recipe crate. A built artifact carries a receipt naming that recipe, its version and its content hash, in its own .nordisk.buildthing linker section. After the build and before the release, the forge writes the other direction into a .znippy: one attestation per artifact, saying recipe R with parameters P produced artifact hash H.
That package is what this backend serves. It is the sixteenth-and-something znippy-backed repository here, and deliberately the least clever one: every serving route is a path mapping onto a file already in the archive.
GET /{repo}/ index.json the manifest
GET /{repo}/artifact/{sha256} one attestation, by the artifact's hash
GET /{repo}/recipe/{krate} what this recipe built, any version
GET /{repo}/recipe/{krate}/{version} what this exact recipe version built
GET /{repo}/atom/{atom} what was built for this atom
GET /{repo}/{path} passthrough of any stored file
The writer is nornir_build_thing::history in edda. This crate does not depend on it and does not model an attestation at all — the archive already holds the derived by-recipe/… and by-atom/… lookups, so answering "what did this recipe build" is a file read, not a query. Nothing to keep in sync, nothing to drift, and a schema change in the attestation body cannot break serving.
The one exception is [RepositoryBackendTrait::list], which reads index.json for human-facing coordinates. It falls back to enumerating attestations/ when the manifest is missing or is a shape this holger does not understand — a repo that cannot pretty-print its listing still serves.
The package is derived: lose it and the forge regenerates it from the artifacts …
Module holger_znippy_build_thing_repository
const INDEX_PATH: & str · src/lib.rs:54
The manifest's path inside the archive.
const ATTESTATION_DIR: & str · src/lib.rs:57
The directory holding one attestation per artifact hash.
const BY_RECIPE_DIR: & str · src/lib.rs:60
The directory holding the derived recipe → artifacts lookup.
const BY_ATOM_DIR: & str · src/lib.rs:63
The directory holding the derived atom → artifacts lookup.
const PACKAGE_KIND: & str · src/lib.rs:66
The kind the manifest must name for this backend to trust its rows.
struct BuildThingRepoZnippy { name: String } · src/lib.rs:94
A forge's build history, served read-only out of a .znippy archive.
Module holger_znippy_build_thing_repository::BuildThingRepoZnippy
fn new(name: String) -> Self · src/lib.rs:105
An unconfigured repo: no archive, so every route 404s and every listing is empty. Serving nothing is the honest state for a history that has not been written yet.
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:115
Open the backing archive.
When the archive cannot be opened — corrupt, truncated, or written by an incompatible znippy version.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:125
Serve from an already-open reader.
fn list_files(&self) -> Vec<String> · src/lib.rs:134
Every path in the backing archive, or empty when there is no archive.
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:146
Extract one stored file.
When no archive is configured, or the path is not in it.
fn attestation(&self, artifact_sha256: &str) -> Result<Vec<u8>> · src/lib.rs:159
The attestation for one artifact hash.
When the history does not cover that artifact.
fn built_by_recipe(&self, krate: &str, version: Option<& str>) -> Result<Vec<u8>> · src/lib.rs:171
What did this recipe build? — the artifact hashes, one per line.
version of None spans every version of the recipe crate, which is the shape a CVE advisory has: it names a crate and a range, not one version.
When the history holds nothing for that recipe.
Crate holger-znippy-composer-repository
Composer / PHP (Packagist) repository backend for holger — passthrough of a Composer metadata repository held in a read-only znippy archive; unlike the deb/rpm siblings it synthesizes no index, so the archive must already carry one.
Composer (PHP / Packagist) repository backend served from a read-only znippy archive — the Composer sibling to znippy-{npm,maven,rpm,...}, one crate per ecosystem, all implementing RepositoryBackendTrait.
Unlike the synthesizing backends (rpm/deb build their native index from the packages present), this one is pure passthrough: the archive must already carry Composer v2 metadata verbatim at p2/{vendor}/{package}.json and dists at dist/{vendor}/{package}/{ref}.zip. handle_http2_request strips the repo name, splits the remainder, and routes three shapes — packages.json (root, emitted inline pointing at the p2 template), p2/… metadata, and dist/… zips — extracting the latter two straight from the archive. fetch() is the in-process dist path (namespace = vendor); put() always errors, read-only.
Gotcha: the emitted packages.json metadata-url is /{name}/p2/%package%.json, and %package% is Composer's own literal placeholder the client substitutes — not a format arg, so it must survive untouched through the format!.
Module holger_znippy_composer_repository
struct ComposerRepoZnippy { name: String } · src/lib.rs:35
znippy-backed Composer (PHP / Packagist) repository implementing the Composer v2 metadata protocol.
GET /{repo}/packages.json→ repository root (points at p2)GET /{repo}/p2/{vendor}/{package}.json→ per-package version metadataGET /{repo}/dist/{vendor}/{package}/{ref}.zip→ distributable zip
Metadata is stored in the archive as p2/{vendor}/{package}.json, dists as dist/{vendor}/{package}/{ref}.zip.
Module holger_znippy_composer_repository::ComposerRepoZnippy
fn new(name: String) -> Self · src/lib.rs:41
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:45
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:50
fn list_files(&self) -> Vec<String> · src/lib.rs:54
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:58
Crate holger-znippy-conda-repository
Conda / Anaconda repository backend for holger — serves a real per-subdir repodata.json plus the packages, with name/version/build/subdir read from each package's parsed info/index.json rather than guessed from the filename.
Conda repository backend: serves a real per-subdir repodata.json index plus the packages, synthesized from a read-only znippy archive (sibling to the deb/rpm/npm/gem backends). The coord-authoritative path resolves through the archive's typed conda view ([ZnippyArchive::as_conda]) — name/version/ build/subdir come from each package's parsed info/index.json, not a filename heuristic — so repodata and package_via_view serve classic {name}-{version}-{build}.tar.bz2 coords exactly. A with_reader fallback has no view and serves by exact path only (empty packages map). Read-only: put/is_writable refuse writes.
Two invariants that bite: repodata.json is cached per-subdir behind a Mutex because building it decompresses and re-hashes every package — without the cache each request is a DoS-amplification vector (mamba hits repodata first). And the sha256 key MUST carry a bare 64-hex SHA-256 (conda/mamba abort on a checksum mismatch); the znippy blake3 rides in a separate non-standard blake3 key, never in sha256.
Module holger_znippy_conda_repository
struct CondaRepoZnippy { name: String } · src/lib.rs:45
znippy-backed Conda channel.
conda/mamba fetch the per-subdir index then the packages:
GET /{repo}/{subdir}/repodata.json→ package indexGET /{repo}/{subdir}/{pkg}.tar.bz2→ package (classic)GET /{repo}/{subdir}/{pkg}.conda→ package (new format)
When backed by a concrete [ZnippyArchive] the repo resolves through the typed conda view ([ZnippyArchive::as_conda]): the package name, version, build, and subdir are read from the index columns the plugin parsed out of each package's info/index.json. The view is built once (cached behind the archive's OnceLock) and reused.
R4 fix: repodata.json is now a REAL index built from the view's authoritative coords — a packages map keyed by filename with {name, version, build, subdir, size, sha256} per the conda repodata schema — not the old dummy {"packages":{},"packages.conda":{}} stub. (See [CondaRepoZnippy::repodata].)
Module holger_znippy_conda_repository::CondaRepoZnippy
fn new(name: String) -> Self · src/lib.rs:59
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:65
Backed by a specific znippy archive file. Holds the open archive so the typed conda view is built once and reused.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:76
Backed by any ZnippyReader. No typed conda view; serves by exact path only.
fn list_files(&self) -> Vec<String> · src/lib.rs:93
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:100
Crate holger-znippy-deb-repository
Debian / APT repository backend for holger — synthesizes the native dists/{dist}/Release and Packages index (real SHA-256, control fields parsed out of the .deb via ar + control.tar) from packages inside a read-only znippy archive.
Debian (APT) repository backend over a read-only znippy archive — the deb sibling of znippy-rpm/conda/go, implementing RepositoryBackendTrait.
APT fetches the index (dists/{dist}/Release + per-component/arch Packages) then the pool/*.deb bodies. Two tiers answer a request, in order: authoritative path-passthrough (get_file) serves package bytes and any mirrored upstream metadata packed into the archive verbatim; when that misses, synthesize_index builds the native Packages/Release from the pool .debs — each stanza's Filename/Size/SHA256 computed over the real served bytes so an [trusted=yes] apt client can verify. Control fields come from the typed deb view (as_deb) when present; otherwise holger parses them NATIVELY out of the served .deb bytes — an ar + control.tar.* read of the real Debian control stanza (control module) — and only when THAT misses falls back to the {name}_{ver}_{arch}.deb filename (a 3-way _ split, exact since name and version never contain _).
Gotcha: synthesized indexes are cached (index_cache, keyed by request remainder) because the archive is immutable — without it every Release request re-hashes the whole pool, a DoS-amplification vector (same guard the conda plugin documents for repodata.json). An empty Packages synthesizes to None → 404, never an empty index.
Module holger_znippy_deb_repository
struct DebRepoZnippy { name: String } · src/lib.rs:55
znippy-backed Debian (APT) repository.
APT first fetches the index (Release + per-component/arch Packages), then the packages out of pool/:
GET /{repo}/dists/{dist}/ReleaseGET /{repo}/dists/{dist}/{component}/binary-{arch}/PackagesGET /{repo}/pool/{component}/…/{pkg}_{ver}_{arch}.deb
Native protocol (mirrors the conda/go plugins): the package files are served straight from the archive, and the index is synthesized from the .deb files present when the archive doesn't already carry it. Each Packages stanza is built from the pool filename ({name}_{ver}_{arch}.deb) plus the archive byte length and a real SHA256 of the served bytes, and the Release references each synthesized Packages by SHA256 + size — so an apt client ([trusted=yes]) can resolve and verify downloads. If the archive already contains authoritative upstream metadata (a mirrored repo), that is served verbatim (path-passthrough wins); synthesis is the fallback.
Synthesized indexes are cached (the archive is immutable) so each request doesn't re-hash the whole pool — the same DoS-amplification guard the conda plugin documents for repodata.json.
Module holger_znippy_deb_repository::DebRepoZnippy
fn new(name: String) -> Self · src/lib.rs:69
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:75
Backed by a specific znippy archive file. Holds the open archive so the typed deb view is built once and reused.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:85
fn list_files(&self) -> Vec<String> · src/lib.rs:97
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:104
Module holger_znippy_deb_repository::control
Native
.debcontrol-archive parse — thear+control.tar.*reader that extracts the authoritative Debiancontrolstanza (Depends / Maintainer / Description) straight from the served package bytes.A
.debis anar(5)archive with exactly three members, in order:debian-binary,control.tar.{,gz,xz,zst},data.tar.{,gz,xz,zst}. The control tarball holds./control(the single-stanza control file). We only need the control member, so we hand-parse thearcontainer (trivial 60-byte headers — no external crate), pick the codec from the member's extension, decompress it, and untar justcontrol.This is the fallback holger uses when the archive carries no typed deb sub-index (a mirrored repo of bare
.debs, or one built without the znippydeb_nativeplugin): the fields come from the real control file rather than being derived from the pool filename.
fn control_from_deb(deb: &[u8]) -> Option<String> · src/control.rs:32
Extract the raw control stanza from .deb bytes, or None if the bytes aren't a well-formed .deb, carry no control.tar* member, or that member has no control entry / an unsupported codec. Never panics on malformed input — a hostile archive just yields None (the caller then falls back to filename-derived fields).
Module holger_znippy_deb_repository::control::tests_support
Test-only fixtures shared with the crate's
lib.rsintegration tests (which need a real.debto prove the native parse enriches the synthesized index).
fn control_tar(body: &str) -> Vec<u8> · src/control.rs:120
A one-file control.tar carrying ./control with body.
fn make_deb(control_ext: &str, control_bytes: &[u8]) -> Vec<u8> · src/control.rs:133
Assemble a minimal well-formed .deb: ar magic + debian-binary + control.tar<ext> (control_bytes) + data.tar.
fn sample_deb() -> Vec<u8> · src/control.rs:157
A realistic gzip-compressed .deb for hello 2.10-2 amd64 carrying a full control stanza (Depends / Maintainer / Description) — the fixture the lib integration test serves to prove the native parse enriches Packages.
Crate holger-znippy-docker-repository
Docker / OCI registry backend for holger, READ-ONLY — serves the Registry HTTP API V2 straight out of a znippy archive whose entries already sit in on-wire layout. Pure passthrough; see holger-file-oci-repository for the writable push target.
Read-only Docker/OCI repository backend: serves the Registry HTTP API V2 straight out of a znippy archive whose entries already sit in on-wire layout (v2/{name}/manifests/{ref}, v2/{name}/blobs/{digest}). No index is synthesized here — unlike the deb/rpm siblings that build a native index from bare packages, this backend is pure passthrough of pre-baked registry files.
DockerRepoZnippy implements RepositoryBackendTrait; handle_http2_request dispatches the four V2 verbs (version probe, tags/list, manifests, blobs), and fetch maps an ArtifactId (namespace/name/version) onto the manifest key. Writes are rejected — it's an immutable airgap artifact source.
Gotcha: Docker image names are namespaced and span a variable number of path segments (library/nginx), so routing anchors on the trailing verb token rather than a fixed position — everything between v2 and the verb is the slash-joined name. Manifests are served with their own declared mediaType (not a fixed OCI type) plus a Docker-Content-Digest computed over the exact bytes served, so pull-by-digest verifies; tags/list is built with serde_json because both the name and tags are attacker-controlled.
Module holger_znippy_docker_repository
struct DockerRepoZnippy { name: String } · src/lib.rs:37
znippy-backed OCI / Docker registry implementing the Registry HTTP API V2.
GET /v2/→ 200 (version probe)GET /v2/{name}/tags/list→ tag list JSONGET /v2/{name}/manifests/{reference}→ image manifestGET /v2/{name}/blobs/{digest}→ layer / config blob
Content is stored in the archive under v2/{name}/manifests/{ref} and v2/{name}/blobs/{digest} (the on-wire layout).
Module holger_znippy_docker_repository::DockerRepoZnippy
fn new(name: String) -> Self · src/lib.rs:43
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:47
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:52
fn list_files(&self) -> Vec<String> · src/lib.rs:56
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:60
Crate holger-znippy-gem-repository
RubyGems repository backend for holger — serves .gem bytes plus a REAL compact-index (versions, info/{name}) synthesized from a read-only znippy archive, which is what bundler resolves against.
RubyGems repository backend over a read-only znippy archive — the gem sibling of znippy-{maven,python,rust,npm,...}. Serves the subset bundler/gem needs: gems/{file}.gem bytes plus a REAL compact-index (info/{name}, versions) synthesized here, not upstream passthrough.
The authoritative path resolves through the typed gem view ([ZnippyArchive::as_gem], built once behind the archive's OnceLock): the (name, version, platform) coords come from the columns the plugin parsed out of each gem's metadata.gz, so a platform-suffixed native gem (foo-1.2.3-java.gem) resolves by its true coords instead of a filename guess that folds the platform into the version. A bare ZnippyReader ([GemRepoZnippy::with_reader]) has no view and serves by exact path only.
Gotcha: bundler parses the compact-index |checksum: field as a bare SHA-256 hex and aborts bundle install on a mismatch — so info hashes the served gem bytes with SHA-256, NOT the archive's blake3 digest. Index tokens are stripped of \n \r | space so an attacker-controlled metadata.gz field can't inject or split compact-index lines.
Module holger_znippy_gem_repository
struct GemRepoZnippy { name: String } · src/lib.rs:46
znippy-backed RubyGems repository.
Implements the subset of the RubyGems API needed for bundler/gem install:
GET /{repo}/gems/{name}-{ver}[-{platform}].gem→ the gem bytesGET /{repo}/info/{name}→ the RubyGems compact index info fileGET /{repo}/versions→ the compact-index versions file
When backed by a concrete [ZnippyArchive] the repo resolves through the typed gem view ([ZnippyArchive::as_gem]): the gem name, version, and platform are read from the index columns the plugin parsed out of the gem's metadata.gz. So a platform-suffixed native gem (foo-1.2.3-java.gem, authoritative version 1.2.3 + platform java) resolves by its real coords instead of a filename guess that folds the platform into the version. The view is built once (cached behind the archive's OnceLock) and reused.
R1 fix: the info/{name} and versions endpoints now serve a REAL compact-index built from the view's authoritative coords — not the old dummy ---\n YAML stub. (See [GemRepoZnippy::info].)
Module holger_znippy_gem_repository::GemRepoZnippy
fn new(name: String) -> Self · src/lib.rs:55
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:61
Backed by a specific znippy archive file. Holds the open archive so the typed gem view is built once and reused.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:67
Backed by any ZnippyReader. No typed gem view; serves by exact path only.
fn list_files(&self) -> Vec<String> · src/lib.rs:79
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:86
Crate holger-znippy-go-repository
Go module repository backend for holger — serves the GOPROXY protocol (@v/list, .info, .mod, .zip) out of a read-only znippy archive laid out like the on-disk module-proxy cache.
Go repository backend: serves the GOPROXY protocol out of a read-only znippy archive. Sibling to the other znippy-<eco> backends (maven, npm, rpm, …); like them it synthesizes the native index from the files the archive already holds rather than proxying upstream.
Archive layout mirrors the on-disk module-proxy cache: {module}/@v/{ver}.{info,mod,zip}. handle_http2_request strips the leading repo name, splits the module path from the op on /@v/, and either synthesizes @v/list (scan the archive, sorted + deduped) or serves the .info/.mod/.zip file straight through with a suffix-derived Content-Type. fetch rebuilds the module path from ArtifactId (namespace = module prefix, name = last segment) and only ever returns the .zip. Read-only: put errors and is_writable is false.
Gotcha: @v/list is built by scanning .zip entries alone, so a version carrying only .info/.mod (no zip) is invisible to list even though its metadata is still directly fetchable.
Module holger_znippy_go_repository
struct GoRepoZnippy { name: String } · src/lib.rs:35
znippy-backed Go module proxy implementing the GOPROXY protocol.
GET /{repo}/{module}/@v/list→ newline-separated versionsGET /{repo}/{module}/@v/{ver}.info→{ "Version": …, "Time": … }GET /{repo}/{module}/@v/{ver}.mod→ thego.modGET /{repo}/{module}/@v/{ver}.zip→ module zip
Files are stored in the archive as {module}/@v/{ver}.{info,mod,zip}, matching the on-disk module-proxy cache layout.
Module holger_znippy_go_repository::GoRepoZnippy
fn new(name: String) -> Self · src/lib.rs:41
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:45
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:50
fn list_files(&self) -> Vec<String> · src/lib.rs:54
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:58
Crate holger-znippy-helm-repository
Helm chart repository backend for holger — serves a Helm repo's native index.yaml and chart .tgz files from a read-only znippy archive, synthesizing index.yaml from the charts present when the archive ships none.
Helm chart repository backend over a read-only znippy archive.
Serves a Helm repo's native surface — GET /{repo}/index.yaml and GET /{repo}/{chart}-{ver}.tgz — from chart tarballs inside the archive. Passthrough wins: if the archive ships an index.yaml, it's served verbatim; otherwise index.yaml is synthesized from every *.tgz (decompress + blake3-less digest each chart) via [index::generate_index]. Because the archive is immutable that synthesis is done once and memoized in index_cache (a OnceLock) — it used to run per request, a DoS-amplifier.
Gotcha: the synthesized index lists charts by basename while the archive may store them under a subdir (charts/nginx-1.2.3.tgz), so serve and [RepositoryBackendTrait::fetch] both resolve through get_chart_by_basename (exact path, then a basename scan) — a plain root lookup would 404 subdir charts. Sibling to the other znippy-<eco> backends; read-only (put errors).
Module holger_znippy_helm_repository
struct HelmRepoZnippy { name: String } · src/lib.rs:37
znippy-backed Helm chart repository.
GET /{repo}/index.yaml→ chart index (served from archive)GET /{repo}/{chart}-{ver}.tgz→ chart archive
Charts are stored in the archive as {chart}-{ver}.tgz, plus an index.yaml at the archive root.
Module holger_znippy_helm_repository::HelmRepoZnippy
fn new(name: String) -> Self · src/lib.rs:48
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:52
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:61
fn list_files(&self) -> Vec<String> · src/lib.rs:69
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:76
fn generated_index(&self) -> Result<String> · src/lib.rs:89
Build a fresh index.yaml from every *.tgz chart in the archive.
This lets holger host a Helm repository from a bare set of chart tarballs: drop charts into the znippy archive and a correct, digest-bearing index is generated on demand — no pre-baked index.yaml required.
Module holger_znippy_helm_repository::index
Helm repository index generation.
A Helm chart repository is, at minimum, a set of
*.tgzchart archives plus anindex.yamlthat lists every chart/version with its SHA-256 digest and download URL. This module turns a set of chart tarballs into a validindex.yaml(HelmapiVersion: v1) and extracts the chart metadata from a packaged chart, so holger can host charts rather than only serve a pre-baked index.No YAML dependency is used:
index.yamlhas a fixed, simple shape and chartChart.yamlfiles only need a handful of top-level scalar fields, so both are handled with small, explicit parsers/emitters. This keeps the airgap dependency surface minimal.
struct ChartMeta { name: String, version: String, app_version: Option<String>, description: Option<String> } · src/index.rs:21
The subset of Chart.yaml fields holger needs to index a chart.
fn chart_digest(tgz: &[u8]) -> String · src/index.rs:29
SHA-256 hex digest of a chart tarball — the Helm digest field.
fn chart_meta_from_tgz(tgz: &[u8]) -> Result<ChartMeta> · src/index.rs:41
Extract the chart metadata from a packaged chart (.tgz).
Helm packages a chart as gzip(tar) with every entry under a top-level directory named after the chart, e.g. njord/Chart.yaml. We locate the first Chart.yaml and parse its top-level scalar fields.
fn parse_chart_yaml(text: &str) -> Result<ChartMeta> · src/index.rs:70
Parse the top-level scalar fields of a Chart.yaml.
Only unindented key: value lines are considered (nested maps/sequences are ignored), which is all that is needed for name, version, appVersion, and description.
struct ChartArtifact { filename: & 'a str, tgz: & 'a [u8] } · src/index.rs:123
One chart artifact to index: its file name and tarball bytes.
fn generate_index(charts: &[ChartArtifact<'_>], base_url: &str, created: &str,) -> Result<String> · src/index.rs:134
Build a Helm index.yaml (apiVersion v1) from a set of chart tarballs.
base_urlis prefixed to each chart's file name to form its download URL (use""for relative URLs, which Helm resolves against the repo URL).createdis an RFC 3339 timestamp string stamped on each entry and on the index itself; passing it in keeps generation deterministic and testable.
Crate holger-znippy-maven-repository
Maven / Java (maven3) repository backend for holger — serves JARs and POMs out of a read-only znippy archive and synthesizes the native maven-metadata.xml version listing, so a bare partial mirror still resolves.
Maven repository backend over a read-only znippy archive: serves JARs/POMs and synthesizes the native maven-metadata.xml GA version listing from the packages inside, so a partial mirror (bare .jars, no upstream metadata) is still resolvable by a Maven client. Sibling of the other znippy-{rpm,deb,npm,...} backends behind [RepositoryBackendTrait].
Resolution order per request: exact archive-path passthrough (a mirrored repo shipping its own metadata wins) → typed maven view lookup by GAV[+classifier] → maven-metadata.xml synthesis (GA version listing AND version-level -SNAPSHOT metadata) → .sha1/.md5 checksum synthesis over the served bytes. The typed view ([ZnippyArchive::as_maven], built once behind the archive's OnceLock) reads coords from index columns the packer extracted, so -SNAPSHOT versions and -sources/-javadoc classifiers resolve by authoritative coords instead of a fragile filename split.
Gotcha: the typed view is only populated when the archive was packed WITH the maven plugin. An archive tagged maven but packed without GAV columns (e.g. the agent's directory→znippy packer) yields an EMPTY view — [MavenRepoZnippy::list] must therefore fall through to the legacy filename parse rather than trusting an empty view, and the with_reader (non-archive) path has no view at all, serving by exact path only.
Module holger_znippy_maven_repository
struct MavenRepoZnippy { name: String } · src/lib.rs:41
Znippy-backed Maven repository.
Serves Maven artifacts (JARs, POMs, etc.) from a znippy archive. When backed by a concrete [ZnippyArchive] the repo resolves requests through the typed maven view ([ZnippyArchive::as_maven]) — GAV[+classifier] coords are read from the index columns the plugin extracted, so -SNAPSHOT and classifiers (-sources/-javadoc) resolve correctly instead of being mis-split from the filename. The view is built ONCE (cached behind the archive's OnceLock) and reused per request. A with_reader fallback keeps the generic path working for non-archive readers (e.g. in-memory test readers) that have no typed view.
Module holger_znippy_maven_repository::MavenRepoZnippy
fn new(name: String) -> Self · src/lib.rs:50
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:56
Create a repo backed by a specific znippy archive file. Holds the open archive so the typed maven view is built once and reused.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:67
Create a repo backed by any ZnippyReader implementation. This path has no typed maven view; it serves by exact archive path only.
fn list_files(&self) -> Vec<String> · src/lib.rs:85
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:92
Crate holger-znippy-npm-repository
[storage-backends] packument is view-only — generic-reader ingestion 404s the packument (breaks npm install).
npm repository backend: serves the npm HTTP registry protocol (packument JSON + /-/-tarballs) synthesized from packages inside a read-only znippy archive — the npm sibling of znippy-{maven,python,rust,nuget,...}, all implementing RepositoryBackendTrait.
The authoritative path resolves through the archive's typed npm view ([ZnippyArchive::as_npm]): (name, version) come from index columns the plugin parsed out of each package.json, so a scoped package (@scope/pkg) resolves by its real name rather than a filename guess — the @scope/ prefix that the tarball filename drops. with_reader keeps a generic exact-path fallback for non-archive readers (no typed view).
Two invariants worth knowing. Packument dist.integrity is the SSRI SHA-512 of the served bytes, recomputed here because the archive stores blake3, not sha512 — npm aborts install on a mismatch. And tarball URLs are built from a configurable base_url (ron_base_url, empty ⇒ host-relative) so pulls work behind any reverse proxy — the R2 fix that replaced a hardcoded 127.0.0.1.
Module holger_znippy_npm_repository
struct NpmRepoZnippy { name: String } · src/lib.rs:44
znippy-backed npm registry.
Implements the subset of the npm HTTP registry API needed to install:
GET /{repo}/{pkg}→ packument JSON (versions + tarball URLs)GET /{repo}/{pkg}/-/{file}.tgz→ the package tarball
When backed by a concrete [ZnippyArchive] the repo resolves through the typed npm view ([ZnippyArchive::as_npm]): the package name (including the @scope/ prefix that the tarball filename drops) and version are read from the index columns the plugin parsed out of package.json. So a scoped package resolves by its authoritative name instead of a filename guess. The view is built once (cached behind the archive's OnceLock) and reused. A with_reader fallback keeps a generic path-only mode for non-archive readers.
Tarball URLs in the packument are built from a configurable base URL (ron_base_url) instead of a hardcoded host, so npm pulls work behind any public host / reverse proxy (an empty base yields host-relative URLs).
Module holger_znippy_npm_repository::NpmRepoZnippy
fn new(name: String) -> Self · src/lib.rs:55
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:61
Backed by a specific znippy archive file. Holds the open archive so the typed npm view is built once and reused. Host-relative tarball URLs.
fn with_archive_and_url(name: String, archive_path: PathBuf, base_url: String) -> Result<Self> · src/lib.rs:68
Backed by an archive, with a configurable base URL for packument tarball links (e.g. https://npm.example.com). This is the R2 fix: the base is configuration (ron_base_url), not a hardcoded 127.0.0.1.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:79
Backed by any ZnippyReader. No typed npm view; serves by exact path only.
fn with_base_url(mut self, base_url: String) -> Self · src/lib.rs:84
Override the packument base URL.
fn list_files(&self) -> Vec<String> · src/lib.rs:97
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:104
Crate holger-znippy-nuget-repository
NuGet / .NET repository backend for holger — serves the NuGet V3 flat-container protocol (service index, versions, .nupkg) synthesized from packages inside a read-only znippy archive.
NuGet (.NET) repository backend: serves the NuGet V3 flat-container protocol synthesized from .nupkg files inside a read-only znippy archive. Sibling to the other znippy-<eco> backends (npm packument, maven metadata, rpm repomd), implementing RepositoryBackendTrait over a ZnippyReader.
Flow: handle_http2_request routes the three V3 endpoints — service index, v3-flatcontainer/{id}/index.json version list, and the .nupkg fetch — off the archive layout {id-lowercased}/{ver}/{id}.{ver}.nupkg. Ids are lowercased per the flat-container convention; versions_json scans the archive and sorts+dedups. base_url (from ron_base_url) MUST be the externally-reachable holger base, since NuGet follows the absolute PackageBaseAddress @id for every subsequent fetch — leak the loopback default and remote clients break.
Gotcha: the service index advertises ONLY PackageBaseAddress; there is no RegistrationsBaseUrl/SearchQueryService, so dotnet restore of dependency graphs or version ranges won't resolve — only EXACT-version pulls work. Pin exact versions in consumers.
Module holger_znippy_nuget_repository
struct NugetRepoZnippy { name: String } · src/lib.rs:35
znippy-backed NuGet (.NET) repository implementing the NuGet V3 protocol.
GET /{repo}/v3/index.json→ service indexGET /{repo}/v3-flatcontainer/{id}/index.json→ version listGET /{repo}/v3-flatcontainer/{id}/{ver}/{id}.{ver}.nupkg→ package
Packages are stored in the archive as {id}/{ver}/{id}.{ver}.nupkg (ids lowercased, per the flat-container convention).
Module holger_znippy_nuget_repository::NugetRepoZnippy
fn new(name: String) -> Self · src/lib.rs:50
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:54
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:59
fn with_base_url(mut self, url: String) -> Self · src/lib.rs:65
Override the public base URL advertised in the V3 service index. An empty string resets to the default loopback. A trailing slash is normalized.
fn list_files(&self) -> Vec<String> · src/lib.rs:75
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:79
Crate holger-znippy-package-repository
[storage-backends] Holger-native znippy surface; opt-in CMS provenance verify behind sign.
The one repository backend that is znippy all the way down: it serves znippy's OWN .znippy packages as the artifact, not a foreign ecosystem synthesized out of a backing archive (contrast the sibling znippy-{maven,npm,rpm,…} crates, which mimic an upstream registry protocol over the same store). With no registry to imitate, [ZnippyRepoZnippy] speaks a tiny Holger-native HTTP surface — / lists .znippy files, /{name}/{version} maps to {name}-{version}.znippy, anything else is raw path-passthrough — and the read-only [RepositoryBackendTrait] serve/list/archive-info methods parse coordinates from those {name}-{version}.znippy filenames.
Provenance is OPT-IN and feature-gated. with_archive (the default) opens without verifying and records no signer; with_archive_verified (behind sign) checks the detached-CMS signatures against trusted roots from the index digests alone and REJECTS the archive on any failure — including a perfectly good but unsigned one. The gotcha: the two constructors are byte-for-byte identical on the serve path; the only observable difference is verified_signer, so never assume served bytes were provenance-checked unless a signer is recorded.
Module holger_znippy_package_repository
struct ZnippyRepoZnippy { name: String } · src/lib.rs:38
znippy-backed repository that serves znippy packages themselves — the one format that is znippy all the way down.
Every other Holger repo uses a znippy archive as a backing store for some foreign ecosystem (npm tarballs, jars, …). This one serves znippy's own native .znippy packages (merged / signed packages produced by the znippy CLI) as the artifact. There is no third-party registry protocol to mimic, so it speaks a small Holger-native HTTP surface:
GET /{repo}/→ newline list of.znippypackagesGET /{repo}/{name}/{version}→{name}-{version}.znippyGET /{repo}/{path}→ path-passthrough of any stored file
Module holger_znippy_package_repository::ZnippyRepoZnippy
fn new(name: String) -> Self · src/lib.rs:49
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:53
fn with_archive_verified(name: String, archive_path: PathBuf, roots: &znippy_common::sign::CertStore,) -> Result<Self> · src/lib.rs:74
Like with_archive, but FIRST verifies the archive's provenance: every per-artifact detached CMS signature plus the per-archive signature are checked, with the signer cert chained to roots. Digests are recomputed from the index (the sunk-cost chunk BLAKE3 hashes) — no file bytes are read. On success the verified signer's common name is recorded (see verified_signer); on ANY failure — including an unsigned archive or a signer that does not chain to a trusted root — the error is propagated and the archive is rejected (no reader is constructed).
This is the OPT-IN path: callers only reach it when trusted roots are configured. With no roots configured, callers keep using with_archive and behaviour is byte-for-byte unchanged.
Gated behind the sign feature (off by default) so the default dep graph does not pull znippy-common/sign — see the Cargo.toml note.
fn with_archive_verified_der_roots(name: String, archive_path: PathBuf, root_der_paths: &[PathBuf],) -> Result<Self> · src/lib.rs:102
Config-facing form of with_archive_verified: loads one or more DER-encoded trusted root certificates from root_der_paths, builds the CertStore, and verifies the archive's provenance against it. Keeps the PKI/filesystem loading inside this crate so the wiring layer (server/lib) does not need a direct znippy-common dependency. Fails closed on any read/parse/verify error.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:118
fn verified_signer(&self) -> Option<& str> · src/lib.rs:125
The common name of the provenance signer, if this repo was opened via with_archive_verified and verification passed. None for unverified (default) repos.
fn list_files(&self) -> Vec<String> · src/lib.rs:129
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:133
Module holger_znippy_package_repository::sign_tests
Provenance-verification tests. Gated behind the
signfeature because they seal a real SIGNED.znippyarchive (viaznippy-compress+ theznippy_common::signdev PKI helpers) and assert thatwith_archive_verifiedaccepts a trusted root, rejects an untrusted/empty root, and that the unverifiedwith_archivepath keeps working unchanged.
Crate holger-znippy-python-repository
PyPI / pip repository backend for holger — serves a real PEP 503 simple index (plus PEP 691 JSON and #sha256= digests pip/uv verify) synthesized from wheels and sdists inside a read-only znippy archive. This is holger's PyPI crate: config calls it pip, code calls it ArtifactFormat::Pip, the directory is znippy-python.
Python (pip) repository backend: serves a PEP 503 "simple" index synthesized from wheels/sdists inside a read-only znippy archive. Sibling to the other znippy-<eco> backends; implements [RepositoryBackendTrait] and is wired in read-only (is_writable false, put errors).
Layout convention: files live at packages/{normalized-name}/{filename}, name normalized per PEP 503 (lowercase, _/. → -). handle_http2_request routes /simple/, /simple/{name}/, and /packages/{name}/{filename} after stripping the repo-name prefix.
Coord resolution has two tiers: prefer the concrete archive's typed python view ([ZnippyArchive::as_python]) whose (name, version) come from index columns the python plugin extracted, so wheels vs sdists resolve authoritatively; fall back to parsing the dist filename when that view is empty (e.g. an archive built by the agent's directory→znippy path, which tags pkg_type but writes no columns) or when only a generic ZnippyReader is configured.
Gotcha: the simple index reflects attacker-influenced package/file names into HTML, so every interpolated value goes through html_escape (reflected-injection, M10).
Module holger_znippy_python_repository
struct PipRepoZnippy { name: String } · src/lib.rs:89
znippy-backed Python package index implementing PEP 503.
Serves packages from any ZnippyReader backend. Files stored as: packages/{normalized-name}/{filename}.
Module holger_znippy_python_repository::PipRepoZnippy
fn new(name: String) -> Self · src/lib.rs:151
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:156
Create a repo backed by a specific znippy archive file
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:167
Create a repo backed by any ZnippyReader implementation
fn list_files(&self) -> Vec<String> · src/lib.rs:194
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:201
Crate holger-znippy-rpm-repository
RPM / yum / dnf repository backend for holger — synthesizes the native rpm-md index (repomd.xml + primary.xml, epoch and read from the RPM binary header) from .rpm files inside a read-only znippy archive.
znippy-backed RPM (yum/dnf) repository backend — serves an rpm-md index synthesized from the .rpm files inside a read-only znippy archive, the rpm sibling of the deb/npm/maven znippy-* backends behind RepositoryBackendTrait.
Two-stage handle_http2_request: authoritative path-passthrough first (packages, and any real createrepo repodata/ the archive already carries wins), then native synthesis of repodata/repomd.xml + repodata/primary.xml from the packages present. primary.xml lists one <package> per rpm with a real SHA256 + size over the served bytes; repomd.xml references it by hash so dnf can resolve and verify. NEVRA is parsed from the filename ({name}-{version}-{release}.{arch}.rpm), enriched with epoch + <format> from the znippy rpm_native header view (as_rpm) when the archive carries the sub-index; otherwise holger parses the RPM header NATIVELY out of the served bytes (header module — real epoch/license/vendor/url/summary/sourcerpm/ provides/requires), and only when THAT misses falls back to filename-only epoch=0.
Gotcha: synthesized indexes are cached because hashing the whole Packages/ set per request is a DoS-amplification vector — the cache is sound only because the archive is immutable. And fetch matches on the parsed NEVRA (exact name + version), never a string prefix, so zlib 1.3.1 can't collide with zlib-1.3.10.
Module holger_znippy_rpm_repository
struct RpmRepoZnippy { name: String } · src/lib.rs:54
znippy-backed RPM (yum/dnf) repository.
dnf fetches the repodata index then the packages:
GET /{repo}/repodata/repomd.xml→ index of metadata filesGET /{repo}/repodata/primary.xml→ per-package metadataGET /{repo}/Packages/{pkg}-{ver}-{rel}.{arch}.rpm
Native protocol (mirrors the conda/go plugins): the package files are served straight from the archive, and the rpm-md index is synthesized from the .rpm files present when the archive doesn't already carry it. primary.xml lists each package — name/version/release/arch parsed from the filename, plus the archive byte length and a real SHA256 of the served bytes — and repomd.xml references primary.xml by SHA256 + size, so a dnf client can resolve and verify downloads. The index is served uncompressed (repomd.xml declares the .xml href; dnf picks the codec from the extension), so no gzip dependency is pulled. If the archive already contains authoritative upstream metadata (a mirrored repo) that is served verbatim (path-passthrough wins); synthesis is the fallback.
Synthesized indexes are cached (the archive is immutable) so each request doesn't re-hash the whole Packages/ set — the same DoS-amplification guard the conda plugin documents for repodata.json.
Module holger_znippy_rpm_repository::RpmRepoZnippy
fn new(name: String) -> Self · src/lib.rs:67
fn with_archive(name: String, archive_path: PathBuf) -> Result<Self> · src/lib.rs:73
Backed by a specific znippy archive file. Holds the open archive so the typed rpm view is built once and reused.
fn with_reader(name: String, reader: Arc<dyn ZnippyReader>) -> Self · src/lib.rs:83
fn list_files(&self) -> Vec<String> · src/lib.rs:95
fn get_file(&self, relative_path: &str) -> Result<Vec<u8>> · src/lib.rs:102
Module holger_znippy_rpm_repository::header
Native RPM header parse — reads the authoritative NEVRA epoch and the rpm-md
<format>fields (license / vendor / URL / summary / sourcerpm / provides / requires) straight out of the.rpmheader tag table, so holger enrichesprimary.xmleven for archives that carry no typed rpm sub-index (a mirrored repo of bare.rpms, or one built without the znippyrpm_nativeplugin).An
.rpmis: a 96-byte lead, a signature header, then the main header — both headers share the RPMv3 "header" structure:[8e ad e8 01] [4 reserved] [nindex:u32be] [hsize:u32be]followed bynindex16-byte index entries(tag, type, offset, count)and anhsize-byte data store the offsets point into. The signature header's store is padded to an 8-byte boundary before the main header. We parse the main header's tag table and pull the fields the rpm-md<package>needs.Every read is bounds-checked; malformed or hostile bytes yield
None(the caller then keeps its filename-derived NEVRA withepoch=0). Nounsafe, no panics, no allocation beyond the extracted strings.
fn parse_rpm_header(rpm: &[u8], name: &str, version: &str, release: &str, arch: &str,) -> Option<RpmMetaRow> · src/header.rs:174
Parse the header fields of a .rpm into an [RpmMetaRow], keyed by the coords the caller already derived from the filename. Returns None when the bytes aren't a decodable RPM (bad lead, truncated headers, …).
Module holger_znippy_rpm_repository::header::tests_support
Test-only RPM synthesis — a byte-exact minimal
.rpm(lead + empty signature header + a main header carrying the chosen tags) so the crate'slib.rsintegration tests can drive the native parse without a binary fixture.
enum Tag { Str, Int32, StrArray } · src/header.rs:214
A tag to encode into the synthetic main header.
fn make_rpm(tags: &[Tag]) -> Vec<u8> · src/header.rs:273
Assemble a full minimal .rpm: lead + empty signature header + a main header carrying tags, with the 8-byte signature/main alignment padding.
Crate xtask
[dev-tooling] gen-proto (protox+tonic-build, committed output), demo-wasm/deploy-pages.
holger xtask — runs the local test/bench suites for solo dev work.
No regression gate here anymore. The bench results that used to be appended to bench_history.json now flow into nornir's Urðr warehouse during nornir release, where the no-regression gate runs against the persistent history.
Run a release-gate-equivalent bench locally with: cargo run -p xtask --release --example nornir-bench
Module xtask::appliance_pack
appliance-pack— pack a built holger server binary into the app-.znippythe appliance boot chain execs, plus its blake3 pin file.This is the packer the installer-cell wall report named as holger's missing input #2 — and it is deliberately THIN: the archive writer is the shared znippy codec (
znippy_common::create_archive_to_vec, the exact functionkorp_installer::payload::pack_znippywraps), and the entry name is thekorp.entry=selector the boot chain resolves (for_product("holger")bakeskorp.entry=holger;korp-startdoesget_file(archive, entry)andmemfd+fexecves the result). One codec, one entry contract, no dialect.Outputs, named for the installer medium's vocabulary:
<out>/holger-app.znippy the app payload (streamed raw onto P2 at install) <out>/holger-app.blake3 blake3 hex of the ARCHIVE bytes — the pin the ESP carries and every boot re-checksThe verdict is APPLIED OUTPUT: after writing, the archive is read back through
get_fileand the extracted entry must be byte-identical to the input binary — the packer proves its own product, never just its exit code.Honest wall, stated where the operator will meet it: the OS-less installed shape execs the entry with NO loader and NO /lib, so a production payload must be STATICALLY linked. holger's musl-static build is walled by openzl-sys-rs's cross-C++ need (measured, see .nornir/build-variants.toml), so a glibc-dynamic holger-mvp packs and verifies fine — and will refuse at exec on real metal until that wall falls. The packer warns (loudly, by PT_INTERP) rather than refusing: the container/lab shapes run it today.
fn run() -> Result<()> · src/appliance_pack.rs:65
Run the verb. Args: --bin <path> --entry <name> --out <dir> --level <zstd> — every default spelled above.
Module xtask::gen_proto
cargo xtask gen-proto— compile holger's.protofiles to Rust withoutprotocand without shelling out, then commit the generated code.Why this exists: the old
server/lib/build.rscalledtonic_build::compile_protos(...), which shells out to theprotocbinary at everycargo build. On checkouts that lackprotoc(e.g. thenornirserver user) that build-script FAILS and takes downholger-server-liband everything path-dep'd on it (viz test arms, holger/skade/nornir benches).The fix (Rickard's directive: "use an xtask, avoid shelling"): move codegen out of build-time entirely. This xtask uses protox — the pure-Rust protobuf compiler — to parse the
.protointo aFileDescriptorSet(noprotoc, no subprocess), then hands that to tonic-build'scompile_fds(withskip_protoc_run()) to emit Rust. The output is written into the source tree atserver/lib/src/generated/holger.v1.rsand committed.After this, a plain
cargo buildcompiles ONLY the checked-in generated file — noprotoc, no shell-out. Regenerate (idempotently) withcargo xtask gen-proto.
fn run() -> Result<PathBuf> · src/gen_proto.rs:53
Compile proto/holger.proto → committed Rust. Returns the path written.
Pure-Rust pipeline: protox::compile (parse, no protoc) → tonic-build compile_fds with skip_protoc_run() (codegen, no protoc).
Module xtask::installer_iso
installer-iso— compose and master holger's PER-SITE installer ISO from the declared[[build.installer]] holger-installercell.The whole chain, one command per site:
cargo xtask installer-iso \ --kernel /path/to/bzImage \ --param site=stockholm-7 \ --param hub_conf=@site7-hub.confEverything of substance lives elsewhere, on purpose — this verb is WIRING: the parameter grammar and refusals are
nornir_build_thing::params(the declared roster on the cell; secrets refused by name), the composition is edda'skorp_installer::isocompose::compose_and_master(the SAME code the KVM proofs drive — korp-boot, static-musl PID-1s, blake3-pinned kernel, payloads on the ISO-9660 volume), and the provenance is the attested descriptor the mastering seam already seals (applied params ride it: the build record answers which ISO holds which hub key).Inputs beyond the params:
--kernel <bzImage>(or$KORP_TEST_KERNEL) — a real EFI-stub kernel with virtio built in;--app <path>(default.znippy-out/holger-app.znippy— runcargo xtask appliance-packfirst, the STATIC payload from themvp-server-muslatom is the one that boots metal).
fn run() -> Result<()> · src/installer_iso.rs:33
Run the verb. See the module doc for the grammar.
Module xtask::wasm_plugins
cargo xtask build-wasm-plugins— the build door for holger's wasm package handlers, and the only place that decides where a built module lives.Zero
.wasmfiles existed anywhere in this constellation before this lane, so there was no precedent for where a module lands or how it ships. This file is the decision, written down:
question answer where does cargo put it? target/wasm32-unknown-unknown/release/<crate>.wasm, cargo's own path — nothing is copied, so there is no second location to go stalewhere does a deployment read it? a plugin directory the operator names, scanned by holger_plugin_host::registry::scan. Shipping is a file copy; the module is a build artifact, not a committed blob.is it committed? no. It is reproducible from source in under a second, and a committed binary would be the one copy nothing rebuilds. who builds it? this verb — in CI, never by hand into a gitignored path. The agreement guards fail with this exact command when the module is missing or stale.
--listprints the crates and the target path without building, so CI can assert the set without paying for a compile.
const WASM_PLUGIN_CRATES: & [& str] · src/wasm_plugins.rs:23
Every wasm package-handler crate. Adding a handler pair means adding one line here — which is also what makes the set assertable rather than discovered.
const TARGET: & str · src/wasm_plugins.rs:27
The wasm target every module is built for.
fn module_rel_path(crate_name: &str) -> PathBuf · src/wasm_plugins.rs:30
Cargo's output path for a crate's module, relative to the target dir.
fn list() · src/wasm_plugins.rs:52
Print the plugin set and where each module lands, without building.
fn run() -> anyhow::Result<()> · src/wasm_plugins.rs:68
Build every wasm package handler, then assert each module actually exists.
The existence check is not belt-and-braces: cargo build succeeding does not prove a .wasm was produced — a crate whose [lib] crate-type lost cdylib builds green and emits nothing, which is precisely a green nobody has seen red. So the artifact is stat'd by name.
Benchmarks and test infrastructure
(no crates in this group)
Incomplete-work markers (full)
| Location | Classification | Kind | Text |
agent/cli/src/main.rs:281 |
false-positive | code | _ => unreachable!(), |
agent/cli/src/main.rs:320 |
false-positive | code | _ => unreachable!(), |
agent/cli/src/main.rs:370 |
false-positive | code | _ => unreachable!(), |
agent/cli/src/main.rs:418 |
false-positive | code | _ => unreachable!(), |
agent/cli/src/main.rs:440 |
false-positive | code | _ => unreachable!(), |
agent/cli/src/main.rs:492 |
false-positive | code | _ => unreachable!(), |
agent/cli/src/main.rs:536 |
false-positive | code | _ => unreachable!(), |
agent/cli/tests/integration_artifactory_bench_twin.rs:26 |
heavy-test | test | //! Marked #[ignore] (container = heavy) exactly like the Nexus twin, so it runs |
bench-scenarios/src/nexus.rs:3 |
heavy-test | test | //! These port the two #[ignore]'d integration tests |
build-space/tests/appliance_container_robot.rs:6 |
heavy-test | test | //! #[ignore]d and opted into explicitly, matching the suite convention: |
mannequin/tests/smoke_serve.rs:10 |
heavy-test | test | //! #[ignore]'d). |
mannequin/tests/smoke_serve.rs:14 |
heavy-test | test | //! #[ignore]'d (heavy: builds + spawns the binary), so it runs in the release |
mannequin/tests/smoke_serve.rs:34 |
heavy-test | test | /// This is the boot proof CI can run every push, unlike the #[ignore]'d |
server/lib/src/upstream_rust.rs:233 |
heavy-test | test | #[ignore] |
server/repository/znippy-rust/tests/integration_znippy_serve.rs:609 |
heavy-test | test | /// Hermetic sibling of test_znippy_archive_smoke— NO network, NO#[ignore]. |
server/repository/znippy-rust/tests/integration_znippy_serve.rs:617 |
heavy-test | test | /// cargo test --workspace(and airgap/CI) — the#[ignore]d heavy tests above |
server/ui/tests/thin_client.rs:137 |
heavy-test | test | /// #[ignore] because it release-builds four egui applications. Run it with: |
server/ui/tests/tier_ladder.rs:40 |
heavy-test | test | //! * [the_tier_ladder_is_strictly_leaner_in_bytes] — #[ignore]d because it |
server/ui/tests/tier_ladder.rs:196 |
heavy-test | test | /// #[ignore] because it release-builds three egui applications. The crate-count |
xtask/examples/nornir-bench.rs:351 |
heavy-test | test | // These port the two #[ignore]'d integration tests: |
xtask/tests/build_verb.rs:281 |
heavy-test | test | // #[ignore]d, never caught the rename.) |
xtask/tests/build_verb.rs:291 |
heavy-test | test | /// HEAVY and #[ignore]d: this is three real release builds, and the full`` |
xtask/tests/holger_matrix.rs:26 |
heavy-test | test | //! and the mannequin daemon boot are explicit #[ignore]'d cells run in the |
xtask/tests/holger_matrix.rs:36 |
heavy-test | test | //! * mannequin::boot— the heavy#[ignore]'d daemon smoke test (spawns the |
xtask/tests/holger_matrix.rs:82 |
heavy-test | test | //! No light cell compiles a tier. The compiling proof lives in the #[ignore]'d |
xtask/tests/holger_matrix.rs:198 |
heavy-test | test | /// The mannequin **boot** cell — runs the #[ignore]'d smoke test that builds + |
xtask/tests/holger_matrix.rs:210 |
heavy-test | test | /// non-#[ignore]'d mannequin_boots_in_process_and_serves test |
xtask/tests/holger_matrix.rs:426 |
heavy-test | test | /// compile anything; the compiling proof is the #[ignore]'d heavy test in the |
xtask/tests/holger_matrix.rs:456 |
heavy-test | test | /// compiles. The compiling proof is the #[ignore]'d heavy test in |
xtask/tests/holger_matrix.rs:579 |
heavy-test | test | // --all and the #[ignore]'d heavy test in build_verb.rs.` |
xtask/tests/holger_matrix.rs:726 |
heavy-test | test | /// #[ignore]so it joins the release gate's--ignored pass. |
Tests inventory
658 unit test fns across 91 files; 239 integration test fns across 67 files.
| Crate | Unit test fns | Integration test fns |
agent |
33 | 14 |
airgap |
10 | 0 |
bench-scenarios |
6 | 0 |
build-space |
14 | 7 |
errcode |
7 | 2 |
holger-build |
15 | 2 |
holger-embed |
2 | 0 |
mannequin |
5 | 18 |
plugin |
28 | 39 |
server |
481 | 139 |
traits |
11 | 3 |
update-bundle |
44 | 0 |
xtask |
2 | 15 |
Size and generation metadata
Measured for the full pack (excludes this footer):
| Metric | Value |
| Bytes | 619669 |
| Lines | 12379 |
| Words | 79695 |
| Approx tokens (chars/4) | 154918 |
| Token budget | OVER by ~24918 |
| Duplicate-sentence ratio | 0.059 (max 0.080) |
| Duplicate-row ratio | 0.023 (max 0.050) |
⚠ Over budget. Trim in order: repeated descriptions → duplicated tables → product prose → trivial API docs → benchmark internals → repeated file references. Never trim security invariants, runtime wiring, failure semantics, trust lifecycle, state inventory, the task map, required tests, or action-safety boundaries.