nordisk/holger
server/cli/src/lib.rs
//! **The Holger server program**, as a library: [`run`] is `main` for every server binary
//! (`cargo install holger`, `cargo install holger-downstream`, the appliance builds).
//! The crates are thin: their `main` calls [`run`], and the features decide the product.
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use server_lib::{dev_pair_ron, read_ron_config, wire_holger};
/// glibc's malloc arenas capped (lane HOL-RSS): what `main` calls before the runtime.
pub use server_lib::memory::bound_allocator;
use std::path::{Path, PathBuf};
// The Secure Update Gateway lives behind the `secure_update` feature (see
// server/cli/Cargo.toml): the mvp-server / full / default builds compile it in;
// the read-only micro floors (`mini`/`embedded`) omit it.
#[cfg(feature = "secure_update")]
mod intel_cli;
#[cfg(feature = "bundle-verbs")]
mod secure_update;
#[cfg(feature = "secure_update")]
mod sigs_cli;
#[cfg(feature = "secure_update")]
mod sigstore_cli;
#[cfg(feature = "secure_update")]
mod time_cli;
// The airgap export of the approved set, and the landing half of an export
// bundle's import: the intake chain (`supply-chain`) and the bundle crate.
#[cfg(all(feature = "secure_update", feature = "supply-chain"))]
mod airgap;
// The sync protocol by hand (Holger v2.0 H4): key, invalidate, manifest, apply, ack, board, pull.
/// `holger-server passkey …` — invites, credentials and sessions over holger's
/// own passkey door, so nothing about the people is UI-only.
mod passkey_cli;
mod repo_key_cli;
#[cfg(feature = "sync")]
mod sync_cli;
/// `holger-server token …` — repository tokens for package clients (lane P3), over
/// `holger.token.v1.TokenService` with the operator's own credential.
#[cfg(feature = "auth-passkey")]
mod token_cli;
// The appliance identity ceremony (edda `wg-appliance-core`) lives behind the
// `appliance` feature; the container/ISO appliance cells declare the
// HOLGER_APPLIANCE=1 gate into their artifacts. See appliance.rs.
#[cfg(feature = "appliance")]
mod appliance;
// holger as a box of an installer-built estate (vm1 front, vm2 primary, vm3 twin).
#[cfg(feature = "paired")]
mod paired;
/// The one-way appliance's init without pairing (lane HOL-CDS-5, feature `pid1`).
#[cfg(feature = "pid1")]
pub mod pid1;
// DEV-NARRATE (feature `dev-narrate`): the boot log, sealed, over udp/51822; the STAGE lines.
// The holger-dev-narrate crate, shared with holger-estate-front (A28 L12).
#[cfg(feature = "paired")]
use holger_dev_narrate as dev_narrate;
// The box keeps the certificate its installer renews: the shared one (P4c, A28 M11).
#[cfg(feature = "paired")]
use nordisk_pairing::boxside::renewing;
#[cfg(any(feature = "paired", feature = "pid1"))]
mod sandbox_boot;
// `tracing`'s events into the process logger, its spans silent (a box's edda crates speak tracing).
#[cfg(feature = "paired")]
mod tracing_log;
// The systemd flavour (lane B4): `holger install` and `holger service` through
// Skíðblaðnir's systemd lifecycle crate alone, behind the `systemd` feature.
#[cfg(feature = "systemd")]
mod systemd_verbs;
#[cfg(feature = "systemd")]
use systemd_verbs::*;
#[derive(Parser)]
#[command(name = "holger-server")]
#[command(about = "Holger: Guards your artifacts at rest.", long_about = None)]
struct Cli {
/// With NO command, the FIRST START (`cargo install holger`, then `holger`):
/// the data root is `$HOLGER_DATA` or `~/.holger`, its `holger.ron` is written
/// on the first run (see [`first_start`]) and the server starts on it.
#[command(subcommand)]
command: Option<Commands>,
}
/// The first start's gRPC control plane (the `dev-pair` default, loopback only).
const FIRST_START_GRPC: &str = "127.0.0.1:18443";
/// The first start's HTTP door: the front page, `/healthz` and the repositories.
const FIRST_START_HTTP: &str = "127.0.0.1:18444";
/// **The first start** — `holger` with no command, the line the README gives after
/// `cargo install holger`. It needs no config and no Build Thing: the data root is
/// `$HOLGER_DATA` (or `~/.holger`); when its `holger.ron` is missing it is written
/// once from [`dev_pair_ron`] (a `/cache` caching mirror of crates.io plus a writable
/// `/sparring` registry, both on loopback), and an existing one is used as it is,
/// so the second start keeps the operator's edits. Returns the `start` it runs.
fn first_start() -> Result<Commands> {
let data = match std::env::var_os("HOLGER_DATA").filter(|v| !v.is_empty()) {
Some(d) => PathBuf::from(d),
None => std::env::var_os("HOME")
.map(|h| PathBuf::from(h).join(".holger"))
.context("neither $HOLGER_DATA nor $HOME is set: the first start needs a data root")?,
};
let config = first_start_config(&data)?;
println!(
"Holger first start: data {}, config {}",
data.display(),
config.display()
);
println!(" open http://{FIRST_START_HTTP}/");
println!(" health http://{FIRST_START_HTTP}/healthz");
println!(" cargo sparse+http://{FIRST_START_HTTP}/cache/index/");
Ok(Commands::Start { config })
}
/// Write `<data>/holger.ron` when it is missing (never over an existing one) and
/// return its path.
fn first_start_config(data: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(data).with_context(|| format!("create data dir {}", data.display()))?;
let config = data.join("holger.ron");
if !config.exists() {
std::fs::write(
&config,
dev_pair_ron(data, FIRST_START_GRPC, FIRST_START_HTTP),
)
.with_context(|| format!("write {}", config.display()))?;
}
Ok(config)
}
#[derive(Subcommand)]
enum Commands {
/// Start Holger server (gRPC endpoints from config)
Start {
#[arg(short, long)]
config: PathBuf,
},
/// Generate + start the dev release pair (/cache + /sparring) — the registry
/// nornir embeds. Writes the autogen config + archives under --data.
DevPair {
/// Data root for the generated config + the /cache and /sparring archives.
#[arg(long)]
data: PathBuf,
/// gRPC control-plane URL to bind.
#[arg(long, default_value = "127.0.0.1:18443")]
grpc: String,
/// HTTP sparse-registry bind addr cargo talks to
/// (`sparse+http://<addr>/cache/index/`).
#[arg(long, default_value = "127.0.0.1:18444")]
http: String,
},
/// Manage Holger as a systemd service on THIS host (add | update | remove |
/// status | render). Uses the Skíðblaðnir (skidbladnir) install engine.
///
/// Two **scopes** (auto-picked by privilege, overridable with `--scope`),
/// mirroring nornir with holger-named paths:
/// - `user` — non-root: a `systemctl --user` unit running as YOU, execing
/// `~/.cargo/bin/holger-server` in place, state in `~/.holger`. No `/etc`,
/// no system user.
/// - `system` — root: a unit running as the dedicated `holger` system user,
/// binary staged to `/usr/local/bin`, state under `/home/holger/.holger`.
#[cfg(feature = "systemd")]
Install {
#[command(subcommand)]
op: InstallOp,
},
/// Drive Holger's service lifecycle through the Skíðblaðnir **ship**
/// (`skidbladnir::service::Lifecycle`) — the single lifecycle expert nornir and
/// dwarfs share. holger declares a `holger_spec()` and delegates every verb
/// (install | start | stop | status | health | uninstall); it hand-rolls no
/// systemd. Same dual scope as `install` (auto-picked by privilege).
#[cfg(feature = "systemd")]
Service {
#[command(subcommand)]
op: ServiceOp,
},
/// Móðguðr security scan (Tools#3 CLI twin): walk a directory of artifacts (or
/// a repo staging tree), map each file to a modgunn `scan::Artifact`, and fold
/// the per-artifact CVE/license/provenance verdicts into a Pass/Warn/Block
/// report. Exits non-zero when any artifact BLOCKs (a gate-able check).
#[cfg(feature = "supply-chain")]
Scan(ScanArgs),
/// Retention / GC **plan** (dry-run): load a config, list one (or every)
/// repository, and report which artifacts a retention policy would KEEP vs
/// RECLAIM — keep-last-N versions per name, max-age, a keep-min floor, and a
/// store-size limit. Computes the plan only (holger's znippy archives are
/// immutable; carrying a plan out is a delegated archive-set drop), so it
/// deletes nothing. See `.nornir/retention-design.md`.
Retention(RetentionArgs),
/// Cross-repo artifact SEARCH (parity §H): load a config and find artifacts
/// across every configured repository by name, GAVC-style coordinate
/// (namespace/group · name · version), sha256 content checksum, or a raw
/// archive path substring. Read-only — the same engine backs the
/// `SearchService` gRPC RPC + the `GET /-/search` HTTP door.
Search(SearchArgs),
/// SBOM hosting (parity §6/§11): attach a CycloneDX SBOM document to an
/// artifact coordinate, or fetch a hosted one back byte-identical. Drives the
/// same on-disk store the server hosts (derived from the config's audit dir),
/// so a CLI-attached SBOM is served by the `SbomService` RPC + `GET /-/sbom`
/// door. See `.nornir/sbom-hosting-design.md`.
#[cfg(feature = "supply-chain")]
Sbom(SbomArgs),
/// Custom artifact PROPERTIES (parity §8): set/remove or read `key=value(s)`
/// metadata on an artifact coordinate. Drives the same on-disk store the
/// server's `property` search axis + `GET /-/properties` door read, so a
/// CLI-set property is immediately searchable. See `.nornir/properties-design.md`.
#[cfg(feature = "supply-chain")]
Properties(PropertiesArgs),
/// Repository REPLICATION (parity §I): copy a repo's artifacts from a SOURCE
/// holger store into a DESTINATION store. Load two RON configs (`--from`,
/// `--to`), pair repos by name, and copy every artifact the destination does
/// not already hold BY CONTENT ID — idempotent, so a re-run over an unchanged
/// source is a no-op. Dry-run (preview) unless `--execute`. Reads only from
/// the source, writes only into the destination; the same engine backs the
/// `ReplicationService.Replicate` gRPC RPC. See `.nornir/replication-design.md`.
Replicate(ReplicateArgs),
/// MIRROR from a declared upstream: one cycle of list → diff → fetch →
/// adopt, for every pair the config's `mirror:` block declares.
///
/// This is the caller `server/lib/src/mirror.rs` was written for. The change
/// map under the block's `ron_state_dir` is what makes the SECOND cycle
/// cheap and what survives a restart: a coordinate already proved present is
/// not re-fetched, and one whose sameness cannot be PROVED is re-fetched
/// unless that pair opted into `ron_unproven: TrustSize`.
///
/// The running server does this on its own timer. This verb runs exactly one
/// cycle and prints the counts, so a cycle can be driven and MEASURED without
/// waiting on a clock. A one-way build (no `fetch`) has no mirror.
#[cfg(feature = "fetch")]
Mirror(MirrorArgs),
/// PROMOTE one artifact DEV → PROD (parity §J): read it out of a source
/// repository and SEAL it, by content address, into the immutable PROD store
/// of a destination config. Runs the same policy + property gates the served
/// path runs, and is a dry-run unless `--execute`. Local and in-process (no
/// network) — see the ACCESS CONTROL note on `promote --help`.
/// See `.nornir/promotion-design.md`.
#[cfg(feature = "supply-chain")]
Promote(PromoteArgs),
/// The INTAKE CHAIN's wire verb, over a server's gRPC door with your own
/// credential: `advance` (the next state), `quarantine`, `release`, `block`,
/// or `state` (state, history, evidence). Separation of duties decides: the
/// importer never approves, APPROVED needs the configured number of distinct
/// approvers, Developer and Auditor move nothing. See
/// `server/lib/src/promote/duties.rs`.
#[cfg(feature = "intake")]
Intake(IntakeArgs),
/// The AUDIT LOG: `trail` (one artifact's ordered trail by sha256, over a
/// server's gRPC door as yourself — an Auditor or an Admin), `verify` (walk
/// the hash chain of a local audit dir and name the first break; exit 1 on
/// a break), `query` (filter and page the records of a local audit dir: --who,
/// --repository, --artifact, --action, --source-ip, --status, --since, --until),
/// `aggregate` (count them, --by repo,action,day,ident,client,status), `export`
/// (with --out: the selected records as one Arrow IPC file; without: send every
/// enabled `audit.siem` sink from its cursor to the end of the log, once). The
/// reads go through the Arrow index the chain keeps current. See
/// `server/lib/src/audit/`.
Audit(AuditArgs),
/// The signed AIRGAP EXPORT of the approved set: AVAILABLE artifacts of the
/// intake chain's production repository, with their stored evidence, signed
/// attestations, SBOMs and intake records, as a bundle candidate whose
/// verdict is COMPUTED from that evidence. An artifact without passing
/// evidence is refused by name and nothing is written. Approve and
/// release-sign the candidate with `holger-bundle approve` / `finalize`;
/// `holger-server bundle import` on the secure side verifies it and lands
/// each artifact AVAILABLE. See `server/lib/src/promote/airgap.rs`.
#[cfg(all(feature = "secure_update", feature = "supply-chain"))]
Export(airgap::ExportArgs),
/// THE PUSH INTO THE DIODE (lane HOL-CDS): one push of the `export:` block — the new
/// AVAILABLE artifacts as one signed bundle, written atomically into `export.push_to`.
/// The started server pushes every `export.every`. `--trust-policy-out <file>` writes the
/// pinned policy the one-way downstream verifies the pushes with.
#[cfg(feature = "export-push")]
ExportPush(airgap::ExportPushArgs),
/// The SYNC PROTOCOL between an upstream Holger and its downstreams (H4): `key`,
/// `invalidate` (upstream), `manifest --out <stick>` (upstream), `apply --manifest
/// <file> --ack-out <stick>` (downstream), `ack --ack <file>` (upstream), `board`
/// (upstream, JSON), `pull` (downstream, `online`). See `server/lib/src/sync.rs`.
#[cfg(feature = "sync")]
Sync(sync_cli::SyncArgs),
/// The REPOSITORY SIGNING KEY of the deb and rpm views (lane HOL-SIGNED-APT-DNF):
/// `create` makes this box's OpenPGP key once in the data root's key store (0600) and
/// prints its public half; `show` prints the public key `/-/repo-key.asc` serves. With
/// it the views serve `InRelease`, `Release.gpg` and `repomd.xml.asc`, so apt needs no
/// `[trusted=yes]` and dnf runs with `repo_gpgcheck=1`.
RepoKey(repo_key_cli::RepoKeyArgs),
/// PAIR two holgers: an upstream records a downstream holger and the
/// repositories it may pull; a downstream records the upstream its mirror
/// pulls from. Once, like the twin's election. See `PairArgs`.
Pair(PairArgs),
/// RECHARGE a fresh main from its twin, by hand — the same lane monetize's
/// restore runs through holger's plugin: the four refusals before a byte
/// moves (no twin; the main not fresh — re-image it; an empty twin; a twin
/// that cannot give everything back without --accept-lost), certify the
/// main against the TWIN's replication door, `Recharge` (the pull over
/// mutual TLS; resumable — run it again), then the twin re-pointed at the new
/// main. Over both boxes' AdminService with the console's bearer. See
/// `server/lib/src/recharge_lane.rs`.
Recharge(RechargeArgs),
/// **JOIN THE ONE MONETIZE** (metered): redeem the operator's one-shot attach token at
/// `POST <door>/attach/enrol`, signed by the estate owner's OpenSSH key, and keep what
/// it answers (the monetize key orders verify under, the estate's certificate) under
/// the data root, where this box's pull channel reads it on its next start. Monetize
/// never dials this box; the box calls in. See `holger-fleet/src/enrol.rs`.
#[cfg(feature = "metered")]
FleetEnrol(FleetEnrolArgs),
/// **THE TWIN'S ESTATE, both directions** — the config and the side stores a
/// replica must hold, by data root. `estate --from <primary> --to <twin>` is
/// a follow; `estate --from <twin> --to <new primary>` is a RESTORE, and it
/// is the same code because a restore is a follow with the roots swapped.
///
/// Until 2026-09-21 there was NO verb of either kind:
/// `replication::replicate_estate` had no caller outside its own tests, so a
/// twin's evidence, audit, properties, SBOMs, sealed PROD store, passkeys,
/// tenants, entitlements, meters, jobs and served config could not be moved
/// by any command holger shipped. Artifacts are the other half and move with
/// `replicate --from --to`, which takes RON configs rather than data roots.
Estate(EstateArgs),
/// The PEOPLE, over this server's passkey door (`/-/auth/passkey/*` on the
/// HTTP gateway): whoami · invite · invites · revoke-invite · credentials ·
/// revoke-credential · revoke-sessions. The same decisions the console's
/// pages get — holger's: admin for administration, step-up for a passkey
/// session, never an admin invite.
Passkey(passkey_cli::PasskeyArgs),
/// REPOSITORY TOKENS for package clients, over `holger.token.v1.TokenService`:
/// list · revoke · mint. The box's static bearer lists every token and revokes any;
/// a passkey session lists, revokes and mints its own.
#[cfg(feature = "auth-passkey")]
Token(token_cli::TokenArgs),
/// Secure Update Gateway: verified offline bundle import (anti-rollback,
/// atomic, audited). `bundle import` — there is no --force bypass.
#[cfg(feature = "bundle-verbs")]
Bundle(secure_update::BundleArgs),
/// Integrity scrub of the imported bundle store (read-only; non-zero on any
/// corruption).
#[cfg(feature = "bundle-verbs")]
Scrub(secure_update::ScrubArgs),
/// Backup create / verify / restore of the protected store (bundles +
/// anti-rollback state + receipts); excludes private keys and inline secrets.
#[cfg(feature = "bundle-verbs")]
Backup(secure_update::BackupArgs),
/// Offline vulnerability intelligence: `intel build` (online side: OSV /
/// NVD / CISA KEV → one signed, dated intel bundle), `intel import` (secure
/// side: verify, anti-rollback, fail-closed audit → local advisory
/// database), `intel status`, `intel scan` (offline, against that database).
#[cfg(feature = "secure_update")]
Intel(intel_cli::IntelArgs),
/// Scanner signatures (ClamAV's .cvd/.cld, YARA) across the airgap as a signed bundle
/// (`holger.scanner-sigs/v1`): `scanner-sigs build` on the online side, `bundle import`
/// on the secure side installs it for the sandboxed clamd, `scanner-sigs status` says
/// what is installed. A hotfix is a bundle with the next sequence and one file.
#[cfg(feature = "secure_update")]
#[command(name = "scanner-sigs")]
ScannerSigs(sigs_cli::SigsArgs),
/// **The job runner** (lane HOL-SANDBOX-TRAIT): where this config's untrusted work
/// runs (every bundle's verification, every scan), what that runner has and has not
/// (a container per job, a generated syscall profile, Landlock, the syscall broker,
/// wasm) and the id of the per-boot key its results are pinned to. Interim until
/// Heimdall drops in.
#[cfg(feature = "intake")]
Jobs {
/// The RON config.
#[arg(long)]
config: PathBuf,
},
/// THE TIME FLOOR (lane HOL-NCSC): `time-token make` (the online side: a signed
/// `holger.time-token/v1`, carried in like any bundle, which raises the airgap box's
/// monotone time floor) and `time-token status --config C`.
#[cfg(feature = "secure_update")]
TimeToken(time_cli::TimeTokenArgs),
/// THE SIGSTORE TRUST ROOT (lane HOL-OCI-EVIDENCE): `sigstore-trust make` (the release
/// build: the pinned trusted_root.json signed into a holger.sigstore-trust/v1 bundle, and
/// the policy pinning its signers) and `sigstore-trust status --config C`.
#[cfg(feature = "secure_update")]
SigstoreTrust(sigstore_cli::SigstoreTrustArgs),
}
/// Knobs for the `holger-server estate` verb — move a data root's CONFIG AND
/// SIDE STORES between two boxes.
#[derive(Args)]
struct EstateArgs {
/// SOURCE data root (`/var/lib/holger` on the cloud box, `/data` on an
/// appliance). Read-only: nothing here is written or deleted.
#[arg(long)]
from: PathBuf,
/// DESTINATION data root. Created if it does not exist (under `--execute`).
#[arg(long)]
to: PathBuf,
/// Actually copy. Absent = a dry run that writes nothing, the same gate
/// `replicate` uses — and the same default, because the destructive
/// direction of this verb is a restore onto a box somebody is rebuilding.
#[arg(long)]
execute: bool,
}
/// `holger-server estate --from --to` — the follow AND the restore.
///
/// One function for both directions on purpose. A restore that ran different
/// code from the follow would be code that is exercised once, on the worst day.
fn run_estate(a: EstateArgs) -> Result<()> {
use server_lib::replication::ReplicationOptions;
let opts = ReplicationOptions {
execute: a.execute,
limit: 0,
};
let report = server_lib::replication::replicate_estate(&a.from, &a.to, &opts)
.map_err(|e| anyhow::anyhow!("{e}"))?;
println!(
"estate {} → {} [{}]: copied {}, skipped {}, would-copy {}, failed {}",
a.from.display(),
a.to.display(),
if a.execute { "execute" } else { "dry-run" },
report.copied(),
report.skipped(),
report.would_copy(),
report.failed(),
);
for item in &report.items {
if item.absent_on_primary {
println!(
" [absent] {} — not on the source; nothing to copy",
item.path
);
continue;
}
println!(
" [{}] {} — copied {} skipped {} would {} failed {}",
if item.failed > 0 { "FAIL" } else { "ok" },
item.path,
item.copied,
item.skipped,
item.would_copy,
item.failed,
);
for f in &item.failures {
println!(" {f}");
}
}
// ★ The one thing this verb must never do quietly: report a clean run over a
// data root whose ARTIFACT STORE it did not touch.
if let Some(warning) = report.unreplicated_warning() {
println!(" {warning}");
println!(
" → move the artifacts with: holger-server replicate --from <source holger.ron> \
--to <dest holger.ron> --execute"
);
}
functional_status(
"holger-cli/estate",
"estate_replication_ran",
report.is_complete(),
&format!(
"copied {} skipped {} failed {} uncovered {} (execute={})",
report.copied(),
report.skipped(),
report.failed(),
report.unreplicated.len(),
a.execute,
),
);
if !report.is_complete() {
anyhow::bail!("estate replication had {} failure(s)", report.failed());
}
Ok(())
}
/// Knobs for the `holger-server replicate` verb — copy artifacts between two
/// file-backed holger stores (laptop-testable, no network).
#[derive(Args)]
struct ReplicateArgs {
/// SOURCE RON config (read-only here — only listed + fetched).
#[arg(long)]
from: PathBuf,
/// DESTINATION RON config (where artifacts land — its repos must be writable).
#[arg(long)]
to: PathBuf,
/// Restrict to a single source repository name. Omit = every repo with a
/// same-named destination.
#[arg(long)]
repo: Option<String>,
/// Actually copy. Omit for a dry-run preview (reports what would copy, writes
/// nothing).
#[arg(long, default_value_t = false)]
execute: bool,
/// Per-repo cap on artifacts examined (0 = unlimited).
#[arg(long, default_value_t = 0)]
limit: usize,
}
/// Knobs for the `holger-server mirror` verb.
#[derive(Args)]
struct MirrorArgs {
/// This holger's RON config — the one carrying the `mirror:` block.
#[arg(long)]
config: PathBuf,
/// Restrict to one declared source by `ron_name`. Omit = every source.
#[arg(long)]
source: Option<String>,
/// Restrict to one upstream repository by `ron_from`. Omit = every pair.
#[arg(long)]
repo: Option<String>,
/// Print each pair's report as one JSON object per line, for a harness.
#[arg(long, default_value_t = false)]
json: bool,
}
/// Knobs for the `holger-server promote` verb — seal ONE artifact from a source
/// (DEV) store into the immutable PROD store of a destination config.
///
/// # ACCESS CONTROL — read this before scripting it
///
/// This local verb is **not access-controlled**. The gRPC
/// `PromotionService.PromoteArtifact` path enforces an **ADMIN authz gate scoped to
/// the destination repo** and records `promoted_by` from the *authenticated*
/// identity (mTLS CN / OIDC `sub`). Running here, the only gate is **filesystem
/// permissions** on the configs and the store, and `promoted_by` is recorded as the
/// honest **OS user** that ran the command — never a faked admin subject. That is
/// the same trade-off `replicate` already makes; use the served path when you need
/// an authenticated, per-repo-authorised promotion.
///
/// The POLICY gates are NOT relaxed here: the `promotion:` block's gates (CVE /
/// license / provenance scan) and `require_property` apply exactly as they do on
/// the served path, a target the --to config does not serve is refused by name,
/// and a misconfigured advisory feed fails CLOSED.
#[derive(Args)]
#[command(after_help = "\
ACCESS CONTROL — this local verb is NOT access-controlled.
The gRPC PromotionService.PromoteArtifact path enforces an ADMIN authz gate
SCOPED to the destination repo, and records promoted_by from the AUTHENTICATED
identity (mTLS CN / OIDC sub). Here the only gate is FILESYSTEM PERMISSIONS on
the configs and the store, and promoted_by is the honest OS user ($USER, else
$LOGNAME, else uid:<euid>) — never a faked admin subject. Same trade-off as
`replicate`; use the served path when you need an authorised promotion.
POLICY IS NOT RELAXED HERE
The --to config's promotion: gates (CVE/license/provenance scan) and
require_property apply exactly as on the served path, a target repository
the --to config does not serve is refused by name, and a misconfigured
advisory feed FAILS CLOSED (refuses to promote).
DRY-RUN BY DEFAULT — a promotion is irreversible; pass --execute to seal.")]
#[cfg(feature = "supply-chain")]
struct PromoteArgs {
/// SOURCE (DEV) RON config — read-only here; the artifact is fetched from it.
#[arg(long)]
from: PathBuf,
/// DESTINATION (PROD) RON config — its data root owns the immutable PROD store
/// the artifact is sealed into.
#[arg(long)]
to: PathBuf,
/// Source repository name (the DEV shelf the artifact lives on).
#[arg(long)]
repo: String,
/// Destination repository name (the PROD shelf to seal into). Defaults to
/// `--repo`, mirroring how `replicate` pairs repositories by name.
#[arg(long)]
to_repo: Option<String>,
/// Artifact namespace (Maven groupId / npm scope). Omit for none.
#[arg(long, alias = "group")]
namespace: Option<String>,
/// Artifact name.
#[arg(long)]
name: String,
/// Artifact version.
#[arg(long)]
version: String,
/// Actually seal. Omit for a dry-run preview: the gates still run and the
/// verdict is reported, but NOTHING is written. A promotion is irreversible —
/// the immutable store refuses to overwrite a sealed coordinate — so the safe
/// default matters here even more than it does for `replicate`.
#[arg(long, default_value_t = false)]
execute: bool,
}
/// `holger-server sbom` — attach / fetch hosted SBOM documents.
#[derive(Args)]
#[cfg(feature = "supply-chain")]
struct SbomArgs {
#[command(subcommand)]
action: SbomAction,
}
#[derive(Subcommand)]
#[cfg(feature = "supply-chain")]
enum SbomAction {
/// Attach (host) an SBOM document to an artifact coordinate. Reads the raw
/// document bytes from `--file` and stores them; a re-attach overwrites.
Attach(SbomAttachArgs),
/// Fetch a hosted SBOM document back (byte-identical), to `--out` or stdout.
Fetch(SbomFetchArgs),
}
/// The artifact coordinate axes shared by attach + fetch.
#[derive(Args)]
struct SbomCoord {
/// RON config to load (same file the server starts with; the SBOM store root
/// is derived from its audit dir).
#[arg(short, long)]
config: PathBuf,
/// Repository the artifact lives in.
#[arg(long)]
repo: String,
/// Artifact namespace (Maven groupId / npm scope). Omit for none.
#[arg(long, alias = "group")]
namespace: Option<String>,
/// Artifact name.
#[arg(long)]
name: String,
/// Artifact version.
#[arg(long)]
version: String,
}
#[derive(Args)]
struct SbomAttachArgs {
#[command(flatten)]
coord: SbomCoord,
/// Path to the SBOM document (CycloneDX 1.5 JSON) to host.
#[arg(long)]
file: PathBuf,
}
#[derive(Args)]
struct SbomFetchArgs {
#[command(flatten)]
coord: SbomCoord,
/// Write the fetched SBOM here; omit to print to stdout.
#[arg(long)]
out: Option<PathBuf>,
}
/// Knobs for the `holger-server search` cross-repo search verb. Every axis is
/// optional and they compose (AND); a bare invocation lists everything.
#[derive(Args)]
struct SearchArgs {
/// RON config to load (same file the server starts with).
#[arg(short, long)]
config: PathBuf,
/// Case-insensitive substring on the artifact name.
#[arg(long)]
name: Option<String>,
/// Case-insensitive substring on the namespace (Maven groupId / npm scope).
#[arg(long, alias = "group")]
namespace: Option<String>,
/// Exact artifact version.
#[arg(long)]
version: Option<String>,
/// sha256 content checksum (hex) — forces a per-candidate fetch + hash.
#[arg(long, alias = "sha256")]
checksum: Option<String>,
/// Case-insensitive substring on a raw archive file path (yields path hits).
#[arg(long)]
path: Option<String>,
/// Custom-property filter `key=value` (repeatable, AND-composed). `key` alone
/// (no `=`) matches "the key is present, any value".
#[arg(long = "property", alias = "prop", value_name = "KEY=VALUE")]
properties: Vec<String>,
/// Restrict the search to these repositories (repeatable). Empty = all.
#[arg(long = "repo")]
repos: Vec<String>,
/// Cap on hits per kind (0 = server default).
#[arg(long, default_value_t = 0)]
limit: usize,
}
/// `holger-server properties` — set / get custom artifact properties (the
/// `property` search axis's write + read side). The property store root is
/// derived from the config's audit dir, so the CLI, the gRPC `Search` axis, and
/// the `GET /-/properties` HTTP door all share one on-disk store.
#[derive(Args)]
#[cfg(feature = "supply-chain")]
struct PropertiesArgs {
#[command(subcommand)]
action: PropertiesAction,
}
#[derive(Subcommand)]
#[cfg(feature = "supply-chain")]
enum PropertiesAction {
/// Set (or, with no `--value`, REMOVE) a property key on a coordinate.
Set(PropertiesSetArgs),
/// Print all properties on a coordinate as JSON (`{}` when none).
Get(PropertiesGetArgs),
}
/// The coordinate axes shared by properties set + get (mirrors [`SbomCoord`]).
#[derive(Args)]
struct PropertiesCoord {
/// RON config to load (the property store root is derived from its audit dir).
#[arg(short, long)]
config: PathBuf,
/// Repository the artifact lives in.
#[arg(long)]
repo: String,
/// Artifact namespace (Maven groupId / npm scope). Omit for none.
#[arg(long, alias = "group")]
namespace: Option<String>,
/// Artifact name.
#[arg(long)]
name: String,
/// Artifact version.
#[arg(long)]
version: String,
}
#[derive(Args)]
struct PropertiesSetArgs {
#[command(flatten)]
coord: PropertiesCoord,
/// Property key to set.
#[arg(long)]
key: String,
/// Value(s) for the key (repeatable). Omit ALL to REMOVE the key.
#[arg(long)]
value: Vec<String>,
}
#[derive(Args)]
struct PropertiesGetArgs {
#[command(flatten)]
coord: PropertiesCoord,
}
/// Knobs for the `holger-server retention` planner verb. Every policy dimension
/// is optional and they compose; omitting them all is a keep-everything preview.
#[derive(Args)]
struct RetentionArgs {
/// RON config to load (same file the server starts with).
#[arg(short, long)]
config: PathBuf,
/// Repository to plan. Omit to plan every configured repository.
#[arg(long)]
repo: Option<String>,
/// Keep the N newest versions per artifact name; reclaim the rest.
#[arg(long)]
keep_last_n: Option<usize>,
/// Reclaim artifacts older than this many seconds (needs an ingest time; the
/// listing carries none today, so this is a no-op until per-artifact ingest
/// times land — see `.nornir/retention-design.md`).
#[arg(long)]
max_age_secs: Option<i64>,
/// Never drop below this many versions of a name, even when age or the size limit would.
#[arg(long)]
keep_min_per_name: Option<usize>,
/// Store-size limit (bytes): evict oldest-first until the kept set fits.
#[arg(long)]
max_total_bytes: Option<u64>,
/// Turn OFF the "never delete the newest version of a name" safety net
/// (protection is ON by default).
#[arg(long, default_value_t = false)]
no_protect_latest: bool,
/// Pin any artifact carrying this custom property from reclamation:
/// `key=value`, or bare `key` for "any value" (e.g. `--protect-property
/// keep=true`). A tagged blob is never dropped by any rule.
#[arg(long = "protect-property", value_name = "KEY=VALUE")]
protect_property: Option<String>,
/// **Actually reclaim** the plan's delete-set (drop the superseded artifact
/// bytes from the store) and historize the GC to the audit warehouse. OFF by
/// default — without this the verb is a dry-run that deletes nothing. Each
/// delete is content-id verified before the unlink; protected/latest
/// generations are never dropped.
#[arg(long, default_value_t = false)]
execute: bool,
/// Print the per-artifact keep/reclaim verdict, not just the roll-up.
#[arg(long, default_value_t = false)]
verbose: bool,
}
/// Knobs for the `holger-server scan` Móðguðr security verb.
#[derive(Args)]
#[cfg(feature = "supply-chain")]
struct ScanArgs {
/// Directory of artifacts to scan (walked recursively). Filenames carry the
/// coords (`name-version.ext`) the verdict is keyed by.
#[arg(long)]
path: PathBuf,
/// Ecosystem of the artifacts (`rust`/`cargo`, `pip`, `npm`, `maven`, …).
/// Default: `rust`.
#[arg(long, default_value = "rust")]
ecosystem: String,
/// A Holger RON config. With an `intel:` block the scan reads the local
/// advisory database (offline; the evidence names its sequence and date,
/// and a stale one warns, then blocks) instead of the demo set.
#[arg(long)]
config: Option<PathBuf>,
}
/// **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 CLI verb emits one so `nornir test
/// --features testmatrix` SEES the verb fire. Mirrors the reference emitter in
/// `korp-collectors/src/lib.rs`.
#[inline]
fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(component, check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (component, check, ok, detail);
}
}
/// Install the process logger. Replaces a bare `env_logger::init()`, and the whole
/// difference is `default_filter_or`.
///
/// `env_logger::init()` defaults to `error` when
/// `RUST_LOG` is unset, and the rendered systemd unit
/// (`holger_service_spec` → `skidbladnir::systemd`) sets no `RUST_LOG` either. So
/// on the shipped service every boot-time `SECURITY:` / `AUTH:` line holger emits
/// — the unpinned-provider warning, the mTLS-CA trust note, the unauthenticatable
/// read gate, the `auth_allow_unbound_oidc` acknowledgement — was written at
/// `warn` and dropped on the floor: the operator's evidence that a control is OFF
/// was invisible precisely on the deployment that never configured logging.
///
/// The floor is raised in the BINARY rather than by adding `Environment=RUST_LOG=`
/// to the unit, because an inline `Environment=` would then override the
/// operator's own setting and would only help installs that came through
/// `holger-server service install`. `RUST_LOG` still wins wherever it is set.
fn init_logger() {
let mut b = env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or(server_lib::DEFAULT_LOG_FILTER),
);
// A box of an installer-built estate: every line ALSO goes to the boot log
// the installer collects (the ESP's story first), still to stderr.
#[cfg(feature = "paired")]
if let Some(target) = paired::log_target() {
b.target(target);
}
b.init();
// DEV builds only: the same boot log, sealed to the installer's key, over UDP from the
// moment the NIC has an address — before the installer's link (Gunnar's dev media).
#[cfg(feature = "dev-narrate")]
if paired::seed_staged() {
dev_narrate::start(
&paired::BOOT_LOG,
paired::appliance_root(),
"holger-server",
|l| eprintln!("{l}"),
);
}
// A box's edda crates speak `tracing`: their EVENTS are carried into this logger
// (and so into the boot log), their SPANS print nothing — with no subscriber,
// `tracing`'s `log` feature printed `WARN wg; peer=…` for every packet the
// dataplane sealed (tracing_log.rs).
#[cfg(feature = "paired")]
let _ = tracing::subscriber::set_global_default(tracing_log::EventsIntoLog::logger());
}
/// **The program** — what every Holger server binary runs (`holger`, `holger-downstream`,
/// the appliance's `holger-mvp`): the features the binary was built with decide what it is.
/// `holger-downstream` is its own crate whose `main` calls this, with the gateway's features.
pub async fn run() -> Result<()> {
// A panic on the appliance ends as a fault (`HL-99`, lane G6, Gunnar's GN-99): the
// crash record, then a serving box restarts (unless it loops) and a booting one powers off.
#[cfg(feature = "paired")]
paired::install_panic_fault();
#[cfg(all(feature = "pid1", not(feature = "paired")))]
pid1::install_panic_fault();
let out = run_main().await;
// **PID 1 never returns from `main`** (A7 row 17, Gunnar 539a73e3 / a164f13e): that is
// the kernel's `Attempted to kill init!`, no code, and `panic=10` boots into the same
// fault. An `Err` here says its HL code, keeps the crash record, then a box that served
// restarts (unless it faulted within 30 min) and a booting box powers off. An estate
// box's refused start never gets here: it stays up and starts again (lane D10).
#[cfg(feature = "paired")]
if let Err(e) = &out {
if appliance_pid1::net::is_init() {
paired::fault_end(&format!("{e:#}"));
}
}
#[cfg(all(feature = "pid1", not(feature = "paired")))]
if let Err(e) = &out {
if appliance_pid1::net::is_init() {
pid1::fault_end(&format!("{e:#}"));
}
}
out
}
async fn run_main() -> Result<()> {
// `--version` / `-V`: print the single-source identity string
// (`holger v<CARGO_PKG_VERSION> (<git-hash>)`) and EXIT before any boot,
// logger init, or config load. Handled manually (clap has no version set on
// this command) so every holger-* server bin prints the SAME string that the
// gRPC + in-process `health.version` reports. See `server_lib::VERSION_LINE`.
// Only a GLOBAL `--version` counts — one that appears BEFORE any subcommand.
// Scanning every argument (the original form) meant a verb's own `--version`
// VALUE flag was swallowed: `sbom fetch … --version 1.0.0`, `properties set …
// --version 1.0.0` and `promote … --version 1.0.0` all printed the banner and
// exited 0 without doing the work — a silent no-op for any script that passed
// an artifact version. Stop at the first non-flag token (the subcommand).
if std::env::args()
.skip(1)
.take_while(|a| a.starts_with('-'))
.any(|a| a == "--version" || a == "-V")
{
println!("{}", server_lib::VERSION_LINE);
return Ok(());
}
init_logger();
let cli = Cli::parse();
let command = match cli.command {
Some(c) => c,
None => first_start()?,
};
match command {
Commands::Start { config } => {
// PID 1 on a box: the reaper and the network come before everything
// (korp-start brings no network up; the installer's link rides eth1).
// The live trial, armed at the first line (lane B3, A9 row 6): before the network,
// the pairing and `boot_server`, whatever they wait for.
#[cfg(feature = "paired")]
paired::arm_trial();
#[cfg(feature = "paired")]
paired::stage(
"exec",
&format!(
"{} dev-narrate={}",
server_lib::VERSION_LINE,
if dev_narrate::ENABLED { "on" } else { "off" }
),
);
#[cfg(feature = "paired")]
let is_pid1 = paired::pid1_duties().map_err(|e| anyhow::anyhow!("{e}"))?;
#[cfg(feature = "paired")]
if is_pid1 {
appliance_pid1::fault::reached("network");
// The previous stop's record (the ESP survives a read-only /data), said once and removed.
appliance_pid1::stop_record::say_previous(paired::STOP_RECORD_PRODUCT, &|l| {
log::info!("{l}")
});
}
#[cfg(feature = "paired")]
paired::stage(
"network",
if is_pid1 {
"PID 1: the declared interfaces are up"
} else {
"not PID 1: the host's network"
},
);
// The one-way appliance (lane HOL-CDS-5): PID 1's duties without pairing, the
// network from the medium; it dials nothing.
#[cfg(all(feature = "pid1", not(feature = "paired")))]
let is_pid1 = pid1::duties().map_err(|e| anyhow::anyhow!("{e}"))?;
// The appliance ceremony runs FIRST, before any config load, so the
// canonical identity line is the first thing on stdout — the line
// every robot, pane and operator watches for. Held in a binding:
// dropping the guard tears the tunnel down, so it lives exactly as
// long as the serve loop below.
#[cfg(feature = "appliance")]
let _appliance_guard = if appliance::gate_enabled() {
match appliance::run_ceremony() {
Ok(a) => {
functional_status(
"holger-appliance",
"identity_ready",
true,
&format!(
"pubkey={} minted={} tunnel={}",
a.public_b64,
a.minted,
a.tunnel.is_some()
),
);
Some(a)
}
Err(msg) => {
functional_status("holger-appliance", "identity_ready", false, &msg);
eprintln!("{msg}");
anyhow::bail!(
"appliance ceremony refused — see the line above; \
holger does not serve as a half-configured appliance"
);
}
}
} else {
None
};
// Built WITHOUT the ceremony but asked to run it: refuse loudly.
// A silent no-op here would boot an appliance with no identity and
// no tunnel that LOOKS healthy from the outside.
#[cfg(not(feature = "appliance"))]
if std::env::var("HOLGER_APPLIANCE").ok().as_deref() == Some("1") {
anyhow::bail!(
"HOLGER_APPLIANCE=1 is set but this binary was built without the \
`appliance` feature — it cannot mint an identity or bring a tunnel \
up. Build with `--features appliance` (the declared appliance cell \
does), or unset HOLGER_APPLIANCE."
);
}
println!("Starting Holger {:?}", config);
// ★ THE LISTENER BEFORE THE DECLARATION (lane B1). R10's installed main read
// `/data/holger.ron` first, found none, and PID 1 died before 8474 existed — the
// installer could not even fetch the log that said why. A box whose medium names
// its data root (`HOLGER_DATA`) opens the installer's listener FIRST; a missing or
// bad declaration is then a refusal in its pair status and a retry (below), never
// an ended box. The declaration itself is the release's, unless the site laid an
// override at the configured path (`server_lib::declaration`).
#[cfg(feature = "paired")]
let early_root = std::env::var_os("HOLGER_DATA").map(std::path::PathBuf::from);
#[cfg(not(feature = "paired"))]
let early_root: Option<std::path::PathBuf> = None;
let read_declared = |config: &std::path::Path| -> anyhow::Result<server_lib::Holger> {
let (h, said) = server_lib::declaration::read(config)?;
log::warn!("{said}");
Ok(h)
};
let (first_read, peers_root) = match &early_root {
Some(root) => (read_declared(&config), root.clone()),
None => {
let h = read_declared(&config)?;
let root = h.peers_root();
(Ok(h), root)
}
};
if let (Ok(h), Some(root)) = (&first_read, &early_root) {
if &h.peers_root() != root {
log::warn!("declaration: its data root {} is not the medium's HOLGER_DATA {} — the pairing records stay under HOLGER_DATA", h.peers_root().display(), root.display());
}
}
// A box of an installer-built estate serves the installer's listener
// from its first moment.
#[cfg(feature = "paired")]
let _paired = paired::start_or_retry(&peers_root);
#[cfg(not(feature = "paired"))]
let _ = &peers_root;
#[cfg(feature = "paired")]
if _paired.is_some() {
appliance_pid1::fault::reached("pairing");
paired::stage(
"pairing",
"the seed is read and the installer's listener serves",
);
}
#[cfg(feature = "paired")]
let on_estate = _paired.is_some();
#[cfg(not(feature = "paired"))]
let on_estate = false;
// An estate box never ends on a refused start (lane D10): as PID 1 that hung the
// box with its last pair status frozen at PAIRED. It says the refusal, word for
// word, in its pair status and starts again with a backoff — a feed that arrives
// late (the console's bearer) clears it.
let mut first = Some(first_read);
#[cfg(feature = "paired")]
let mut refusals = 0u32;
let holger = loop {
let attempt = match first.take() {
Some(h) => h,
None => read_declared(&config),
};
let started = match attempt {
Ok(h) => boot_server(h, on_estate).await,
Err(e) => Err(e),
};
match started {
Ok(h) => break h,
Err(e) if on_estate => {
#[cfg(not(feature = "paired"))]
drop(e);
#[cfg(feature = "paired")]
{
refusals += 1;
let why = format!("{e:#}");
let again = paired::start_backoff(refusals);
paired::server_refused(&why);
log::error!("holger-server: the start is REFUSED ({why}) — this box stays up, says it in its pair status, and starts again in {} s (try {})", again.as_secs(), refusals + 1);
tokio::time::sleep(again).await;
}
}
Err(e) => return Err(e),
}
};
functional_status(
"holger-cli/start",
"server_started",
true,
"gRPC endpoints wired from config",
);
#[cfg(feature = "paired")]
paired::stage(
"serving",
if holger.role.is_twin() {
"a TWIN: the follow starts below"
} else {
"a primary"
},
);
#[cfg(feature = "paired")]
paired::server_serving();
// The one-way appliance serves; a HOLGERINBOX stick's signed bundles reach its inbox.
#[cfg(all(feature = "pid1", not(feature = "paired")))]
if is_pid1 {
appliance_pid1::fault::serving_now();
pid1::spawn_stick_inbox(
holger
.data_root()
.join(server_lib::import_door::BUNDLE_INBOX),
);
}
// THE BOX'S FEED BUS (A7 row 8): every value the installer feeds after the start
// (the front's offer: the fleet door, the monetize key) reaches its consumer live.
#[cfg(feature = "paired")]
let feeds: std::sync::Arc<dyn server_lib::late::Feeds> = match _paired.clone() {
Some(installed) => std::sync::Arc::new(paired::BoxFeeds(installed)),
None => std::sync::Arc::new(server_lib::late::NoFeeds),
};
#[cfg(not(feature = "paired"))]
let feeds: std::sync::Arc<dyn server_lib::late::Feeds> =
std::sync::Arc::new(server_lib::late::NoFeeds);
let _ = on_estate;
// THE ESTATE'S SITE (RA5-1): sign-up follows what a later feed declares.
holger.follow_estate_site(feeds.as_ref());
// THE LATE MONETIZE KEY AND PAY PAGE (metered): enforced the moment they are fed.
#[cfg(feature = "metered")]
server_lib::late::follow_monetize(
feeds.as_ref(),
holger.live_keys(),
holger
.monetize_pubkey
.as_deref()
.and_then(server_lib::holger_fleet::key32),
);
// THE PULL CHANNEL (metered): this box reports to the ONE monetize and pulls
// its signed orders as jera jobs; monetize never calls in (G1-pull, G16). It
// starts when a feed names the door and reads its role at every round.
#[cfg(feature = "metered")]
match server_lib::fleet_box::start_on(
std::sync::Arc::clone(&feeds),
holger.peers_root(),
holger.role.is_twin(),
holger.monetize_pubkey.clone(),
say_report,
) {
Ok(on) => functional_status(
"holger-cli/start",
"fleet_pull_channel",
true,
if on {
"on"
} else {
"off: no ONE monetize named yet"
},
),
Err(why) => log::error!("fleet: the pull channel is off: {why}"),
}
// THE OFF-SITE COPY (lane M5): a main fed `offsite_recipient` and a store ships
// its estate and artifacts there, sealed for the operator, as jera jobs; a later
// feed starts or changes it live, and a box elected twin copies nothing.
{
let repos = server_lib::offsite::repo_source(&holger);
let (configured_twin, root) = (holger.role.is_twin(), holger.peers_root());
let is_twin: std::sync::Arc<dyn Fn() -> bool + Send + Sync> =
std::sync::Arc::new(move || {
configured_twin
|| server_lib::twin::recorded_role(&root)
.ok()
.flatten()
.is_some_and(|r| r.role.is_twin())
});
match server_lib::offsite::start_on(
feeds.as_ref(),
holger.peers_root(),
repos,
is_twin,
) {
Ok(()) => functional_status(
"holger-cli/start",
"offsite_copy",
true,
"follows the feed's offsite_recipient",
),
Err(why) => eprintln!("offsite: the off-site copy is off: {why}"),
}
}
// THE MIRROR PLANE. Planned before it is spawned, so a pair whose
// local repository has no backend refuses the BOOT rather than
// logging once a cycle into a journal nobody is reading. No block
// declared ⇒ an empty plan ⇒ no task, which is where every config
// written before the key existed stays.
// A one-way build (no `fetch`) has no mirror plane at all: its boundary check
// refused a `mirror:` block before this point.
#[cfg(feature = "fetch")]
let data_root = std::env::var_os("HOLGER_DATA").map(PathBuf::from);
#[cfg(feature = "fetch")]
let plans = match server_lib::mirror_cycle::plan(&holger, data_root.as_deref()) {
Ok(p) => p,
Err(e) => {
#[cfg(feature = "paired")]
if on_estate {
stay_up_refused(e).await
}
return Err(e);
}
};
#[cfg(feature = "fetch")]
let planned = plans.len();
#[cfg(not(feature = "fetch"))]
let planned = 0usize;
// An asset that does not land goes to the job queue as its own
// `Job::MirrorAsset` (status, reason, retry) and the cycle moves on.
#[cfg(feature = "fetch")]
let _mirror_tasks =
server_lib::mirror_cycle::spawn_timers_handing_off(plans, holger.mirror_handoff());
// THE TWIN'S FOLLOW (owner plan 2b): on a twin, every repository mirrored
// from the primary and the estate pulled over holger.estate.v1. Nothing on
// a primary.
// A32-14, stays a task: the twin's follow; its handles are kept here for the server's life.
let _twin_tasks =
match server_lib::twin_follow::spawn(&holger).map_err(|e| anyhow::anyhow!("{e}")) {
Ok(t) => t,
Err(e) => {
#[cfg(feature = "paired")]
if on_estate {
stay_up_refused(e).await
}
return Err(e);
}
};
#[cfg(feature = "paired")]
if holger.role.is_twin() {
appliance_pid1::fault::reached("follow");
paired::stage(
"follow",
"the twin follows the primary: every repository mirrored, the estate pulled",
);
}
if planned > 0 {
functional_status(
"holger-cli/start",
"mirror_timers_spawned",
true,
&format!("{planned} upstream(s) on their own cadence"),
);
}
// The admin view's disk speed (edda disk-snapshot `speed`): once, on
// its own thread, on an appliance's PID 1 or with HOLGER_DISK_SPEED=1.
// Serving never waits for it; until it lands the console says N/A.
{
#[cfg(feature = "paired")]
let on_box = is_pid1;
#[cfg(not(feature = "paired"))]
let on_box = false;
if on_box || server_lib::admin_view::disk_speed_asked() {
let started = holger.start_disk_speed_test();
functional_status(
"holger-cli/start",
"disk_speed_test_spawned",
started,
if started {
"the start-up disk speed test runs on its own thread"
} else {
"the disk speed test did not start"
},
);
}
}
println!("Holger is running. Press Ctrl+C to stop.");
// A live update on trial (L20): this app serves now; it confirms its slot after a
// minute of it (jera `live.trial` jobs, L21), or korp-start rolls it back at the next start.
#[cfg(feature = "paired")]
if let Some(installed) = _paired.as_ref() {
paired::watch_trial(
installed,
paired::own_doors(
holger
.exposed_endpoints
.iter()
.map(|e| (e.ron_url.as_str(), e.ron_http_url.as_deref())),
),
);
}
// PID 1 has no default dispositions: SIGTERM is DISCARDED unless
// handled, and a cloud's soft stop is not a signal at all but the
// ACPI power button — appliance-pid1 hears both. And PID 1 must
// never RETURN (`Attempted to kill init!`, then `panic=10` reboots
// the box it was asked to stop, MEASURED on test2.holger.rs: the
// soft stop hung ~20 min): flush, stop the job pool, sync, power off.
#[cfg(feature = "paired")]
if is_pid1 {
let why = appliance_pid1::runtime::shutdown_request().await;
let did = holger.stop_for_power_off(std::time::Duration::from_secs(10));
// The box's own duties (A28 M18) queue nothing more from here.
log::info!(
"holger: {} duty(ies) stopped for the power-off",
paired::stop_duties()
);
// The power only on the appliance (lane G6): PID 1 in a container cannot
// `reboot(2)`, and a refused power-off parks the container instead of ending it.
if appliance_pid1::stop::after_clean_stop(appliance_pid1::stop::on_appliance())
== appliance_pid1::stop::AfterStop::PowerOff
{
println!("Holger stopped ({why}): {did}; the data disk quiesced, the stop kept on the ESP, then the power (or a re-exec into a staged live update).");
// A7 row 18: quiesce /data (bounded at 10 s), keep the stop on the ESP, then power off.
paired::power_off_cleanly(&format!("holger: stopped cleanly ({why}): {did}"));
}
holger.flush_admission();
println!("Holger stopped ({why}): {did}; not the appliance — the process exits.");
return Ok(());
}
// The one-way appliance's clean stop (lane HOL-CDS-5): the power button or SIGTERM,
// the job pool stopped, /data quiesced, the stop kept on the ESP, the power.
#[cfg(all(feature = "pid1", not(feature = "paired")))]
if is_pid1 {
let why = appliance_pid1::runtime::shutdown_request().await;
let did = holger.stop_for_power_off(std::time::Duration::from_secs(10));
if appliance_pid1::stop::after_clean_stop(appliance_pid1::stop::on_appliance())
== appliance_pid1::stop::AfterStop::PowerOff
{
println!("Holger stopped cleanly ({why}): {did}; the data disk quiesced, the stop kept on the ESP, then the power.");
pid1::power_off_cleanly(&format!(
"holger-downstream: stopped cleanly ({why}): {did}"
));
}
holger.flush_admission();
println!("Holger stopped ({why}): {did}; not the appliance — the process exits.");
return Ok(());
}
let (tx, rx) = std::sync::mpsc::channel();
ctrlc::set_handler(move || {
let _ = tx.send(());
})?;
rx.recv().expect("Failed to receive signal");
// The admission book's last minute, to the job ledger.
holger.flush_admission();
println!("Holger stopped.");
}
Commands::DevPair { data, grpc, http } => {
std::fs::create_dir_all(&data)?;
let cfg_path = data.join("dev-pair.ron");
std::fs::write(&cfg_path, dev_pair_ron(&data, &grpc, &http))?;
println!(
"Holger dev-pair: /cache (caching crates.io mirror) + /sparring on \
sparse+http://{http}/<repo>/index/ (config {})",
cfg_path.display()
);
let mut holger = read_ron_config(&cfg_path)?;
holger.instantiate_backends()?;
wire_holger(&mut holger)?;
holger.start()?;
functional_status(
"holger-cli/dev-pair",
"dev_pair_started",
true,
"cache + sparring mirror",
);
println!("Running. Press Ctrl+C to stop.");
let (tx, rx) = std::sync::mpsc::channel();
ctrlc::set_handler(move || {
let _ = tx.send(());
})?;
rx.recv().expect("Failed to receive signal");
println!("Holger stopped.");
}
#[cfg(feature = "systemd")]
Commands::Install { op } => match op {
InstallOp::Add(a) | InstallOp::Update(a) => {
let scope = Scope::resolve(a.scope.map(ScopeArg::to_engine));
let data = resolve_data(scope, &a)?;
// User scope creates its own data dir; system scope's home is
// created + chowned by the engine's ensure_system_user.
if scope == Scope::User {
std::fs::create_dir_all(&data)
.with_context(|| format!("create data dir {}", data.display()))?;
}
let exec = resolve_exec(scope, &a, /* stage */ true)?;
// The unit starts `start --config <data>/holger.ron`: lay it when
// missing (OX-HOLFLAV), before the engine chowns the data root.
if let Some(laid) = lay_service_config(&a, &data)? {
println!(" config : {} (the first start's: two local registries on loopback)", laid.display());
}
let spec = holger_service_spec(&a, &exec, &data);
install_systemd_scoped(&spec, scope)?;
functional_status(
"holger-cli/install",
"systemd_unit_installed",
true,
&format!("{scope:?} scope"),
);
if scope == Scope::User {
println!(
" start at boot : loginctl enable-linger {}",
whoami_or("$USER")
);
println!(" follow logs : journalctl --user -u holger -f");
} else {
println!(" follow logs : journalctl -u holger -f");
}
}
InstallOp::Remove(a) => {
let scope = Scope::resolve(a.scope.map(ScopeArg::to_engine));
let data = resolve_data(scope, &a)?;
let exec = resolve_exec(scope, &a, /* stage */ false)?;
remove_systemd_scoped(&holger_service_spec(&a, &exec, &data), scope)?;
if a.purge {
let report = purge_holger_data(scope, &data, &a.user, a.wipe_identity);
print_purge_report(scope, &data, a.wipe_identity, &report);
} else {
println!(
" kept : {} (archives + audit + config)",
data.display()
);
println!(
" (use --purge to erase the data too — the ssh key + token are kept \
either way)"
);
}
}
InstallOp::Status(a) => {
let scope = Scope::resolve(a.scope.map(ScopeArg::to_engine));
let data = resolve_data(scope, &a)?;
let exec = resolve_exec(scope, &a, /* stage */ false)?;
let active = status_systemd_scoped(&holger_service_spec(&a, &exec, &data), scope)?;
println!(
"holger.service: {}",
if active { "active" } else { "inactive" }
);
}
InstallOp::Render(a) => {
let scope = Scope::resolve(a.scope.map(ScopeArg::to_engine));
let data = resolve_data(scope, &a)?;
let exec = resolve_exec(scope, &a, /* stage */ false)?;
print!(
"{}",
systemd_unit_scoped(&holger_service_spec(&a, &exec, &data), scope)
);
}
},
#[cfg(feature = "systemd")]
Commands::Service { op } => {
// Every verb resolves the scope + data + exec the same way, builds the
// neutral `holger_spec()`, and delegates to the ship's `Lifecycle` — the
// one lifecycle expert. `install` stages the binary (system scope) +
// provisions the user-scope data dir first; the rest never stage.
let (a, stage) = match &op {
ServiceOp::Install(a) => (a, true),
ServiceOp::Start(a)
| ServiceOp::Stop(a)
| ServiceOp::Status(a)
| ServiceOp::Health(a)
| ServiceOp::Uninstall(a) => (a, false),
};
let scope = Scope::resolve(a.scope.map(ScopeArg::to_engine));
let data = resolve_data(scope, a)?;
if matches!(op, ServiceOp::Install(_)) && scope == Scope::User {
std::fs::create_dir_all(&data)
.with_context(|| format!("create data dir {}", data.display()))?;
}
let exec = resolve_exec(scope, a, stage)?;
if matches!(op, ServiceOp::Install(_)) {
if let Some(laid) = lay_service_config(a, &data)? {
println!(" config : {} (the first start's: two local registries on loopback)", laid.display());
}
}
let lc = Lifecycle::new(holger_spec(a, scope, &exec, &data));
match &op {
ServiceOp::Install(_) => {
lc.install()?;
functional_status(
"holger-cli/service",
"lifecycle_install",
true,
&format!("{scope:?} scope"),
);
println!("✓ holger.service installed + started via the Skidbladnir ship ({scope:?} scope)");
if scope == Scope::User {
println!(
" start at boot : loginctl enable-linger {}",
whoami_or("$USER")
);
println!(" follow logs : journalctl --user -u holger -f");
} else {
println!(" follow logs : journalctl -u holger -f");
}
}
ServiceOp::Start(_) => {
lc.start()?;
functional_status("holger-cli/service", "lifecycle_start", true, "started");
println!("✓ started {}", lc.unit());
}
ServiceOp::Stop(_) => {
lc.stop()?;
functional_status("holger-cli/service", "lifecycle_stop", true, "stopped");
println!("✓ stopped {}", lc.unit());
}
ServiceOp::Status(_) => {
let active = lc.status()?;
functional_status(
"holger-cli/service",
"lifecycle_status",
true,
if active { "active" } else { "inactive" },
);
println!(
"{}: {}",
lc.unit(),
if active { "active" } else { "inactive" }
);
}
ServiceOp::Health(_) => {
let healthy = lc.health()?;
functional_status(
"holger-cli/service",
"lifecycle_health",
healthy,
if healthy { "healthy" } else { "unhealthy" },
);
println!(
"{}: {}",
lc.unit(),
if healthy { "healthy" } else { "unhealthy" }
);
if !healthy {
std::process::exit(1);
}
}
ServiceOp::Uninstall(_) => {
lc.uninstall()?;
functional_status("holger-cli/service", "lifecycle_uninstall", true, "removed");
if a.purge {
// `--purge` is honoured here too — a flag the verb accepts but
// silently ignored would be the worst kind of surprise.
println!("✓ removed {}", lc.unit());
let report = purge_holger_data(scope, &data, &a.user, a.wipe_identity);
print_purge_report(scope, &data, a.wipe_identity, &report);
} else {
println!("✓ removed {} (service user + data kept)", lc.unit());
}
}
}
}
#[cfg(feature = "supply-chain")]
Commands::Scan(a) => run_scan(a)?,
Commands::Retention(a) => run_retention(a)?,
Commands::Search(a) => run_search(a)?,
#[cfg(feature = "supply-chain")]
Commands::Sbom(a) => run_sbom(a)?,
#[cfg(feature = "supply-chain")]
Commands::Properties(a) => run_properties(a)?,
Commands::Replicate(a) => run_replicate(a)?,
Commands::Estate(a) => run_estate(a)?,
Commands::Passkey(a) => passkey_cli::run(a)?,
#[cfg(feature = "auth-passkey")]
Commands::Token(a) => token_cli::run(a).await?,
#[cfg(feature = "fetch")]
Commands::Mirror(a) => run_mirror(a).await?,
#[cfg(feature = "supply-chain")]
Commands::Promote(a) => run_promote(a)?,
#[cfg(feature = "intake")]
Commands::Intake(a) => run_intake(a).await?,
Commands::Audit(a) => run_audit(a).await?,
#[cfg(all(feature = "secure_update", feature = "supply-chain"))]
Commands::Export(a) => airgap::run_export(a)?,
#[cfg(feature = "export-push")]
Commands::ExportPush(a) => airgap::run_export_push(a)?,
#[cfg(feature = "sync")]
Commands::Sync(a) => sync_cli::run(a)?,
Commands::Pair(a) => run_pair(a)?,
Commands::RepoKey(a) => repo_key_cli::run(a)?,
Commands::Recharge(a) => run_recharge(a).await?,
#[cfg(feature = "metered")]
Commands::FleetEnrol(a) => run_fleet_enrol(a)?,
#[cfg(feature = "bundle-verbs")]
Commands::Bundle(a) => secure_update::run_bundle(a)?,
#[cfg(feature = "bundle-verbs")]
Commands::Scrub(a) => secure_update::run_scrub(a)?,
#[cfg(feature = "bundle-verbs")]
Commands::Backup(a) => secure_update::run_backup(a)?,
#[cfg(feature = "secure_update")]
Commands::Intel(a) => intel_cli::run(a)?,
#[cfg(feature = "secure_update")]
Commands::ScannerSigs(a) => sigs_cli::run(a)?,
#[cfg(feature = "intake")]
Commands::Jobs { config } => run_jobs(&config)?,
#[cfg(feature = "secure_update")]
Commands::TimeToken(a) => time_cli::run(a)?,
#[cfg(feature = "secure_update")]
Commands::SigstoreTrust(a) => sigstore_cli::run(a)?,
}
Ok(())
}
/// Drive the `holger-server sbom {attach|fetch}` verb. Both actions derive the
/// hosted-SBOM store from the config (the same `<data_root>/holger-sboms` the
/// server, gRPC RPC, and HTTP door use), so a CLI attach is immediately servable
/// and a CLI fetch reads exactly what the server hosts. Read/write in-process —
/// no running server required.
#[cfg(feature = "supply-chain")]
fn run_sbom(a: SbomArgs) -> Result<()> {
let coord = match &a.action {
SbomAction::Attach(x) => &x.coord,
SbomAction::Fetch(x) => &x.coord,
};
let holger = read_ron_config(&coord.config)
.with_context(|| format!("load sbom config {}", coord.config.display()))?;
let store = holger.sbom_store();
let id = server_lib::ArtifactId {
namespace: coord.namespace.clone(),
name: coord.name.clone(),
version: coord.version.clone(),
};
let label = format!(
"{}/{}@{}",
id.namespace.as_deref().unwrap_or("-"),
id.name,
id.version,
);
match &a.action {
SbomAction::Attach(x) => {
let doc = std::fs::read(&x.file)
.with_context(|| format!("read SBOM document {}", x.file.display()))?;
if doc.is_empty() {
anyhow::bail!("refusing to attach an empty SBOM document");
}
let content_address = store
.attach(&coord.repo, &id, &doc)
.with_context(|| format!("attach SBOM to '{label}' in '{}'", coord.repo))?;
println!(
"✓ attached SBOM ({} bytes) to [{}] {label} — content {content_address}",
doc.len(),
coord.repo,
);
functional_status(
"holger-cli/sbom",
"sbom_attached",
true,
&format!("{} bytes → {label}", doc.len()),
);
}
SbomAction::Fetch(x) => {
let doc = store
.fetch(&coord.repo, &id)
.with_context(|| format!("fetch SBOM for '{label}' in '{}'", coord.repo))?;
match doc {
Some(bytes) => {
match &x.out {
Some(path) => {
std::fs::write(path, &bytes)
.with_context(|| format!("write {}", path.display()))?;
eprintln!(
"✓ fetched SBOM ({} bytes) for [{}] {label} → {}",
bytes.len(),
coord.repo,
path.display(),
);
}
None => {
use std::io::Write;
std::io::stdout().write_all(&bytes)?;
}
}
functional_status(
"holger-cli/sbom",
"sbom_fetched",
true,
&format!("{} bytes ← {label}", bytes.len()),
);
}
None => {
functional_status(
"holger-cli/sbom",
"sbom_fetched",
false,
&format!("no SBOM for {label}"),
);
anyhow::bail!("no SBOM hosted for '{label}' in '{}'", coord.repo);
}
}
}
}
Ok(())
}
/// Drive the `holger-server properties {set|get}` verb. Derives the property store
/// from the config (the same `<data_root>/holger-props` the gRPC `Search` axis and
/// the `GET /-/properties` door read), so a CLI-set property is immediately
/// searchable. Read/write in-process — no running server required.
#[cfg(feature = "supply-chain")]
fn run_properties(a: PropertiesArgs) -> Result<()> {
let coord = match &a.action {
PropertiesAction::Set(x) => &x.coord,
PropertiesAction::Get(x) => &x.coord,
};
let holger = read_ron_config(&coord.config)
.with_context(|| format!("load properties config {}", coord.config.display()))?;
let store = holger.property_store();
let id = server_lib::ArtifactId {
namespace: coord.namespace.clone(),
name: coord.name.clone(),
version: coord.version.clone(),
};
let label = format!(
"{}/{}@{}",
id.namespace.as_deref().unwrap_or("-"),
id.name,
id.version,
);
match &a.action {
PropertiesAction::Set(x) => {
let removing = x.value.is_empty();
let map = store
.set(&coord.repo, &id, &x.key, x.value.clone())
.with_context(|| {
format!("set property '{}' on '{label}' in '{}'", x.key, coord.repo)
})?;
if removing {
println!(
"✓ removed property '{}' from [{}] {label}",
x.key, coord.repo
);
} else {
println!(
"✓ set property '{}' = {:?} on [{}] {label}",
x.key, x.value, coord.repo,
);
}
functional_status(
"holger-cli/properties",
"property_set",
map.contains_key(&x.key) != removing,
&format!("{}={:?} @ {label}", x.key, x.value),
);
}
PropertiesAction::Get(_) => {
let map = store.get(&coord.repo, &id);
if map.is_empty() {
println!("(no properties on [{}] {label})", coord.repo);
} else {
println!("properties on [{}] {label}:", coord.repo);
for (k, values) in &map {
println!(" {k} = {}", values.join(", "));
}
}
functional_status(
"holger-cli/properties",
"property_get",
true,
&format!("{} key(s) ← {label}", map.len()),
);
}
}
Ok(())
}
/// Refuse a retention pin the SERVER does not reserve to admins.
///
/// `--protect-property <key>=<value>` tells retention "never reclaim an artifact
/// carrying this". The key is a flag on THIS process; the gate that stops an
/// ordinary Writer from setting that same property over the wire lives in the
/// server process and reads `reserved_properties` from the config. The two are
/// only connected if they name the same key.
///
/// When they do not, the pin is not a pin: any Writer marks anything permanent,
/// and a quota-bounded store silently stops reclaiming. That failure is invisible
/// — retention reports a successful run and simply keeps more than it should —
/// so it is refused here, at the one moment both halves are on screen, rather
/// than warned about in a log nobody reads.
///
/// A run with no `--protect-property` is unaffected.
fn refuse_an_unreserved_pin(
holger: &server_lib::Holger,
policy: &server_lib::retention::RetentionPolicy,
) -> Result<()> {
let Some((key, _)) = &policy.protect_property else {
return Ok(());
};
#[cfg(not(feature = "supply-chain"))]
{
let _ = holger;
anyhow::bail!(
"retention pin '{key}': this server is built without `supply-chain`, so it keeps no \
property store to read a pin from"
);
}
#[cfg(feature = "supply-chain")]
let reserved = holger.reserved_property_keys();
#[cfg(feature = "supply-chain")]
if reserved.iter().any(|k| k == key) {
return Ok(());
}
#[cfg(feature = "supply-chain")]
anyhow::bail!(
"retention pin '{key}' is not reserved to admins by this server's config, so any \
Writer can set it over the wire and pin an artifact against reclamation.\n\
\n\
Fix it in the RON config, then re-run:\n\
\n reserved_properties: Some([{}]),\n\
\n\
(currently reserved: {})",
reserved
.iter()
.chain(std::iter::once(key))
.map(|k| format!("{k:?}"))
.collect::<Vec<_>>()
.join(", "),
if reserved.is_empty() {
"nothing".to_string()
} else {
reserved.join(", ")
},
)
}
/// Drive the `holger-server retention` planner verb: load + wire the config, build
/// the policy from the flags, and print a KEEP/RECLAIM plan for one repo (or all).
/// Dry-run by construction — the planner computes the decision, it never deletes.
fn run_retention(a: RetentionArgs) -> Result<()> {
use server_lib::retention::RetentionPolicy;
let mut holger = read_ron_config(&a.config)
.with_context(|| format!("load retention config {}", a.config.display()))?;
holger.instantiate_backends()?;
wire_holger(&mut holger)?;
let policy = RetentionPolicy {
keep_last_n: a.keep_last_n,
max_age_secs: a.max_age_secs,
keep_min_per_name: a.keep_min_per_name,
max_total_bytes: a.max_total_bytes,
protect_latest_per_name: !a.no_protect_latest,
// `key=value` (or bare `key` = any value) → the property pin predicate.
protect_property: a
.protect_property
.as_ref()
.map(|p| match p.split_once('=') {
Some((k, v)) => (k.trim().to_string(), v.to_string()),
None => (p.trim().to_string(), String::new()),
}),
};
refuse_an_unreserved_pin(&holger, &policy)?;
// Age by the estate's clock (`server_lib::clock`), like every expiry here.
let now = i64::try_from(server_lib::clock::now_unix_secs()).unwrap_or(0);
let repos: Vec<String> = match &a.repo {
Some(r) => vec![r.clone()],
None => holger
.list_repositories()
.iter()
.map(|s| s.to_string())
.collect(),
};
if policy.is_noop() {
println!("retention: no bounds set — this is a keep-everything preview.");
}
// `--execute` promotes the verb from planner to executor: it carries the plan
// out (dropping superseded artifact bytes, content-id-verified) and historizes
// the GC to the audit warehouse. Without it the verb stays dry-run.
if a.execute {
return run_retention_execute(&holger, &policy, now, &repos, a.verbose);
}
println!("retention plan (DRY-RUN — deletes nothing) @ {now}");
let mut all_ok = true;
for repo in &repos {
match holger.plan_repo_retention(repo, &policy, now) {
Ok(plan) => {
println!(
" {repo}: {} names, keep {} / reclaim {} — kept {} bytes, reclaimable {} bytes",
plan.name_count,
plan.kept_count,
plan.delete_count,
plan.kept_bytes,
plan.reclaimed_bytes,
);
if a.verbose {
for d in &plan.decisions {
let verb = if d.delete { "RECLAIM" } else { "keep " };
let reasons: Vec<&str> = d.reasons.iter().map(|r| r.as_str()).collect();
let ns = d.record.id.namespace.as_deref().unwrap_or("-");
println!(
" {verb} {ns}/{}@{} ({} bytes) [{}]",
d.record.id.name,
d.record.id.version,
d.record.size_bytes,
reasons.join(","),
);
}
}
}
Err(e) => {
all_ok = false;
eprintln!(" {repo}: retention plan failed: {e:#}");
}
}
}
functional_status(
"holger-cli/retention",
"plan_computed",
all_ok,
&format!("{} repositories planned (dry-run)", repos.len()),
);
if !all_ok {
anyhow::bail!("one or more repositories failed their retention plan");
}
Ok(())
}
/// The `--execute` arm of `run_retention`: plan then carry it out per repo,
/// reclaiming the superseded artifact bytes and historizing each GC to the audit
/// warehouse (the same append-only Arrow sink the running server writes).
fn run_retention_execute(
holger: &server_lib::Holger,
policy: &server_lib::retention::RetentionPolicy,
now: i64,
repos: &[String],
verbose: bool,
) -> Result<()> {
use server_lib::retention::GcOptions;
// The GC record lands in the very warehouse the server uses.
let audit = holger.audit_log()?;
let opts = GcOptions::execute();
println!("retention EXECUTE (reclaiming superseded generations) @ {now}");
let mut all_ok = true;
let mut total_deleted = 0usize;
let mut total_bytes = 0u64;
for repo in repos {
match holger.run_repo_retention(repo, policy, now, &opts, audit.as_ref(), None) {
Ok(report) => {
total_deleted += report.deleted_count;
total_bytes += report.reclaimed_bytes;
println!(
" {repo}: reclaimed {} artifacts ({} bytes), skipped {}, failed {}",
report.deleted_count,
report.reclaimed_bytes,
report.skipped_count,
report.failed_count,
);
if report.failed_count > 0 {
all_ok = false;
}
if verbose {
for act in &report.actions {
let ns = act.id.namespace.as_deref().unwrap_or("-");
println!(
" {:<12} {ns}/{}@{} ({} bytes)",
act.outcome.as_str(),
act.id.name,
act.id.version,
act.size_bytes,
);
}
}
}
Err(e) => {
all_ok = false;
eprintln!(" {repo}: retention execute failed: {e:#}");
}
}
}
functional_status(
"holger-cli/retention",
"gc_executed",
all_ok,
&format!(
"{total_deleted} artifacts ({total_bytes} bytes) reclaimed across {} repositories",
repos.len()
),
);
if !all_ok {
anyhow::bail!("one or more repositories failed to execute retention");
}
Ok(())
}
/// Drive the `holger-server search` verb: load + wire the config, build the
/// query from the flags, run the cross-repo search engine, and print the hits.
/// Read-only by construction (the same `search_repos` engine that backs the gRPC
/// RPC + the HTTP door). Exercises `Holger::search_all`.
fn run_search(a: SearchArgs) -> Result<()> {
use server_lib::search::SearchQuery;
let mut holger = read_ron_config(&a.config)
.with_context(|| format!("load search config {}", a.config.display()))?;
holger.instantiate_backends()?;
wire_holger(&mut holger)?;
let query = SearchQuery {
name: a.name,
namespace: a.namespace,
version: a.version,
checksum: a.checksum,
path: a.path,
// `key=value` (or bare `key` = any value) → (key, value) filters.
properties: a
.properties
.iter()
.filter_map(|p| {
let (k, v) = match p.split_once('=') {
Some((k, v)) => (k.trim().to_string(), v.to_string()),
None => (p.trim().to_string(), String::new()),
};
if k.is_empty() {
None
} else {
Some((k, v))
}
})
.collect(),
repositories: a.repos,
limit: a.limit,
};
let results = holger.search_all(&query);
println!(
"search: {} artifact hit(s), {} path hit(s){}",
results.artifacts.len(),
results.paths.len(),
if results.truncated {
" (truncated — raise --limit)"
} else {
""
},
);
for h in &results.artifacts {
let ns = h.id.namespace.as_deref().unwrap_or("-");
let sum = h
.checksum
.as_deref()
.map(|c| format!(" sha256:{c}"))
.unwrap_or_default();
println!(
" [{}] {ns}/{}@{} ({} bytes){sum}",
h.repository, h.id.name, h.id.version, h.size_bytes,
);
}
for p in &results.paths {
println!(" [{}] path: {}", p.repository, p.path);
}
let hits = results.artifacts.len() + results.paths.len();
functional_status(
"holger-cli/search",
"search_ran",
true,
&format!(
"{hits} hits across {} repositories",
holger.list_repositories().len()
),
);
Ok(())
}
/// Drive the `holger-server replicate --from --to` verb: load two RON configs as
/// separate in-process holger stores, replicate source→dest (content-id
/// idempotent), and print the report. The same `Holger::replicate_into` engine
/// backs the `ReplicationService.Replicate` gRPC RPC. Read-only over the source;
/// dry-run unless `--execute`. Two file-backed stores ⇒ laptop-testable, no
/// network.
/// Drive `holger-server mirror` — ONE cycle over every declared pair.
///
/// Exits non-zero when any pair failed outright (the upstream was unreachable,
/// the listing refused) or when any artifact within a pair failed. A mirror that
/// silently exited 0 having taken nothing is the failure mode the whole lane
/// exists to close.
/// **One start of holger-server, after the pairing listener serves** (lane D10): the boot's
/// waits for the installer's feed, the backends, the wiring, the recharge and the doors. An
/// estate box whose start is refused stays up, says the refusal in its pair status and
/// starts again (`main`); everywhere else the refusal ends the process as before.
async fn boot_server(
mut holger: server_lib::Holger,
on_estate: bool,
) -> Result<server_lib::Holger> {
let _ = on_estate;
// The console's token file the RON declares is written by the installer's
// feed: on the first boot, wait for it rather than start a login that
// authenticates nobody.
#[cfg(feature = "paired")]
if on_estate {
paired::server_stage("the boot reads the installer's feed");
paired::await_console_token(
holger.auth.as_ref(),
paired::boot_wait_budget(std::time::Duration::from_secs(3600)),
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
// An estate box is WireGuard-only (lane E1): its estate doors bind on wg0,
// so the tunnel is waited for before they open.
paired::await_estate_tunnel(paired::boot_wait_budget(std::time::Duration::from_secs(
3600,
)))
.map_err(|e| anyhow::anyhow!("{e}"))?;
paired::stage("tunnel", "wg0 answers: the estate doors open there");
// holger's passkey plane, bound to the console host this estate's
// installer fed (the people are this box's: holger-ui `people`).
paired::fed_passkey(&mut holger.auth);
// A sold estate's sign-up page leads to the site it was bought at (A16, C19).
paired::fed_provider();
// The shared medium's `{estate}` (the repository's base URL), from the
// name the installer fed (lane W2).
holger.resolve_estate(paired::fed_estate().as_deref());
// ... and its own public repositories (NIGHT-PUBREPO: the ONE box feeds each estate's
// list; the medium makes none public). A list the ONE box fed again after the lay (a
// `refeed`) goes over the lay's. A name the box cannot make public is refused by name
// and stays private; it never refuses the start (RA5-19).
// Each refusal is in the boot log by name.
let _refused = holger.resolve_public_repositories_at_boot(
&paired::fed_public_repositories_at(&holger.peers_root()),
);
// ... and its separation of duties: the console's admins approve, invited
// Importers upload (lane W7).
paired::estate_duties(&mut holger.auth);
// The shop's first-login invite (lane P1): the buyer's mail link enrols the estate's
// first admin, once (taken in when the passkey store opens).
#[cfg(feature = "auth-passkey")]
if let Some((sha256_hex, ttl_secs, display)) = paired::fed_first_login_kept() {
holger.first_admin_invite = Some(server_lib::passkey::FirstAdminInvite {
sha256_hex,
ttl_secs,
display,
});
}
// ... and its site, as the ONE box declared the estate (RA5-1): a sold private Holger is
// invite only from its first start, whatever its host is called.
#[cfg(feature = "auth-passkey")]
{
holger.estate_site = paired::fed_site_at(&holger.peers_root());
}
// A dead main re-admitted into its estate is fed where its twin is.
if holger
.recharge_from
.as_deref()
.is_none_or(|r| r.trim().is_empty())
{
holger.recharge_from = paired::fed_recharge_from();
}
}
// A metered box of an estate: the front's monetize key arrives in the feed. The kept
// feed's key is enforced at once; with none kept yet, the paid-state store waits for the
// front's offer and takes it live (A7 row 8: no 600 s wait, no restart).
#[cfg(all(feature = "paired", feature = "metered"))]
if on_estate
&& holger
.monetize_pubkey
.as_deref()
.is_none_or(|k| k.trim().is_empty())
{
holger.monetize_pubkey = paired::fed_monetize_key_kept();
if holger.monetize_pubkey.is_none() {
holger.monetize_key_awaited = true;
log::warn!("monetize: the installer's feed names no monetize key yet: the paid limit is enforced the moment the front offers it (no restart)");
}
}
// ... and the estate's pay page a paid refusal names (lane M4).
#[cfg(all(feature = "paired", feature = "metered"))]
if on_estate
&& holger
.pay_page
.as_deref()
.is_none_or(|p| p.trim().is_empty())
{
holger.pay_page = paired::fed_pay_page_kept();
}
// THE SCAN SANDBOX FROM THE MEDIUM (lane HOLGER-MEDIA): crun, the worker's rootfs and
// ClamAV's, laid from the running slot's archive and checked by sha256, then clamd in
// its sandbox, before the intake chain (`rules: (sandbox: required)`) opens. A medium
// that carries none leaves every scan refused by name (`holger-sandbox-1`), as before.
// A paired downstream (no scanners) verifies its bundles in jobs too (lane HOL-CDS-5).
#[cfg(feature = "paired")]
{
paired::server_stage("the scan sandbox is laid from the medium");
let root = holger.data_root();
match tokio::task::spawn_blocking(move || sandbox_boot::boot(&root)).await.map_err(|e| anyhow::anyhow!("the sandbox boot task: {e}"))? {
Ok(Some(laid)) => println!("scan sandbox: {}", laid.words()),
Ok(None) => println!("scan sandbox: no medium carries one here (HOLGER_SANDBOX_MEDIA_DIR or a korp-start boot lays it)"),
Err(e) => {
// A refusal, by name: the box serves, the intake refuses every scan until a
// release that lays a sandbox arrives (a keep medium, a live update).
log::error!("scan sandbox: {e}");
println!("scan sandbox: REFUSED {e}");
}
}
// HOL-OCI-EVIDENCE: the release's Sigstore trust root and its policy, from the same medium,
// before the intake chain opens (the estate RON's floor names the seeded slot).
let root = holger.data_root();
match tokio::task::spawn_blocking(move || sandbox_boot::seed_trust(&root)).await.map_err(|e| anyhow::anyhow!("the trust seed task: {e}"))? {
Ok(Some(s)) => println!("sigstore trust root: seq {} {}", s.sequence, if s.kept { "kept (an imported root is as new or newer)" } else { "seeded from the medium" }),
Ok(None) => println!("sigstore trust root: this medium carries none; keyless signatures verify against nothing until a signed bundle is imported"),
Err(e) => {
log::error!("sigstore trust root: {e}");
println!("sigstore trust root: REFUSED {e}");
}
}
}
// The one-way appliance (lane HOL-CDS-5): every bundle is verified in a job, so crun and
// the worker are laid from the medium before the intake chain opens, as on the main.
#[cfg(all(feature = "pid1", not(feature = "paired")))]
if appliance_pid1::net::is_init() {
let root = holger.data_root();
match tokio::task::spawn_blocking(move || sandbox_boot::boot(&root))
.await
.map_err(|e| anyhow::anyhow!("the sandbox boot task: {e}"))?
{
Ok(Some(laid)) => println!("job sandbox: {}", laid.words()),
Ok(None) => {
println!("job sandbox: no medium carries one here; every import is refused by name")
}
Err(e) => {
log::error!("job sandbox: {e}");
println!("job sandbox: REFUSED {e}");
}
}
}
#[cfg(feature = "paired")]
paired::server_stage("the backends open");
holger.instantiate_backends()?;
wire_holger(&mut holger)?;
// RECHARGE (owner plan 2b): a new main named `recharge_from` takes the
// twin's estate and every repository ONCE, before it serves. A partial
// recharge refuses the boot: half a box served as whole is the worse fault.
#[cfg(feature = "paired")]
paired::server_stage("the recharge from the twin, if one is named");
if let Some(r) = server_lib::twin_follow::recharge(&holger)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?
{
println!("recharge: {}", r.estate);
for a in &r.artifacts {
println!("recharge: {a}");
}
if !r.failed.is_empty() {
anyhow::bail!(
"recharge from {:?} is incomplete — this box will not serve half of itself: {}",
holger.recharge_from,
r.failed.join("; ")
);
}
}
#[cfg(feature = "paired")]
paired::server_stage("the doors start");
holger.start()?;
Ok(holger)
}
/// **What the pull channel does, in the boot log** (a STAGE line on an estate box).
#[cfg(feature = "metered")]
fn say_report(words: &str) {
// At WARN (lane R10): an estate box logs at `warn` by default, and at info this line was never seen.
#[cfg(feature = "paired")]
log::warn!("{}", dev_narrate::stage_line("report", words));
#[cfg(not(feature = "paired"))]
let _ = words;
}
/// **An estate box refused after it served stays up** (lane D10): PID 1 must never
/// return, and the refusal is said in the pair status the installer reads.
#[cfg(feature = "paired")]
async fn stay_up_refused(e: anyhow::Error) -> ! {
let why = format!("{e:#}");
paired::server_refused(&why);
log::error!("holger-server: REFUSED after its doors opened ({why}) — this box stays up and says it in its pair status");
loop {
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
}
}
#[cfg(feature = "fetch")]
async fn run_mirror(a: MirrorArgs) -> Result<()> {
let mut holger = read_ron_config(&a.config)
.with_context(|| format!("load mirror config {}", a.config.display()))?;
holger.instantiate_backends()?;
wire_holger(&mut holger)?;
let data_root = std::env::var_os("HOLGER_DATA").map(PathBuf::from);
let reports = server_lib::mirror_cycle::run_once(
&holger,
data_root.as_deref(),
a.source.as_deref(),
a.repo.as_deref(),
)
.await?;
if reports.is_empty() {
anyhow::bail!(
"mirror: the config declares a `mirror:` block but no pair matched \
--source/--repo — nothing ran, and reporting success for a cycle that \
did not happen would be the lie this verb exists to avoid"
);
}
let mut bad = 0usize;
for r in &reports {
match r {
Ok(report) => {
if a.json {
println!("{}", serde_json::to_string(report)?);
} else {
println!("{}", report.line());
}
if report.failed > 0 {
bad += 1;
}
}
Err(e) => {
bad += 1;
eprintln!("mirror: {e:#}");
}
}
}
functional_status(
"holger-cli/mirror",
"mirror_cycle_ran",
bad == 0,
&format!("{} pair(s), {bad} with failures", reports.len()),
);
if bad > 0 {
anyhow::bail!(
"mirror: {bad} of {} pair(s) did not complete",
reports.len()
);
}
Ok(())
}
fn run_replicate(a: ReplicateArgs) -> Result<()> {
use server_lib::replication::ReplicationOptions;
let mut source = read_ron_config(&a.from)
.with_context(|| format!("load replicate --from config {}", a.from.display()))?;
source.instantiate_backends()?;
wire_holger(&mut source)?;
let mut dest = read_ron_config(&a.to)
.with_context(|| format!("load replicate --to config {}", a.to.display()))?;
dest.instantiate_backends()?;
wire_holger(&mut dest)?;
let opts = ReplicationOptions {
execute: a.execute,
limit: a.limit,
};
let report = source.replicate_into(&dest, a.repo.as_deref(), &opts);
println!(
"replicate {} → {} [{}]: examined {}, copied {}, skipped {}, would-copy {}, failed {}{}",
a.from.display(),
a.to.display(),
if a.execute { "execute" } else { "dry-run" },
report.examined(),
report.copied(),
report.skipped(),
report.would_copy(),
report.failed(),
if report.truncated {
" (truncated — raise --limit)"
} else {
""
},
);
for item in &report.items {
use server_lib::replication::Outcome;
let tag = match item.outcome {
Outcome::Copied => "COPIED",
Outcome::Skipped => "skip",
Outcome::WouldCopy => "would",
Outcome::Failed => "FAIL",
};
let ns = item.id.namespace.as_deref().unwrap_or("-");
let detail = if item.detail.is_empty() {
String::new()
} else {
format!(" — {}", item.detail)
};
println!(
" [{tag}] {}→{} {ns}/{}@{}{detail}",
item.source_repo, item.dest_repo, item.id.name, item.id.version,
);
}
functional_status(
"holger-cli/replicate",
"replication_ran",
report.is_complete(),
&format!(
"copied {} skipped {} failed {} (execute={})",
report.copied(),
report.skipped(),
report.failed(),
a.execute,
),
);
if !report.is_complete() {
anyhow::bail!("replication had {} failure(s)", report.failed());
}
Ok(())
}
/// **PAIR TWO HOLGERS** — the local verb, run once on each side.
///
/// Upstream (the cloud box that serves the mirror):
/// ```text
/// holger-server pair downstream --config /etc/holger.ron --name onprem-1 \
/// --cert /etc/holger/peers/onprem-1.crt --repos prod
/// ```
/// Downstream (the on-prem box that mirrors):
/// ```text
/// holger-server pair upstream --config /etc/holger.ron --source cloud \
/// --name holger-rs --cert /etc/holger/peers/cloud-server.crt
/// ```
///
/// Local, like `promote`: gated by filesystem permissions, `paired_by` is the
/// OS user, and the record takes effect at the next start. See
/// `server/lib/src/peers.rs`.
#[derive(Parser, Debug)]
struct PairArgs {
#[command(subcommand)]
op: PairOp,
}
#[derive(Subcommand, Debug)]
enum PairOp {
/// On the UPSTREAM: record a downstream holger and the repositories it may pull.
Downstream {
/// The RON config whose data root the record is written under.
#[arg(long)]
config: PathBuf,
/// The downstream's name, as the flow picture will draw it.
#[arg(long)]
name: String,
/// PEM of the CLIENT certificate the downstream's mirror presents.
#[arg(long)]
cert: PathBuf,
/// Repositories on this box it may pull. Repeat, or comma-separate.
#[arg(long, value_delimiter = ',')]
repos: Vec<String>,
},
/// On the DOWNSTREAM: record the upstream holger a mirror source pulls from.
Upstream {
/// The RON config whose data root the record is written under.
#[arg(long)]
config: PathBuf,
/// The `mirror.ron_sources[].ron_name` that pulls from it.
#[arg(long)]
source: String,
/// The upstream's name, as the flow picture will draw it.
#[arg(long)]
name: String,
/// PEM of the upstream's TLS SERVER certificate.
#[arg(long)]
cert: PathBuf,
},
/// RE-PAIR a known peer with its new certificate (a lost key, a rolled certificate):
/// `--downstream <name>` on the upstream, or `--source <mirror source>` on the
/// downstream. The name and what it may pull are kept; the old key is refused from the
/// next start. A peer that was never paired is refused by name (`holger-peer-5`).
Repair {
/// The RON config whose data root the record is under.
#[arg(long)]
config: PathBuf,
/// On the UPSTREAM: the paired downstream's name.
#[arg(long, conflicts_with = "source", required_unless_present = "source")]
downstream: Option<String>,
/// On the DOWNSTREAM: the mirror source whose upstream is re-pinned.
#[arg(long)]
source: Option<String>,
/// PEM of the peer's NEW certificate.
#[arg(long)]
cert: PathBuf,
},
/// List the pairings this box has.
List {
/// The RON config whose data root is read.
#[arg(long)]
config: PathBuf,
},
}
fn run_pair(a: PairArgs) -> anyhow::Result<()> {
let (config, action) = match &a.op {
PairOp::Downstream { config, .. } => (config.clone(), "downstream"),
PairOp::Upstream { config, .. } => (config.clone(), "upstream"),
PairOp::Repair { config, .. } => (config.clone(), "repair"),
PairOp::List { config } => (config.clone(), "list"),
};
let holger = read_ron_config(&config)?;
let root = holger.peers_root();
let by = promoting_os_user();
let now = server_lib::audit::now_nanos() / 1_000_000_000;
match a.op {
PairOp::Downstream {
name, cert, repos, ..
} => {
let pem = std::fs::read(&cert)
.with_context(|| format!("reading the certificate {}", cert.display()))?;
match server_lib::peers::pair_downstream(&root, &name, &pem, &repos, &by, now as u64) {
Ok(rec) => {
println!(
" PAIRED downstream holger '{}' key {} for [{}] — takes effect at the next start",
rec.name,
server_lib::peers::short(&rec.fingerprint),
rec.repos.join(", ")
);
functional_status("holger-cli/pair", "peer_paired", true, &rec.name);
Ok(())
}
Err(msg) => {
println!(" REFUSED: {msg}");
functional_status("holger-cli/pair", "peer_paired", false, &msg);
anyhow::bail!(msg)
}
}
}
PairOp::Upstream {
source, name, cert, ..
} => {
let pem = std::fs::read(&cert)
.with_context(|| format!("reading the certificate {}", cert.display()))?;
match server_lib::peers::pair_upstream(&root, &source, &name, &pem, &by, now as u64) {
Ok(rec) => {
println!(
" PAIRED upstream holger '{}' key {} on mirror source '{}' — takes effect at the next start",
rec.name,
server_lib::peers::short(&rec.fingerprint),
rec.source
);
functional_status("holger-cli/pair", "peer_paired", true, &rec.name);
Ok(())
}
Err(msg) => {
println!(" REFUSED: {msg}");
functional_status("holger-cli/pair", "peer_paired", false, &msg);
anyhow::bail!(msg)
}
}
}
PairOp::Repair {
downstream,
source,
cert,
..
} => {
let pem = std::fs::read(&cert)
.with_context(|| format!("reading the certificate {}", cert.display()))?;
let done = match (downstream, source) {
(Some(name), _) => server_lib::peers::repair_downstream(&root, &name, &pem, &by, now as u64).map(|(new, old)| {
format!(
"RE-PAIRED downstream holger '{}': key {} replaces {} for [{}] — takes effect at the next start",
new.name,
server_lib::peers::short(&new.fingerprint),
server_lib::peers::short(&old.fingerprint),
new.repos.join(", ")
)
}),
(None, Some(source)) => server_lib::peers::repair_upstream(&root, &source, &pem, &by, now as u64).map(|(new, old)| {
format!(
"RE-PAIRED upstream holger '{}' on mirror source '{}': key {} replaces {} — takes effect at the next start",
new.name,
new.source,
server_lib::peers::short(&new.fingerprint),
server_lib::peers::short(&old.fingerprint)
)
}),
(None, None) => Err("a re-pair names --downstream <name> or --source <mirror source>".to_string()),
};
match done {
Ok(words) => {
println!(" {words}");
functional_status("holger-cli/pair", "peer_repaired", true, action);
Ok(())
}
Err(msg) => {
println!(" REFUSED: {msg}");
functional_status("holger-cli/pair", "peer_repaired", false, &msg);
anyhow::bail!(msg)
}
}
}
PairOp::List { .. } => {
let (down, up) = server_lib::peers::load(&root).map_err(|e| anyhow::anyhow!(e))?;
println!(" pairings under {}", root.display());
if down.is_empty() && up.is_empty() {
println!(" (none)");
}
for d in &down {
println!(
" downstream {} key {} may pull [{}] (paired {} by {})",
d.name,
server_lib::peers::short(&d.fingerprint),
d.repos.join(", "),
d.paired_at,
d.paired_by
);
}
for u in &up {
println!(
" upstream {} key {} on source '{}' (paired {} by {})",
u.name,
server_lib::peers::short(&u.fingerprint),
u.source,
u.paired_at,
u.paired_by
);
}
functional_status("holger-cli/pair", "peers_listed", true, action);
Ok(())
}
}
}
/// `holger-server promote` — seal ONE artifact DEV → PROD, in-process.
///
/// The LOCAL twin of the `PromotionService.PromoteArtifact` RPC
/// (`server/lib/src/grpc/services.rs`), in the exact style of [`run_replicate`]:
/// read two RON configs, instantiate their backends, and do the work here — no
/// network, no gRPC client. The steps mirror the handler so behaviour matches:
/// fetch from the source repo → policy gate → property requirement → `seal`.
///
/// **Dry-run by default.** Without `--execute` the gates still run and the verdict
/// is printed, but nothing is written; a promotion is irreversible, so the preview
/// is the default. **Not access-controlled** — see the note on [`PromoteArgs`].
#[cfg(feature = "supply-chain")]
fn run_promote(a: PromoteArgs) -> Result<()> {
let mut source = read_ron_config(&a.from)
.with_context(|| format!("load promote --from config {}", a.from.display()))?;
source.instantiate_backends()?;
wire_holger(&mut source)?;
let mut dest = read_ron_config(&a.to)
.with_context(|| format!("load promote --to config {}", a.to.display()))?;
dest.instantiate_backends()?;
wire_holger(&mut dest)?;
let to_repo = a.to_repo.clone().unwrap_or_else(|| a.repo.clone());
let id = server_lib::ArtifactId {
namespace: a.namespace.clone().filter(|s| !s.is_empty()),
name: a.name.clone(),
version: a.version.clone(),
};
let label = match &id.namespace {
Some(ns) => format!("{ns}/{}@{}", id.name, id.version),
None => format!("{}@{}", id.name, id.version),
};
// The intake chain's repositories are moved only by its transitions: a local
// promotion naming one would seal past every state before SEALED, exactly as
// the gRPC `PromoteArtifact` refusal says.
if let Some(intake) = dest.promotion.as_ref().and_then(|p| p.intake.as_ref()) {
if let Some(repo) = [a.repo.as_str(), to_repo.as_str()]
.into_iter()
.find(|r| intake.repos().contains(r))
{
let refusal = server_lib::promote::intake::IntakeError::DoorRefused {
repo: repo.to_string(),
why: "a promotion into or out of the intake chain is the SEALED transition, not a \
promote"
.to_string(),
};
anyhow::bail!("{}", refusal.tagged());
}
}
// The PROD store + gates come from the DESTINATION config — the one derivation
// `serve` uses, so the local path cannot promote under weaker policy.
// `promotion_engine()` is fail-CLOSED: a configured-but-unreadable advisory
// feed is an Err here, and we refuse rather than seal unscanned.
let engine = dest
.promotion_engine()
.context("build the promotion engine from the --to config (gates fail closed)")?;
// ★ **A CALLER WHO ASKED FOR A GATE AND WOULD GET NONE IS REFUSED, NOT
// SERVED.**
//
// The policy gate moved out of the environment and into the RON
// `promotion:` block on 2026-09-20 (`Holger::promotion_engine`), because an
// appliance with no shell cannot set an environment variable. What was left
// behind was a silent hole: a caller still spelling it the old way —
// `HOLGER_PROMOTE_ENFORCE=block` with an advisory feed — got an engine with
// NO gate, and the promotion sealed. MEASURED on main `1138143`:
// `a_policy_blocked_promotion_refuses_with_the_typed_code_and_seals_nothing`
// printed `SEALED`.
//
// Unreachable configuration that reads as enforcement is worse than no
// enforcement, so this refuses BEFORE the source bytes are even read, with
// the frozen `holger-promote-2` and the sentence that says where the keys
// live now.
const GATE_ENV_KEYS: [&str; 3] = [
"HOLGER_PROMOTE_ENFORCE",
"HOLGER_PROMOTE_ADVISORY_DB",
"HOLGER_PROMOTE_DENY_LICENSES",
];
let asked_by_env: Vec<&str> = GATE_ENV_KEYS
.into_iter()
.filter(|k| {
std::env::var(k)
.map(|v| !v.trim().is_empty())
.unwrap_or(false)
})
.collect();
if !asked_by_env.is_empty() && !engine.has_gate() {
let msg = server_lib::errcode::PROMOTE_POLICY_BLOCKED.tag(format_args!(
"{} set in the environment, but the destination config declares no gated rung, so \
this promotion would run UNSCANNED. The gate keys moved into the RON `promotion:` \
block (`advisory_db`, `deny_licenses`, and a stage with `gated: true` / \
`gates: [\"osv\"]`); an appliance has no shell to set an environment variable in. \
Refusing rather than sealing unscanned.",
asked_by_env.join(", ")
));
println!(" REFUSED: {msg}");
functional_status("holger-cli/promote", "artifact_promoted", false, &msg);
anyhow::bail!(msg);
}
// Resolve the SOURCE backend and read the bytes to promote.
let backend = source
.get_repo(&a.repo)
.with_context(|| format!("resolve source repository '{}'", a.repo))?;
let data = match backend.fetch(&id) {
Ok(Some(d)) => d,
Ok(None) => {
let e = server_lib::errcode::PROMOTE_SOURCE_NOT_FOUND.tag(format_args!(
"artifact '{label}' not found in source repo '{}'",
a.repo
));
functional_status("holger-cli/promote", "artifact_promoted", false, &e);
anyhow::bail!(e);
}
Err(e) => {
let msg = server_lib::errcode::PROMOTE_IO.tag(format_args!(
"reading '{label}' from source repo '{}': {e}",
a.repo
));
functional_status("holger-cli/promote", "artifact_promoted", false, &msg);
anyhow::bail!(msg);
}
};
println!(
"promote {label} ({} bytes) [{}]\n from repo '{}' in {}\n into repo '{}' in {}",
data.len(),
if a.execute { "execute" } else { "dry-run" },
a.repo,
a.from.display(),
to_repo,
a.to.display(),
);
// ── Policy gate (design §4) — the REAL CVE/license/provenance scan, run at
// the boundary BEFORE sealing, exactly as the RPC does. Skipping it locally
// would make this verb a way around the policy the served path enforces.
if engine.has_gate() {
let ecosystem = server_lib::security::ecosystem_for_format(backend.format());
// The gate sees the DIGEST and the BYTES. `data` was read above, so
// there was never a reason to hand the scanner a bare coordinate — and
// a bare coordinate is what made modgunn's verdict cache a single
// bucket for every artifact holger ever promoted.
let digest = server_lib::attest::ArtifactDigest::of_sha256(&data);
let attestations: Vec<server_lib::attest::Statement> = Vec::new();
let set = [server_lib::promote::PromotedArtifact {
id: &id,
ecosystem: &ecosystem,
digest,
bytes: &data,
attestations: &attestations,
}];
match engine.gate_check_hop(&a.repo, &to_repo, &set, "cli") {
Ok(decision) => println!(" promotion gates: {}", decision.summary()),
Err(e) => {
let msg = e.tagged();
println!(" BLOCKED by the promotion gates ({ecosystem}): {msg}");
if let server_lib::promote::PromoteError::PolicyBlocked(reasons) = &e {
for r in reasons {
println!(" · {r}");
}
}
functional_status("holger-cli/promote", "artifact_promoted", false, &msg);
anyhow::bail!(msg);
}
}
}
// ── Property requirement — only promote when the SOURCE coordinate carries
// the required custom property. Fail-closed, mirroring the handler.
if let Some((key, value)) = engine.required_property() {
let props = source.property_store().get(&a.repo, &id);
if !engine.property_requirement_met(&props) {
let msg = server_lib::errcode::PROMOTE_POLICY_BLOCKED.tag(format_args!(
"promotion requires property '{key}{}' on the source artifact",
if value.is_empty() {
String::new()
} else {
format!("={value}")
}
));
println!(" BLOCKED by property requirement: {msg}");
functional_status("holger-cli/promote", "artifact_promoted", false, &msg);
anyhow::bail!(msg);
}
println!(" property requirement: PASS ('{key}')");
}
// Immutability preview: report the refusal in dry-run rather than at --execute.
if engine.is_promoted(&to_repo, &id) {
let msg = server_lib::promote::PromoteError::AlreadyPromoted.tagged();
println!(" REFUSED: {msg}");
functional_status("holger-cli/promote", "artifact_promoted", false, &msg);
anyhow::bail!(msg);
}
if !a.execute {
println!(" would seal into '{to_repo}' (dry-run — nothing written; pass --execute)");
functional_status(
"holger-cli/promote",
"artifact_promoted",
true,
"dry-run — gates passed, nothing written",
);
return Ok(());
}
// `promoted_by`: no auth gate on the local path, so record the HONEST identity —
// the OS user (pure std, no shell). Never "anonymous", never a faked admin.
let promoted_by = promoting_os_user();
let promoted_at = server_lib::audit::now_nanos() / 1_000_000_000;
match engine.seal(&a.repo, &to_repo, &id, &data, &promoted_by, promoted_at) {
Ok(record) => {
println!(
" SEALED {label} into '{to_repo}' — content {} ({} bytes), by {} at {}",
record.content_address, record.size, record.promoted_by, record.promoted_at,
);
functional_status(
"holger-cli/promote",
"artifact_promoted",
true,
&format!("{label} → {to_repo} content {}", record.content_address),
);
Ok(())
}
Err(e) => {
let msg = e.tagged();
println!(" FAILED: {msg}");
functional_status("holger-cli/promote", "artifact_promoted", false, &msg);
anyhow::bail!(msg)
}
}
}
/// The OS user recorded as `promoted_by` on a LOCAL promotion — `$USER`, then
/// `$LOGNAME`, then the numeric euid (`uid:1000`). Pure `std`: no shell, no
/// `whoami` subprocess (`pure-rust-zero-shell-law`). Never returns "anonymous":
/// an unattributable promotion is worse than an ugly one.
fn promoting_os_user() -> String {
for key in ["USER", "LOGNAME"] {
if let Ok(v) = std::env::var(key) {
let v = v.trim().to_string();
if !v.is_empty() {
return v;
}
}
}
#[cfg(all(unix, feature = "systemd"))]
#[allow(clippy::needless_return)] // the tail only in a build with `systemd`
{
// skidbladnir already owns the euid lookup the lifecycle code uses — reuse
// it rather than adding a libc dep + an unsafe block here (L5).
return format!("uid:{}", skidbladnir_systemd::systemd::current_euid());
}
// Without the systemd flavour (a container, the appliance): `/proc/self` is
// owned by this process's effective uid. Pure std.
#[cfg(all(unix, not(feature = "systemd")))]
#[allow(clippy::needless_return)] // the tail only in a unix build without `systemd`
{
use std::os::unix::fs::MetadataExt as _;
return match std::fs::metadata("/proc/self") {
Ok(m) => format!("uid:{}", m.uid()),
Err(_) => "unknown-local-user".to_string(),
};
}
#[cfg(not(unix))]
{
"unknown-local-user".to_string()
}
}
/// Recursively collect file paths under `dir`, relative to `dir` (so the scanner
/// sees `name-version.ext` basenames, matching the UI's archive listing).
#[cfg(feature = "supply-chain")]
fn collect_files(dir: &Path, base: &Path, out: &mut Vec<String>) -> Result<()> {
for entry in std::fs::read_dir(dir).with_context(|| format!("read dir {}", dir.display()))? {
let path = entry?.path();
if path.is_dir() {
collect_files(&path, base, out)?;
} else if let Ok(rel) = path.strip_prefix(base) {
out.push(rel.to_string_lossy().into_owned());
}
}
Ok(())
}
/// Drive the `holger-server scan` Móðguðr verb: walk `--path`, scan every file
/// against the demo policy + advisory DB, print the Pass/Warn/Block report, and
/// fail (non-zero exit) when any artifact BLOCKs. The verdict logic is modgunn's;
/// this is pure reuse of `server_lib::security` (the Tools#3 UI twin).
#[cfg(feature = "supply-chain")]
fn run_scan(a: ScanArgs) -> Result<()> {
use server_lib::security::{self, Decision};
let ecosystem = security::ecosystem_for(&a.ecosystem);
let mut files = Vec::new();
collect_files(&a.path, &a.path, &mut files)?;
let refs: Vec<&str> = files.iter().map(|s| s.as_str()).collect();
// S7: an `intel:` block means the local advisory database, never the network.
let intel = match &a.config {
Some(c) => {
read_ron_config(c)
.with_context(|| format!("load config {}", c.display()))?
.intel
}
None => None,
};
let (scanner, evidence) = match &intel {
Some(i) => {
use server_lib::holger_intel::{resolve_local, LocalIntelDb, StalenessRule};
let rule = StalenessRule {
warn_after_days: i.warn_after_days,
block_after_days: i.block_after_days,
};
let now = chrono::DateTime::<chrono::Utc>::from_timestamp_millis(
server_lib::clock::now_unix_ms(),
)
.unwrap_or_else(chrono::Utc::now);
let r = resolve_local(
&LocalIntelDb::open(&i.db_dir),
rule,
now,
Some("offline: network refused".into()),
)?;
(
security::OsvScanner::new(security::demo_policy(), r.db),
Some(r.evidence),
)
}
None => (security::demo_scanner(), None),
};
let mut report = security::scan_listing(&ecosystem, refs, &scanner);
if let Some(ev) = &evidence {
println!("INTEL {}", ev.describe());
report.pass = 0;
report.warn = 0;
report.block = 0;
for v in report.verdicts.iter_mut() {
*v = server_lib::holger_intel::fold_staleness(v.clone(), ev);
match v.decision {
Decision::Pass => report.pass += 1,
Decision::Warn => report.warn += 1,
Decision::Block => report.block += 1,
}
}
}
println!(
"Móðguðr scan: {} · scanned {} · pass {} · warn {} · block {} · skipped {}",
ecosystem,
report.verdicts.len(),
report.pass,
report.warn,
report.block,
report.skipped,
);
for v in report.verdicts.iter().filter(|v| !v.findings.is_empty()) {
let tag = match v.decision {
Decision::Block => "BLOCK",
Decision::Warn => "WARN",
Decision::Pass => "PASS",
};
for f in &v.findings {
println!(
" [{tag}] {} {} — {} ({:?}): {}",
v.artifact.name, v.artifact.version, f.id, f.severity, f.summary
);
}
}
match report.decision() {
Decision::Block => {
functional_status("holger-cli/scan", "scan_verdict", false, "BLOCK");
println!("VERDICT: BLOCK");
anyhow::bail!("Móðguðr blocked {} artifact(s)", report.block);
}
Decision::Warn => {
functional_status("holger-cli/scan", "scan_verdict", true, "WARN");
println!("VERDICT: WARN");
}
Decision::Pass => {
functional_status("holger-cli/scan", "scan_verdict", true, "PASS");
println!("VERDICT: PASS");
}
}
Ok(())
}
/// Best-effort current user name for the printed hints (`$USER`/`whoami` fallback).
#[cfg(feature = "systemd")]
fn whoami_or(default: &str) -> String {
std::env::var("USER")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| default.to_string())
}
#[cfg(test)]
mod tests {
#[cfg(feature = "systemd")]
/// `userdel` that fails (or cannot be spawned) is a refusal, never a claim
/// that the account is gone (A28 L14).
#[test]
fn a_failed_userdel_is_counted_and_not_claimed() {
use std::os::unix::process::ExitStatusExt;
let mut failed = PurgeReport::default();
record_userdel(
"holger",
Ok(std::process::ExitStatus::from_raw(6 << 8)),
&mut failed,
);
assert_eq!(
failed.failed, 1,
"a userdel that exited 6 was not counted as a refusal"
);
assert!(
failed.identity_wiped.is_empty(),
"the account was claimed wiped: {:?}",
failed.identity_wiped
);
let mut unspawned = PurgeReport::default();
record_userdel(
"holger",
Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
&mut unspawned,
);
assert_eq!(unspawned.failed, 1);
assert!(unspawned.identity_wiped.is_empty());
let mut ok = PurgeReport::default();
record_userdel("holger", Ok(std::process::ExitStatus::from_raw(0)), &mut ok);
assert_eq!(ok.failed, 0);
assert_eq!(ok.identity_wiped, vec![PathBuf::from("user `holger`")]);
}
/// The first start writes a config that parses and wires, and never
/// overwrites one that is already there (the operator's edits survive).
#[test]
fn the_first_start_writes_its_config_once() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join(".holger");
let cfg = super::first_start_config(&data).unwrap();
assert_eq!(cfg, data.join("holger.ron"));
let mut h = server_lib::read_ron_config(&cfg).expect("the first-start config parses");
h.instantiate_backends().expect("its backends instantiate");
std::fs::write(&cfg, b"( edited: true )").unwrap();
super::first_start_config(&data).unwrap();
assert_eq!(std::fs::read(&cfg).unwrap(), b"( edited: true )");
}
/// No command parses (it is the first start), so `holger` alone is valid.
#[test]
fn no_command_is_the_first_start() {
use clap::Parser;
assert!(super::Cli::try_parse_from(["holger"])
.unwrap()
.command
.is_none());
}
use super::*;
/// RED without the `default_filter_or` above (plain `env_logger::init()`
/// floors at `error`): every boot-time `SECURITY:`/`AUTH:` posture line holger
/// emits is `log::warn!`, so under the shipped default an operator could not
/// tell from the output that a control was off.
#[test]
fn boot_security_warnings_are_visible_with_no_rust_log_set() {
// The level floor a builder settles on when `RUST_LOG` is unset. A variable
// nothing sets stands in for "unset", so the test never mutates process env.
// A closure, not a fn, so the `log::LevelFilter` type is never NAMED — this
// crate does not depend on `log` directly and should not start to for a test.
let floor_with_no_rust_log = |spec: &str| {
env_logger::Builder::from_env(
env_logger::Env::default().filter_or("HOLGER_TEST_UNSET_LOG_VAR", spec),
)
.build()
.filter()
};
let actual = floor_with_no_rust_log(server_lib::DEFAULT_LOG_FILTER);
// `warn`/`error` are named as SPECS, not as `log::LevelFilter` paths, so
// this test needs no `log` dependency of its own.
assert!(
actual >= floor_with_no_rust_log("warn"),
"the default log floor must admit `warn` — holger's whole boot-time security \
posture report is emitted at that level; got {actual:?}"
);
assert!(
actual > floor_with_no_rust_log("error"),
"`error` is env_logger's own default and is exactly the floor that hid the \
security warnings; got {actual:?}"
);
}
#[cfg(feature = "systemd")]
/// The rendered systemd unit sets no `RUST_LOG`, which is why the floor has to
/// live in the binary. If a unit ever starts pinning one, this test says so and
/// the decision gets revisited rather than silently duplicated.
#[test]
fn the_systemd_unit_does_not_pin_rust_log() {
let a = args(Some(ScopeArg::System));
let exec = PathBuf::from("/usr/local/bin/holger-server");
let data = PathBuf::from("/home/holger/.holger");
let unit = systemd_unit_scoped(&holger_service_spec(&a, &exec, &data), Scope::System);
assert!(
!unit.contains("RUST_LOG"),
"unit pins RUST_LOG — the binary default is then dead weight:\n{unit}"
);
}
#[cfg(feature = "systemd")]
fn args(scope: Option<ScopeArg>) -> SvcArgs {
SvcArgs {
scope,
bin: Some(PathBuf::from("/usr/local/bin/holger-server")),
config: None,
data: Some(PathBuf::from("/home/holger/.holger")),
user: "holger".to_string(),
purge: false,
wipe_identity: false,
}
}
#[cfg(feature = "systemd")]
/// **`holger install add` lays the config its unit starts with** (lane
/// OX-HOLFLAV). The unit runs `start --config <data>/holger.ron`; on a fresh
/// host nothing wrote that file, so the service died at its first start and the
/// install rolled back. The install now lays the first start's config (the two
/// local registries on loopback) when the data root has none, never over an
/// existing one, and never when `--config` names the operator's own file.
#[test]
fn the_install_lays_the_first_start_config_once() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join(".holger");
let a = args(Some(ScopeArg::System));
let laid = lay_service_config(&a, &data).unwrap();
assert_eq!(
laid.as_deref(),
Some(data.join("holger.ron").as_path()),
"a fresh data root gets its config"
);
let mut h =
server_lib::read_ron_config(data.join("holger.ron")).expect("the laid config parses");
h.instantiate_backends().expect("its backends instantiate");
std::fs::write(data.join("holger.ron"), b"( edited: true )").unwrap();
assert_eq!(
lay_service_config(&a, &data).unwrap(),
None,
"a second install lays nothing"
);
assert_eq!(
std::fs::read(data.join("holger.ron")).unwrap(),
b"( edited: true )",
"the operator's edit survives"
);
let other = dir.path().join("other");
let mut own = args(Some(ScopeArg::System));
own.config = Some(dir.path().join("mine.ron"));
assert_eq!(
lay_service_config(&own, &other).unwrap(),
None,
"--config names the operator's file"
);
assert!(
!other.join("holger.ron").exists(),
"nothing laid beside an operator's --config"
);
}
/// Build a FAKE holger service home: the `.ssh` deploy key + a token (IDENTITY)
/// beside archives / audit / autogen config / cache / registry state (DATA).
/// Returns `(home, key_bytes, token_bytes)`.
#[cfg(unix)]
#[cfg(feature = "systemd")]
fn fake_holger_home(home: &Path) -> (PathBuf, Vec<u8>, Vec<u8>) {
// (a) IDENTITY — the deploy key registered on the private git remotes.
std::fs::create_dir_all(home.join(".ssh")).unwrap();
let key_bytes =
b"-----BEGIN OPENSSH PRIVATE KEY-----\nHOLGER-DEPLOY-KEY-DO-NOT-NUKE\n".to_vec();
std::fs::write(home.join(".ssh/id_ed25519"), &key_bytes).unwrap();
std::fs::write(
home.join(".ssh/id_ed25519.pub"),
b"ssh-ed25519 AAAA holger\n",
)
.unwrap();
let token_bytes = b"holger-registry-token-abc123".to_vec();
std::fs::write(home.join("token"), &token_bytes).unwrap();
// (b) DATA — everything `--purge` must erase.
let data = home.join(".holger");
std::fs::create_dir_all(data.join("archives")).unwrap();
std::fs::write(data.join("archives/rust.znippy"), b"crate bytes").unwrap();
std::fs::create_dir_all(data.join("audit")).unwrap();
std::fs::write(data.join("audit/audit.arrow"), b"audit rows").unwrap();
std::fs::write(data.join("holger.ron"), b"( autogen: true )").unwrap();
std::fs::create_dir_all(home.join(".cache/holger")).unwrap();
std::fs::write(home.join(".cache/holger/blob"), b"cached blob").unwrap();
(data, key_bytes, token_bytes)
}
#[cfg(feature = "systemd")]
/// **The owner's uninstall law, proven:** `--purge` NUKES ALL holger DATA (archives,
/// audit log, autogen config, caches) while the `.ssh` deploy key and the stored
/// token survive BYTE-IDENTICAL. RED the moment the purge so much as touches one
/// byte of the key — which is exactly what the old `remove_dir_all(/home/holger)`
/// did.
#[cfg(unix)]
#[test]
fn purge_nukes_all_data_but_keeps_the_ssh_key_and_token_byte_identical() {
let d = tempfile::tempdir().unwrap();
let home = d.path().join("home/holger");
std::fs::create_dir_all(&home).unwrap();
let (data, key_bytes, token_bytes) = fake_holger_home(&home);
let key = home.join(".ssh/id_ed25519");
let mut report = PurgeReport::default();
purge_tree_keep_identity(
&home,
nobody_uid(),
/* wipe_identity */ false,
&mut report,
);
// DATA: gone, all of it.
assert!(
!data.exists(),
"the data root {} must be erased",
data.display()
);
assert!(!home.join(".cache").exists(), "caches must be erased");
assert_eq!(report.failed, 0, "no purge target was refused");
assert!(report.removed_anything(), "the purge erased data");
// IDENTITY: untouched, byte-for-byte.
assert!(key.exists(), "the ssh deploy key MUST survive `--purge`");
assert_eq!(
std::fs::read(&key).unwrap(),
key_bytes,
"the ssh deploy key must be BYTE-IDENTICAL after a purge"
);
assert!(
home.join(".ssh/id_ed25519.pub").exists(),
"the whole .ssh dir survives"
);
assert_eq!(
std::fs::read(home.join("token")).unwrap(),
token_bytes,
"the stored token must be BYTE-IDENTICAL after a purge"
);
assert!(
home.exists(),
"the service home itself is never removed by a purge"
);
assert_eq!(
report.identity_kept.len(),
2,
"`.ssh` + token reported as KEPT"
);
assert!(
report.identity_wiped.is_empty(),
"a plain purge destroys no identity"
);
}
#[cfg(feature = "systemd")]
/// The explicit opt-in — and ONLY the explicit opt-in — destroys the identity.
#[cfg(unix)]
#[test]
fn wipe_identity_opt_in_destroys_the_key_and_the_token() {
let d = tempfile::tempdir().unwrap();
let home = d.path().join("home/holger");
std::fs::create_dir_all(&home).unwrap();
let (data, _key, _token) = fake_holger_home(&home);
let mut report = PurgeReport::default();
purge_tree_keep_identity(
&home,
nobody_uid(),
/* wipe_identity */ true,
&mut report,
);
assert!(!data.exists(), "data still erased");
assert!(
!home.join(".ssh").exists(),
"--wipe-identity destroys the deploy key"
);
assert!(
!home.join("token").exists(),
"--wipe-identity destroys the token"
);
assert_eq!(report.failed, 0, "no identity target was refused");
assert!(report.identity_kept.is_empty(), "nothing was kept");
assert_eq!(
report.identity_wiped.len(),
2,
"`.ssh` + token reported as DESTROYED"
);
}
#[cfg(feature = "systemd")]
/// `--wipe-identity` is REFUSED on its own: clap requires `--purge` alongside it,
/// so the destructive flag can never be reached without the operator asking for a
/// purge in the same breath. And `--purge` alone never sets it.
#[test]
fn wipe_identity_requires_purge_and_is_off_by_default() {
let ok = Cli::try_parse_from([
"holger-server",
"install",
"remove",
"--purge",
"--wipe-identity",
]);
assert!(ok.is_ok(), "--purge --wipe-identity is accepted");
let bad = Cli::try_parse_from(["holger-server", "install", "remove", "--wipe-identity"]);
assert!(
bad.is_err(),
"--wipe-identity without --purge must be refused"
);
let plain = args(Some(ScopeArg::System));
assert!(
!plain.purge && !plain.wipe_identity,
"both flags default OFF"
);
}
#[cfg(feature = "systemd")]
/// The purge root is **scope-aware**, never one hardcoded constant: user scope
/// erases only its own data root, system scope erases the `holger` service home
/// that CONTAINS the data root (so `/home/holger/.ssh` is reached by the keep-list
/// instead of by a blanket `remove_dir_all`), plus any out-of-home `--data`.
#[test]
fn purge_root_follows_the_scope_in_use() {
assert_eq!(
purge_roots(Scope::User, Path::new("/home/me/.holger")),
vec![PathBuf::from("/home/me/.holger")],
"user scope purges its own data root and nothing else"
);
assert_eq!(
purge_roots(Scope::System, Path::new("/home/holger/.holger")),
vec![PathBuf::from(HOLGER_SYSTEM_HOME)],
"system scope purges the service home that holds the data root"
);
assert_eq!(
purge_roots(Scope::System, Path::new("/var/lib/holger")),
vec![
PathBuf::from(HOLGER_SYSTEM_HOME),
PathBuf::from("/var/lib/holger")
],
"an explicit --data outside the service home is a second root"
);
}
#[cfg(feature = "systemd")]
/// A uid that owns nothing on this box, so skidbladnir's guard evaluates the temp
/// tree on its own merits (no passwd home, no `$HOME` overlap) — the fixture never
/// depends on who runs the test.
#[cfg(unix)]
fn nobody_uid() -> u32 {
65_534
}
#[cfg(feature = "systemd")]
/// System scope: the rendered unit uses the REAL serve verb (`start --config`),
/// the `holger` service user, the holger-named data root, and the default
/// config under the data root.
#[test]
fn holger_unit_system_scope() {
let a = args(Some(ScopeArg::System));
let exec = PathBuf::from("/usr/local/bin/holger-server");
let data = PathBuf::from("/home/holger/.holger");
let spec = holger_service_spec(&a, &exec, &data);
assert_eq!(spec.unit_file_name(), "holger.service");
let unit = systemd_unit_scoped(&spec, Scope::System);
assert!(unit.contains("User=holger"));
assert!(unit.contains("WorkingDirectory=/home/holger/.holger"));
assert!(unit.contains(
"ExecStart=/usr/local/bin/holger-server start --config /home/holger/.holger/holger.ron"
));
assert!(unit.contains("Restart=on-failure"));
assert!(unit.contains("WantedBy=multi-user.target"));
}
#[cfg(feature = "systemd")]
/// User scope: NO `User=`/`Group=` (it runs as you), targets `default.target`,
/// same `start --config` verb.
#[test]
fn holger_unit_user_scope() {
let a = args(Some(ScopeArg::User));
let exec = PathBuf::from("/home/me/.cargo/bin/holger-server");
let data = PathBuf::from("/home/me/.holger");
let spec = holger_service_spec(&a, &exec, &data);
let unit = systemd_unit_scoped(&spec, Scope::User);
assert!(!unit.contains("User="));
assert!(!unit.contains("Group="));
assert!(unit.contains("WantedBy=default.target"));
assert!(unit.contains(
"ExecStart=/home/me/.cargo/bin/holger-server start --config /home/me/.holger/holger.ron"
));
}
#[cfg(feature = "systemd")]
/// The ship path: `holger_spec()` + `Lifecycle` render the SAME unit as the
/// systemd-spec path (one lifecycle, one renderer), and holger is classified as
/// a FAT node (warehouse-holding) so thin holger nodes inherit the split for
/// free. Proves holger delegates to the ship and hand-rolls no systemd.
#[test]
fn holger_spec_lifecycle_matches_and_is_fat() {
let a = args(Some(ScopeArg::System));
let exec = PathBuf::from("/usr/local/bin/holger-server");
let data = PathBuf::from("/home/holger/.holger");
let spec = holger_spec(&a, Scope::System, &exec, &data);
assert_eq!(
spec.agent,
AgentMode::Fat,
"holger is a fat (warehouse) node"
);
assert_eq!(spec.scope, Scope::System);
let lc = Lifecycle::new(spec);
assert_eq!(lc.unit(), "holger.service");
// The ship's Lifecycle renders the SAME text as the systemd-spec path.
let via_ship = lc.unit_text();
let via_systemd =
systemd_unit_scoped(&holger_service_spec(&a, &exec, &data), Scope::System);
assert_eq!(
via_ship, via_systemd,
"ship + systemd-spec paths render identically"
);
assert!(via_ship.contains(
"ExecStart=/usr/local/bin/holger-server start --config /home/holger/.holger/holger.ron"
));
assert!(via_ship.contains("User=holger"));
// User scope drops User=/Group= and (a --user unit) the network-online edge.
let user = Lifecycle::new(holger_spec(
&args(Some(ScopeArg::User)),
Scope::User,
&exec,
&data,
))
.unit_text();
assert!(!user.contains("User="));
assert!(
!user.contains("After="),
"a --user unit orders against no system target"
);
assert!(user.contains("WantedBy=default.target"));
}
/// A unique scratch dir under the system temp root (no tempfile dep).
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"holger-scan-test-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
/// `collect_files` walks recursively and yields paths RELATIVE to the base
/// (so the scanner sees `name-version.ext` basenames).
#[cfg(feature = "supply-chain")]
#[test]
fn collect_files_is_recursive_and_relative() {
let dir = scratch("collect");
std::fs::create_dir_all(dir.join("crates")).unwrap();
std::fs::write(dir.join("crates/serde-1.0.0.crate"), b"x").unwrap();
std::fs::write(dir.join("top-1.0.0.crate"), b"x").unwrap();
let mut out = Vec::new();
collect_files(&dir, &dir, &mut out).unwrap();
out.sort();
assert_eq!(
out,
vec![
"crates/serde-1.0.0.crate".to_string(),
"top-1.0.0.crate".to_string()
]
);
std::fs::remove_dir_all(&dir).ok();
}
/// A clean corpus passes (Ok); a corpus carrying the demo High openssl
/// advisory BLOCKs (Err) — the gate-able exit-code behaviour.
#[cfg(feature = "supply-chain")]
#[test]
fn scan_passes_clean_and_blocks_vulnerable() {
let clean = scratch("clean");
std::fs::write(clean.join("serde-1.0.0.crate"), b"x").unwrap();
std::fs::write(clean.join("tokio-1.40.0.crate"), b"x").unwrap();
assert!(run_scan(ScanArgs {
path: clean.clone(),
ecosystem: "rust".into(),
config: None
})
.is_ok());
std::fs::remove_dir_all(&clean).ok();
let vuln = scratch("vuln");
std::fs::write(vuln.join("serde-1.0.0.crate"), b"x").unwrap();
std::fs::write(vuln.join("openssl-0.10.55.crate"), b"x").unwrap();
let err = run_scan(ScanArgs {
path: vuln.clone(),
ecosystem: "rust".into(),
config: None,
})
.unwrap_err();
assert!(err.to_string().contains("blocked"), "unexpected: {err}");
std::fs::remove_dir_all(&vuln).ok();
}
#[cfg(feature = "systemd")]
/// An explicit `--config` overrides the `<data>/holger.ron` default.
#[test]
fn explicit_config_overrides_default() {
let mut a = args(Some(ScopeArg::System));
a.config = Some(PathBuf::from("/etc/holger/holger.ron"));
let exec = PathBuf::from("/usr/local/bin/holger-server");
let data = PathBuf::from("/home/holger/.holger");
let unit = systemd_unit_scoped(&holger_service_spec(&a, &exec, &data), Scope::System);
assert!(unit.contains("--config /etc/holger/holger.ron"));
}
// ── the retention pin and the admin gate must name the same key ──────────
fn config_with(reserved: &str) -> server_lib::Holger {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("holger.ron");
std::fs::write(
&path,
format!(
r#"(repositories:[],exposed_endpoints:[],storage_endpoints:[],audit:(dir:"a",enabled:false){reserved})"#
),
)
.unwrap();
read_ron_config(&path).expect("the fixture config parses")
}
fn pin(key: &str) -> server_lib::retention::RetentionPolicy {
server_lib::retention::RetentionPolicy {
protect_property: Some((key.to_string(), "true".to_string())),
..Default::default()
}
}
/// The whole point of the gate: a pin key the server does not reserve is a pin
/// any Writer can set over the wire, which means it is not a pin. Retention
/// refuses rather than running and quietly keeping everything.
#[cfg(feature = "supply-chain")]
#[test]
fn retention_refuses_a_pin_the_server_does_not_reserve_to_admins() {
let holger = config_with("");
let err = refuse_an_unreserved_pin(&holger, &pin("pinned"))
.expect_err("an unreserved pin must be refused")
.to_string();
assert!(
err.contains("pinned"),
"the refusal must name the key: {err}"
);
assert!(
err.contains("reserved_properties"),
"and must name the config key that fixes it: {err}"
);
}
/// The documented default pin works with no config at all — the gate must not
/// break the shipped `--protect-property keep=true`.
#[cfg(feature = "supply-chain")]
#[test]
fn the_documented_default_pin_needs_no_configuration() {
let holger = config_with("");
refuse_an_unreserved_pin(&holger, &pin("keep")).expect("`keep` is reserved by default");
}
/// A deployment that renamed the pin declares it, and then both halves agree.
#[cfg(feature = "supply-chain")]
#[test]
fn a_declared_pin_key_is_accepted_and_the_default_is_then_replaced() {
let holger = config_with(r#",reserved_properties:Some(["pinned"])"#);
refuse_an_unreserved_pin(&holger, &pin("pinned")).expect("a declared key is reserved");
// …and declaring a set REPLACES the default, so the old key is no longer
// reserved and a pin on it is refused. That is the honest reading of an
// explicit list, and it is why the refusal prints the current set.
assert!(refuse_an_unreserved_pin(&holger, &pin("keep")).is_err());
}
/// No pin, no gate: a retention run that does not use `--protect-property` is
/// unaffected however the config is written.
#[cfg(feature = "supply-chain")]
#[test]
fn a_retention_run_without_a_pin_is_never_refused() {
let holger = config_with(r#",reserved_properties:Some([])"#);
let no_pin = server_lib::retention::RetentionPolicy::default();
assert!(no_pin.protect_property.is_none());
refuse_an_unreserved_pin(&holger, &no_pin).expect("no pin, nothing to check");
}
}
/// `holger-server fleet-enrol` — join the ONE monetize.
#[cfg(feature = "metered")]
#[derive(Parser, Debug)]
struct FleetEnrolArgs {
/// The ONE monetize's door (`https://monetize.ignalina.dk`).
#[arg(long)]
door: String,
/// The operator's one-shot token (`att1.<id>.<secret>`).
#[arg(long)]
token: String,
/// This estate's name at the ONE monetize (a DNS label).
#[arg(long)]
estate: String,
/// The estate owner's OpenSSH private key (ed25519) the request is signed with.
#[arg(long)]
owner_key: PathBuf,
/// The PEM signing request of this box's own key (the key never leaves the box).
#[arg(long)]
csr: PathBuf,
/// Where this box's control plane is, `host:port` (kept for the operator's eyes).
#[arg(long)]
endpoint: String,
/// The box that calls in (`main`).
#[arg(long, default_value = "main")]
box_name: String,
/// This box's data root (where `fleet/fleet.json` is kept).
#[arg(long)]
data: PathBuf,
/// The twin's own fleet key (64 hex, its `fleet/box.ed25519` public half), when the
/// estate has a twin: the central pins it for the twin as it pins this box's for the main.
#[arg(long)]
twin_key: Option<String>,
}
#[cfg(feature = "metered")]
fn run_fleet_enrol(a: FleetEnrolArgs) -> Result<()> {
use server_lib::holger_fleet::enrol;
let owner = ssh_key::PrivateKey::read_openssh_file(&a.owner_key)
.map_err(|e| anyhow::anyhow!("{}: {e}", a.owner_key.display()))?;
let csr_pem =
std::fs::read_to_string(&a.csr).map_err(|e| anyhow::anyhow!("{}: {e}", a.csr.display()))?;
let owner_line = owner
.public_key()
.to_openssh()
.map_err(|e| anyhow::anyhow!("the owner's public key: {e}"))?;
let at = server_lib::holger_fleet::now_unix_ms();
let nonce = format!("{at:x}-{:x}", std::process::id());
let req = enrol::EnrolRequest {
v: 1,
token: a.token.trim().to_string(),
estate: a.estate.trim().to_string(),
product: server_lib::holger_fleet::PRODUCT.into(),
box_name: a.box_name,
csr_pem,
endpoint: a.endpoint,
owner_key: owner_line,
estate_ca_pem: String::new(),
nonce,
at_unix_ms: at as i64,
owner_sig: String::new(),
}
.sign(&owner)
.map_err(|e| anyhow::anyhow!(e))?;
let reply = enrol::post(&a.door, &req).map_err(|e| anyhow::anyhow!(e))?;
let cfg = enrol::remember(&a.data, &a.door, &req, &reply).map_err(|e| anyhow::anyhow!(e))?;
println!(
"enrolled: {} reports to {} as holger; orders verify under {}",
cfg.instance, cfg.url, cfg.monetize_pubkey
);
// The boxes that will call, with their keys: the central pins exactly these and keeps the
// main's key as the estate's appliance key (the actor tickets, `room.rs`).
let own = server_lib::holger_fleet::BoxKey::load_or_mint(&a.data)
.map_err(|e| anyhow::anyhow!("this box's fleet key: {e}"))?;
let own_hex = server_lib::holger_fleet::hex(&own.public());
let mut keys = std::collections::BTreeMap::from([(req.box_name.clone(), own_hex.clone())]);
if let Some(t) = a
.twin_key
.as_deref()
.map(str::trim)
.filter(|t| !t.is_empty())
{
keys.insert("twin".into(), t.to_string());
}
let at = server_lib::holger_fleet::now_unix_ms() as i64;
let n = enrol::notice(
&req.estate,
&keys,
&own_hex,
&format!("{at:x}-i-{:x}", std::process::id()),
at,
)
.sign(&owner)
.map_err(|e| anyhow::anyhow!(e))?;
let said = enrol::installed(&a.door, &n).map_err(|e| anyhow::anyhow!(e))?;
println!(
"installed: the central pins {} and keeps the main's ticket key ({said})",
n.roles.join(", ")
);
Ok(())
}
/// `holger-server intake` — the intake chain's wire verb, as yourself.
#[derive(Parser, Debug)]
#[cfg(feature = "intake")]
struct IntakeArgs {
/// advance | quarantine | release | block | state | evidence
verb: String,
/// The server's gRPC door, `host:port` (or a full `http(s)://` URL).
#[arg(long)]
server: String,
/// A file holding YOUR bearer (OIDC access token, static token, passkey
/// session). Absent ⇒ `$HOLGER_TOKEN`; neither ⇒ no credential.
#[arg(long)]
token_file: Option<PathBuf>,
/// The artifact's name (for a raw intake repository, its file name).
#[arg(long, default_value = "")]
name: String,
/// For `evidence`: write every record, envelope and the signer certificate
/// into this directory, so they can be verified offline.
#[arg(long)]
out: Option<PathBuf>,
#[arg(long, default_value = "")]
namespace: String,
#[arg(long, default_value = "")]
version: String,
/// For `advance`: the state you mean to reach (refused unless it is the next one).
#[arg(long)]
to: Option<String>,
/// Why — kept in the audit record and the approval statement.
#[arg(long, default_value = "")]
reason: String,
}
#[cfg(feature = "intake")]
async fn run_intake(a: IntakeArgs) -> Result<()> {
use server_lib::grpc::holger_proto::{
AdvanceArtifactRequest, ArtifactId, GetArtifactStateRequest, IntakeVerb,
};
use server_lib::promote::duties::client;
let bearer = match &a.token_file {
Some(p) => Some(
std::fs::read_to_string(p)
.map_err(|e| anyhow::anyhow!("{}: {e}", p.display()))?
.trim()
.to_string(),
),
None => std::env::var("HOLGER_TOKEN").ok(),
}
.filter(|t| !t.is_empty());
if a.verb == "evidence" {
use server_lib::grpc::holger_proto::GetArtifactEvidenceRequest;
if a.name.is_empty() {
anyhow::bail!("`intake evidence` needs --name");
}
let artifact = Some(ArtifactId {
namespace: a.namespace.clone(),
name: a.name.clone(),
version: a.version.clone(),
});
let r = client::evidence(
&a.server,
bearer.as_deref(),
GetArtifactEvidenceRequest { artifact },
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!(
"{} sha256 {} ({}): {} stored evidence record(s)",
a.name,
r.sha256,
r.state,
r.stored.len()
);
for rec in &r.stored {
let signed = if rec.envelope.is_empty() {
"unsigned"
} else {
"signed"
};
let fresh = if rec.stale.is_empty() {
"fresh".to_string()
} else {
format!("stale: {}", rec.stale)
};
println!(
" {} {} scanned_at {} {signed}, {fresh}",
rec.id, rec.verdict, rec.scanned_at
);
for why in &rec.reasons {
println!(" {why}");
}
}
if !r.sbom_link.is_empty() {
println!("sbom: {}", r.sbom_link);
}
if let Some(dir) = &a.out {
std::fs::create_dir_all(dir).map_err(|e| anyhow::anyhow!("{}: {e}", dir.display()))?;
for rec in &r.stored {
std::fs::write(
dir.join(format!("{}.evidence.json", rec.id)),
&rec.evidence_json,
)?;
if !rec.envelope.is_empty() {
std::fs::write(dir.join(format!("{}.dsse.json", rec.id)), &rec.envelope)?;
}
}
if !r.signer_cert.is_empty() {
std::fs::write(dir.join("attestation-signer.der"), &r.signer_cert)?;
}
println!("written to {}", dir.display());
}
return Ok(());
}
if a.name.is_empty() {
anyhow::bail!("`intake {}` needs --name", a.verb);
}
let artifact = Some(ArtifactId {
namespace: a.namespace.clone(),
name: a.name.clone(),
version: a.version.clone(),
});
let verb = match a.verb.as_str() {
"state" => {
let r = client::state(&a.server, bearer.as_deref(), GetArtifactStateRequest { artifact })
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!(
"{} sha256 {} imported by {} ({} of {} approvals pending)",
r.state, r.sha256, r.importer, r.approvals, r.approvals_required
);
if !r.source.is_empty() {
println!("source {}; effective policy {}", r.source, r.effective_policy);
}
for t in r.history {
let ev: Vec<String> = t.evidence.iter().map(|e| format!("{}:{}={}", e.kind, e.source, e.verdict)).collect();
println!(" {} → {} by {} at {} {}", t.from_state, t.to_state, t.actor, t.at, ev.join(" "));
}
return Ok(());
}
"advance" => IntakeVerb::Advance,
"quarantine" => IntakeVerb::Quarantine,
"release" => IntakeVerb::Release,
"block" => IntakeVerb::Block,
other => anyhow::bail!("'{other}' is not an intake verb (advance, quarantine, release, block, state, evidence)"),
};
let r = client::advance(
&a.server,
bearer.as_deref(),
AdvanceArtifactRequest {
artifact,
verb: verb as i32,
to_state: a.to.clone().unwrap_or_default(),
reason: a.reason.clone(),
evidence: vec![],
},
)
.await
.map_err(|e| anyhow::anyhow!(e))?;
println!(
"{} (as {}; {} of {} approvals)",
r.message, r.actor, r.approvals, r.approvals_required
);
Ok(())
}
/// `holger-server audit` — the trail, the chain verify, the query, the aggregate,
/// the Arrow export and the SIEM batch export.
#[derive(Parser, Debug)]
struct AuditArgs {
/// trail | verify | query | aggregate | export
verb: String,
/// `query`/`aggregate`/`export --out`: the actor.
#[arg(long, default_value = "")]
who: String,
/// `query`/`aggregate`/`export --out`: the repository.
#[arg(long, default_value = "")]
repository: String,
/// `query`/`aggregate`/`export --out`: a prefix of the artifact.
#[arg(long, default_value = "")]
artifact: String,
/// `query`/`aggregate`/`export --out`: the action (download, upload, …).
#[arg(long, default_value = "")]
action: String,
/// `query`/`aggregate`/`export --out`: the client address, with or without a port.
#[arg(long, default_value = "")]
source_ip: String,
/// `query`/`aggregate`/`export --out`: the exact status.
#[arg(long, default_value_t = 0)]
status: u32,
/// `query`/`aggregate`/`export --out`: from (inclusive), ns since the epoch.
#[arg(long, default_value_t = 0)]
since: i64,
/// `query`/`aggregate`/`export --out`: to (exclusive), ns since the epoch.
#[arg(long, default_value_t = 0)]
until: i64,
/// `query`: records to skip.
#[arg(long, default_value_t = 0)]
offset: u64,
/// `query`: page size (0: 100, at most 1000).
#[arg(long, default_value_t = 0)]
limit: usize,
/// `aggregate`: the keys (repo, action, day, ident, client, status).
#[arg(long, value_delimiter = ',')]
by: Vec<String>,
/// `aggregate`: count only what was served (2xx).
#[arg(long)]
ok_only: bool,
/// `export`: write the selected records as one Arrow IPC file here (instead of
/// the SIEM export).
#[arg(long)]
out: Option<PathBuf>,
/// `trail`: the server's gRPC door, `host:port` (or a full `http(s)://` URL).
#[arg(long)]
server: Option<String>,
/// `trail`: a file holding YOUR bearer. Absent ⇒ `$HOLGER_TOKEN`.
#[arg(long)]
token_file: Option<PathBuf>,
/// `trail`: the artifact's sha256 (lowercase hex; `sha256:` allowed).
#[arg(long)]
sha256: Option<String>,
/// `verify` / `export`: the RON config whose audit dir (and `audit.siem`) to use.
#[arg(long)]
config: Option<PathBuf>,
/// `verify`: an audit directory, instead of `--config`.
#[arg(long)]
dir: Option<PathBuf>,
/// `verify`: trust roots (DER) the checkpoint signatures must chain to.
/// Absent ⇒ the config's attestation trust roots; neither ⇒ signatures are
/// counted, not checked.
#[arg(long)]
root: Vec<PathBuf>,
}
/// The audit dir `query`/`aggregate`/`export --out` read: --dir, or --config's
/// (refused when that config records nothing, `holger-audit-8`).
fn audit_read_dir(a: &AuditArgs, verb: &str) -> Result<PathBuf> {
match (&a.dir, &a.config) {
(Some(d), _) => Ok(d.clone()),
(None, Some(c)) => {
let h = server_lib::read_ron_config(c)?;
let mode = h.audit.effective_mode();
if mode == server_lib::audit::AuditMode::Off {
anyhow::bail!(server_lib::errcode::AUDIT_QUERY_UNAVAILABLE.tag(format!(
"{} records no audit data (audit mode: {mode}) — {}",
c.display(),
mode.caveat()
)));
}
Ok(h.resolved_audit_dir())
}
(None, None) => anyhow::bail!("`audit {verb}` needs --dir or --config"),
}
}
The file goes on; the console shows all of it.