proto/holger.proto
syntax = "proto3";
package holger.v1;
// Repository service — query and manage artifacts
service RepositoryService {
// Fetch a single artifact by ID
rpc FetchArtifact(FetchArtifactRequest) returns (FetchArtifactResponse);
// List artifacts in a repository (with optional filter)
rpc ListArtifacts(ListArtifactsRequest) returns (ListArtifactsResponse);
// Upload an artifact (write-enabled repos only)
rpc PutArtifact(PutArtifactRequest) returns (PutArtifactResponse);
// Stream download for large artifacts
rpc StreamArtifact(FetchArtifactRequest) returns (stream ArtifactChunk);
// Stream upload for large artifacts (stinky 14). The FIRST message names the
// repository and the artifact and may carry the first bytes; every later
// message carries bytes only. The server hands the pieces to the backend as
// they arrive, so the body is never whole in this process unless a gate must
// read it (the intake chain, the ingest gates) or `async_job` stages it.
rpc PutArtifactStream(stream PutArtifactChunk) returns (PutArtifactResponse);
}
// Archive service — znippy archive operations
service ArchiveService {
// List files in a znippy archive backing a repository
rpc ListArchiveFiles(ListArchiveFilesRequest) returns (ListArchiveFilesResponse);
// Get archive metadata (file count, total size, compression ratio)
rpc ArchiveInfo(ArchiveInfoRequest) returns (ArchiveInfoResponse);
}
// Health / admin
service AdminService {
// Server health check
rpc Health(HealthRequest) returns (HealthResponse);
// List configured repositories
rpc ListRepositories(ListRepositoriesRequest) returns (ListRepositoriesResponse);
// Report the server's operating profile — static (sealed, read-only, "rigged
// for silent running") vs dynamic (writable / proxy). Server-reported truth so
// the operator UI reflects the server, not a local toggle.
rpc ServerProfile(ServerProfileRequest) returns (ServerProfileResponse);
// Who does the SERVER think the caller is? Resolves the presented credential
// (Bearer / mTLS CN) through the same validation every gated call uses and
// reports the resulting identity + effective GLOBAL role — introspection
// only, no secrets, and NOT an authorization decision: every operation is
// still checked by its own gate. Anonymous callers get an answer too (the
// default role), so a UI can explain what "not logged in" means here.
rpc WhoAmI(WhoAmIRequest) returns (WhoAmIResponse);
// How much storage this holger has, may still write, and has already used —
// one statvfs(3) over its data root plus the archive bytes of every
// repository that OWNS storage (groups are views and are not summed). Admin
// read: it describes the host filesystem. A holger that was handed no data
// root refuses by name; it never answers a capacity of zero for a failure.
rpc Capacity(CapacityRequest) returns (CapacityReply);
// ★★ ELECT THIS BOX A TWIN — the bootstrap door, and the ONLY way a role is
// conferred over the wire.
//
// Owner rulings 2026-09-21: "the weakest machine must become the twin.
// PERIOD" and "IT must not CHANGE role later ! so its a bootstrap thing from
// xtask". So: both machines install from ONE identical medium with NO role
// parameter, the control plane MEASURES them (Capacity here, plus the host
// facts it reads itself), and it tells the weaker one what it is. Exactly
// once: a box that already holds a role record refuses by name
// (holger-twin-4), and a box that is not FRESH refuses by name
// (holger-twin-5) because electing a serving box would put its history
// behind a follower's refusals.
//
// ADMIN-gated like every other mutation here. The election is RECORDED and
// APPLIED LIVE (stinky 14 §P3): every door refuses writes by name from the
// reply on, and the follow starts. What waits for the next start is the
// loopback bind of the public doors — a process does not rebind a door
// underneath its clients.
rpc BecomeTwin(BecomeTwinRequest) returns (BecomeTwinReply);
// ★ THE TWIN OVER MUTUAL TLS — the front certifies, the primary's OWN
// issuer signs, the twin pins it (nordisk-twin; gunnar's model). In order:
// BecomeTwin on the twin; TwinCsr on the twin (the key is born there and
// never crosses); IssueTwinCertificate on the primary; InstallTwinCertificate
// on the twin. The same three with purpose "recharge" certify a fresh main to
// read its twin's door (then Recharge on the main). ADMIN-gated; the three
// pairing steps are asked of a twin too (they change no artifact).
rpc TwinCsr(TwinCsrRequest) returns (TwinCsrReply);
// Refused by name unless this box serves a replication door (holger-twin-7)
// and the principal is one it certifies (holger-twin-8): `holger-twin` from a
// primary only, `holger-recharge` from any box with a door.
rpc IssueTwinCertificate(IssueTwinCertificateRequest) returns (IssueTwinCertificateReply);
// Refused by name: another key (holger-twin-9), a CA that did not sign it
// (holger-twin-10), a CA other than the pinned one (holger-twin-11). A
// renewal under the SAME CA is taken.
rpc InstallTwinCertificate(InstallTwinCertificateRequest) returns (InstallTwinCertificateReply);
// A fresh main pulls the estate and every repository from the box its
// "recharge" credential names, over that door, now. Admin; refused on a twin.
rpc Recharge(RechargeRequest) returns (RechargeReply);
// ★ THE RECHARGE LANE'S TWO QUESTIONS (gunnar's Bootstrap.Status and the
// twin's log head, in holger's words). Asked BEFORE a byte moves, admin-read
// gated, answered by a box that serves nothing yet too.
// On the MAIN: fresh / restoring / recharged, and what makes it not fresh.
rpc RechargeStatus(RechargeStatusRequest) returns (RechargeStatusReply);
// On the TWIN: what it holds and whether it can give everything back.
rpc TwinStatus(TwinStatusRequest) returns (TwinStatusReply);
// ★ FENCE THIS PRIMARY (Gunnar's `fence_primary` pattern): the box that runs a
// pulled `fence_primary` (or a recharge's fence-first) asks the OLD primary to
// refuse every write from now on, and reads the fence back from the reply.
// ADMIN-gated by identity (not by the write gates: a fenced box answers a second
// fence the same). Refused on a twin, which takes no writes.
rpc FencePrimary(FencePrimaryRequest) returns (FencePrimaryReply);
// ★ THE ADMIN VIEW (2026-09-28; gunnar's admin mode, holger's own code): the
// operational snapshot an ADMINISTRATOR's console panes read — every disk and
// volume, the twin, the growth, the clock, plugins and monetize, the audit
// log, the installer's log door. ADMIN-READ (the Admin role under RBAC;
// refused by name to every other role and to a caller with no valid
// credential; open only on a server with no auth at all, like every admin
// read). Measured on every call, never cached. See AdminSnapshotReply.
rpc AdminSnapshot(AdminSnapshotRequest) returns (AdminSnapshotReply);
// ★ THE ESTATE BOARD (lane E3, 2026-10-19; Gunnar's `Admin.Topology` pattern,
// Holger's own code): "which boxes is this estate, as THIS holger knows them?"
// One row per box — this box first, then every box it holds EVIDENCE of: the
// pairing feed's front, main and twin, the twins whose reports it keeps, the
// primary a twin follows, and the downstream holgers that pull from it. Each row
// carries its own live state, the sentence that says how holger knows, the
// WireGuard overlay address, and the twin's lag. The board is DATA (Holger v2.0
// H2): the server decides every word and draws nothing; holger-ui draws the rows
// as they came and decides nothing. ADMIN-READ, like TwinStatus: the rows name
// addresses, never a secret. Also served as JSON, `GET /-/estate.json`, on the
// HTTP door.
rpc Topology(TopologyRequest) returns (TopologyReply);
// The ARTIFACT FLOW, drawn (restored by lane HOL-SVG-RON, owner 2026-10-30): the RON
// configuration as boxes and arrows (sources, repositories, the ladder, the intake
// steps and gates, the doors, storage), the estate it runs in (console, front, main,
// twin, every downstream by name) and the live counters on each. The same picture
// `GET /-/flow.svg` serves, from the same function; `graph_json` is the data model it
// is rendered from (`GET /-/flow.json`, schema `holger.flow/v2`), for another
// renderer. The server computes the picture; a console only shows it. Admin-read like
// the estate board. `not_found` when the RON says `flow: (enabled: false)`.
rpc FlowSvg(FlowSvgRequest) returns (FlowSvgReply);
// ★ THE SECURITY PAGE (lane HOL-SEC, owner 2026-10-27: "Security must show security, not
// the machinery again"). Holger is a security gateway; this is how its gate stands now, in
// sections the server words and the console draws as they came: the intake chain and how
// many sit at each step, each source and what it checks, each scanner and where scans run
// (a container), what waits for the two-person approval, what is quarantined, blocked or
// refused and why (by name), who may read (the `access` block and each repository's
// visibility), the signatures on what syncs to downstreams, and the last security events
// of the audit chain. Read by an Admin, a Repository Admin, a Security Approver or an
// Auditor (`holger-auth-2` otherwise). Names no secret.
rpc SecuritySummary(SecuritySummaryRequest) returns (SecuritySummaryReply);
// ★ THE BOX'S IMPORT DOORS (lane W4, 2026-10-19). A box has no shell, so the
// CLI's `intel import` and `bundle import` are reached here, over HTTP
// (`POST /-/intel/import`, `POST /-/bundle/import`) and through the
// `intel.inbox` / `bundle.inbox` drop directories, all running the SAME code:
// verify the signed bundle, hold it to the anti-rollback floor, write the
// audit record first, fail closed. The client streams the bundle's bytes;
// `first_import` (on any chunk) lets an absent floor be established.
// Repository Admin or Security Approver only (`holger-duty-2` otherwise).
//
// An intel bundle becomes the live local advisory database the next scan
// reads. No `intel:` block: `holger-import-3`.
rpc ImportIntelBundle(stream ImportBundleChunk) returns (ImportIntelBundleReply);
// An update or airgap bundle lands every artifact AVAILABLE through the intake
// chain (`IntakeChain::land_imported`), never into a raw repository's data
// directory. No intake chain: `holger-import-2`; no trust policy
// (`promotion.intake.trust_policy`): `holger-import-4`.
rpc ImportBundle(stream ImportBundleChunk) returns (ImportBundleReply);
}
// Promotion service — the DEV → PROD transition (.nornir/promotion-design.md).
// Promotion moves an artifact from a mutable DEV repository into an immutable,
// content-addressed, sealed PROD store: the promoted artifact is immutable
// (re-promote refused) and blake3 content-addressed. Admin-gated, fail-closed.
service PromotionService {
// Promote (seal) one artifact from a DEV repo into an immutable PROD store.
rpc PromoteArtifact(PromoteArtifactRequest) returns (PromoteArtifactResponse);
// Move one artifact of the intake chain to its next state, or quarantine,
// release or block it. Separation of duties: the caller's duties decide the
// verb (Importer → SCANNED/POLICY_CHECKED, SecurityApprover → APPROVED and
// every hold, SecurityApprover or RepositoryAdmin → SEALED/PROMOTED/
// AVAILABLE; Developer and Auditor move nothing). The importer of an artifact
// never approves or releases it. APPROVED needs `approvals` distinct
// approvers; a vote short of the quorum is recorded and answered with the
// count. Every approval is attested by the approver's principal and kept in
// the approval evidence slot. Refusals carry `holger-duty-<n>` or
// `holger-intake-<n>`.
rpc AdvanceArtifact(AdvanceArtifactRequest) returns (AdvanceArtifactResponse);
// The intake state, history and evidence of one artifact. A Developer sees
// only an AVAILABLE artifact; Importer, SecurityApprover, RepositoryAdmin
// and Auditor see every state.
rpc GetArtifactState(GetArtifactStateRequest) returns (GetArtifactStateResponse);
// The security dashboard's queues: how many intake artifacts stand in each
// queue (incoming, quarantined, awaiting-approval, blocked, available) and
// the rows of the one queue asked for, newest first. The server owns which
// states make up a queue. The same read duty as GetArtifactState decides each
// row: an artifact the caller may not read is neither listed nor counted.
rpc ListIntakeArtifacts(ListIntakeArtifactsRequest) returns (ListIntakeArtifactsResponse);
// The scan evidence of one intake artifact: the latest record of the
// evidence store (integrity hashes, every scanner's verdict and findings,
// the SBOM document) and the policy's words. The read duty of
// GetArtifactState decides it. W5: also every stored record with the DSSE
// envelope that signs it, the signer's certificate, whether a scan now would
// agree, and the SBOM link — the same records the SCANNED transition and the
// `osv` gate read back instead of scanning again. Also `GET
// /-/intake/evidence?sha256=` and `holger-server intake evidence`.
rpc GetArtifactEvidence(GetArtifactEvidenceRequest) returns (GetArtifactEvidenceResponse);
// THE DEPENDENCY SETS (lane HOL-DEPSET-APPROVE; these RPCs lane HOL-ENOSPC-SETS): a
// pull's whole closure, read and judged as ONE set. The same code as the HTTP doors
// `GET /-/intake/sets`, `GET /-/intake/set?id=`, `POST /-/intake/set/approve` and
// `POST /-/intake/set/refuse` (`pull_intake::depset`), and the same duties: a set is
// listed to a caller who may read every member; approve casts the caller's vote on every
// member waiting at POLICY_CHECKED (two-person rule per member, the importer never
// approves); refuse BLOCKS every waiting member with its reason. `holger-cli intake sets`,
// `intake set <id>`, `intake set approve <id>`, `intake set refuse <id> --reason`. A
// server without a pull-through answers FAILED_PRECONDITION, a build without the job
// queue UNIMPLEMENTED (both `holger-server-9`).
rpc ListDependencySets(ListDependencySetsRequest) returns (ListDependencySetsResponse);
rpc GetDependencySet(GetDependencySetRequest) returns (DependencySet);
rpc ApproveDependencySet(JudgeDependencySetRequest) returns (JudgeDependencySetResponse);
rpc RefuseDependencySet(JudgeDependencySetRequest) returns (JudgeDependencySetResponse);
}
// Audit service — the per-artifact trail and the audit chain (lane S8).
// READ-ONLY. Only an Auditor or an Admin may call it (holger-audit-10); a
// server whose audit sink keeps nothing readable answers holger-audit-8 rather
// than an empty trail.
service AuditService {
// The ordered trail of one artifact, keyed by the sha256 of its bytes:
// imported, hash verified, scanned (one per scanner), policy passed or
// denied, approved (one per approver), sealed, exported, imported on the
// airgap side. Each entry names its record's place in the hash chain.
rpc GetArtifactTrail(GetArtifactTrailRequest) returns (GetArtifactTrailResponse);
// Walk the whole hash chain of the audit log and name the first break
// (holger-audit-4 in break_reason).
rpc VerifyAuditChain(VerifyAuditChainRequest) returns (VerifyAuditChainResponse);
// "What did C download": every DOWNLOAD record, by who (ident) and/or by
// repository, inside a time range — the newest `limit` of them, in log order,
// each naming its place in the hash chain (stinky SB #19).
rpc QueryDownloads(QueryDownloadsRequest) returns (QueryDownloadsResponse);
// Filter and page every record (lane HOL-AUDIT): by actor, repository,
// artifact prefix, action, source address, status and time range, oldest
// first, read from the Arrow index the chain keeps current.
rpc QueryAudit(AuditQueryRequest) returns (AuditQueryResponse);
// Count the records: per repository, action and day by default, or by any of
// repo, action, day, ident, client, status, under the same filter.
rpc AggregateAudit(AuditAggregateRequest) returns (AuditAggregateResponse);
// Hand the selected records out as ONE Arrow IPC file (the index schema), for a
// reader's own engine. Capped per call; page with since_seq.
rpc ExportAudit(AuditExportRequest) returns (AuditExportResponse);
}
// Search service — cross-repo artifact discovery (parity §H, .nornir/search-design.md).
// A thin, read-only aggregate over the backends' `list()` + `archive_files()`:
// find artifacts across every configured repository by name, GAVC-style
// coordinate (namespace/group · name · version), sha256 content checksum, or a
// raw archive path substring. Reads are open (anonymous), like the other read
// RPCs; also exposed as a read-only HTTP door (`GET /-/search`).
service SearchService {
// Search artifacts across all (or a named subset of) repositories.
rpc Search(SearchRequest) returns (SearchResponse);
}
// SBOM service — host + serve SBOM documents as first-class servable artifacts
// (parity §6/§11, .nornir/sbom-hosting-design.md). An SBOM (CycloneDX 1.5, built
// from modgunn's shared SBOM model) is ATTACHED to an artifact coordinate
// (repository + namespace/name/version) and served back byte-identical. Attach is
// a write (writer/admin, fail-closed under a configured role policy); fetch is an
// open read, also exposed as a read-only HTTP door (`GET /-/sbom`).
service SbomService {
// Attach (host) an SBOM document for a coordinate. Write-gated; last-wins.
rpc AttachSbom(AttachSbomRequest) returns (AttachSbomResponse);
// Fetch the hosted SBOM document for a coordinate (NOT_FOUND when none).
rpc FetchSbom(FetchSbomRequest) returns (FetchSbomResponse);
}
// Property service — set/get custom artifact PROPERTIES (parity §8,
// .nornir/properties-design.md). A property (`key` → values) is attached to an
// artifact coordinate and drives the `property` search axis (SearchRequest).
// SetProperty is a WRITE (writer/admin, fail-closed under a configured role
// policy, scoped to the artifact's repository); GetProperties is an open read,
// mirrored by the read-only `GET /-/properties` HTTP door + the `holger-server
// properties` CLI verb. All share one on-disk store.
service PropertyService {
// Set `key`'s values on a coordinate (last-wins per key). An EMPTY `values`
// REMOVES the key. Returns the resulting full property map. Write-gated.
rpc SetProperty(SetPropertyRequest) returns (SetPropertyResponse);
// Read all properties on a coordinate (empty map when none). Open read.
rpc GetProperties(GetPropertiesRequest) returns (GetPropertiesResponse);
}
// Replication service — copy a repository's artifacts from a SOURCE into a
// DESTINATION (parity §I, .nornir/replication-design.md). Within one running
// server this replicates every artifact from `from_repo` into a writable
// `to_repo`, CONTENT-ID IDEMPOTENT: an artifact the destination already holds by
// content id (sha256) is skipped, so a re-run over an unchanged source is a
// no-op. Reads only from the source, writes only into the destination. Admin-
// gated on the destination (like promotion), fail-closed. The `holger-server
// replicate --from --to` CLI verb drives the same engine across two SEPARATE
// file-backed stores.
service ReplicationService {
// Replicate from `from_repo` into `to_repo` (dry-run unless `execute`).
rpc Replicate(ReplicateRequest) returns (ReplicateResponse);
}
// ─────────────────────────────────────────────────────────────────────────────
// Migration service — the ONE-TIME move of an existing Nexus or Artifactory
// registry INTO this holger.
//
// This is the first question every operator with an incumbent registry asks,
// and until now the answer was a two-step through a `.znippy` file on a disk.
// It is deliberately NOT a mirror: a migration runs once and finishes. A
// standing, continuously-converging relationship is a different feature with a
// different shape and must not be grown out of this one.
//
// ── Why this is FOUR RPCs and not one ────────────────────────────────────────
//
// `ReplicationService.Replicate` next door is ONE blocking unary RPC with a
// dry-run flag, and that shape is right for what it does: it copies between two
// repositories inside this server, so it is local, bounded and quick. A
// migration pulls someone else's entire registry across a network. It runs for
// HOURS. A single blocking RPC over that would mean a gRPC deadline (or any
// proxy in between) deciding when the migration ends, a UI with nothing to show
// but a spinner, no way to stop it short of killing the server, and a client
// disconnect silently abandoning a half-moved registry with nobody able to say
// how far it got. So the long operation is split from the call that starts it:
//
// Discover — the PREVIEW. Connect, list repositories, count assets, sum the
// advertised sizes, and say exactly which holger repositories
// would be CREATED. It reads no artifact content, so previewing a
// terabyte costs a handful of requests. This is what a UI shows
// before anything is committed, and it is the dry run.
// Start — hands back a job id IMMEDIATELY and returns. It does not block.
// Watch — a server stream of progress rows (repo, done, failed). Any
// number of watchers, joining at any time; a watcher that drops
// does not touch the job.
// Cancel — stops admitting new transfers. In-flight ones are allowed to
// finish, because tearing down a half-written upload is how a
// store gets a truncated artifact.
//
// ADMIN-gated, fail-closed, like every other mutating service here.
//
// ── Re-running is the recovery path ──────────────────────────────────────────
//
// Every asset is checked against the destination BEFORE it is fetched, keyed on
// its path and the sha256 the SOURCE published. So an interrupted or cancelled
// migration is resumed by starting it again: what landed is skipped, what did
// not is fetched, and a completed migration re-run is an all-skips no-op.
// ─────────────────────────────────────────────────────────────────────────────
service MigrationService {
// Preview: what a migration WOULD do. Reads no artifact content.
rpc Discover(DiscoverMigrationRequest) returns (DiscoverMigrationResponse);
// Begin a migration. Returns a job id immediately; does NOT block.
rpc Start(StartMigrationRequest) returns (StartMigrationResponse);
// Follow a running (or finished) migration.
rpc Watch(WatchMigrationRequest) returns (stream MigrationProgressEvent);
// Stop admitting new transfers in a running migration.
rpc Cancel(CancelMigrationRequest) returns (CancelMigrationResponse);
}
// ─── Messages ────────────────────────────────────────────────────────
message ArtifactId {
string namespace = 1; // e.g. groupId for Maven, empty for Rust
string name = 2;
string version = 3;
}
message FetchArtifactRequest {
string repository = 1;
ArtifactId id = 2;
}
message FetchArtifactResponse {
bytes data = 1;
string content_type = 2;
int64 size_bytes = 3;
}
message ArtifactChunk {
bytes data = 1;
}
message ListArtifactsRequest {
string repository = 1;
string name_filter = 2; // optional substring filter
int32 limit = 3;
string page_token = 4;
}
// One row of a repository listing.
//
// ── Fields 4, 5 and 6 are `optional`, and that word is load-bearing ────────────
//
// A proto3 scalar has no field presence: a peer that does not send `sha256`
// and a peer that sends `sha256 = ""` are indistinguishable on the wire, and a
// `published_unix_ms` of 0 arrives looking exactly like a real answer of
// 1970-01-01T00:00:00Z. Both of those would be a FABRICATED value dressed as a
// measurement — an empty digest compares equal to another empty digest, and a
// zero timestamp sorts as the oldest thing in the repository.
//
// The `optional` keyword restores explicit presence (proto3 field presence;
// prost renders these as `Option<i64>` / `Option<String>`), so "this publisher
// does not know" is a distinct state on the wire and not a value at all.
//
// Wire compatibility, both directions:
// * an OLD server answering a NEW client sends neither field; the client
// reads `None` = unknown, which is the truth about an old server.
// * a NEW server answering an OLD client sends two fields the old client
// does not know; proto3 drops unknown fields, and the old client sees
// exactly the listing it saw before.
// Neither peer has to be upgraded first, and nothing here is an invented value.
//
// Belt and braces on top of presence: a receiver still treats a
// `published_unix_ms` that is not strictly positive, and a `sha256` that is not
// 64 hex characters, as unknown — so even a hand-rolled client that emits the
// proto3 zero values cannot claim 1970 or an empty hash. See
// `holger_traits::normalize_published_unix_ms` / `normalize_sha256`.
//
// This is the same reasoning as `StartMigrationRequest.skip_digest_verification`
// next door, which is INVERTED on the wire so that the proto3 zero value is the
// safe one. Here the safe state is "unknown", and presence says it outright
// rather than by inversion.
message ArtifactEntry {
ArtifactId id = 1;
int64 size_bytes = 2;
string content_type = 3;
// When these bytes were published, Unix milliseconds UTC.
// ABSENT = the serving backend does not know. Never guessed, never 0.
optional int64 published_unix_ms = 4;
// Lowercase-hex SHA-256 of the artifact bytes, 64 chars, no `sha256:` prefix.
// ABSENT = the serving backend does not know, and in particular does NOT mean
// the bytes are trusted — the same rule `RemoteAsset.sha256` states. A
// backend that would have to read the artifact to answer leaves it absent
// rather than turning a listing into a full read of the repository.
optional string sha256 = 5;
// An OPAQUE CHANGE TOKEN for these bytes, scoped to this server+repository.
//
// Equal ⇒ unchanged HERE. Different ⇒ changed HERE. It is not a content
// digest and it is not comparable with one; nor is it comparable with a
// token from any other source, because two sources holding byte-identical
// artifacts legitimately publish different tokens (holger's binds the
// archive-relative path, exactly as znippy's own provenance digest does).
// A consumer stamps its own source scope onto the value before it stores or
// compares it — see `holger_traits::scope_change_token`.
//
// This is the ONE provenance question a znippy-backed repository can answer
// cheaply: the archive index already carries a BLAKE3 per chunk, so a
// per-entry fold of them reads no artifact byte. 15 of 17 backends are
// znippy-backed, and without this every one of them answered `None` to both
// fields above, landed in the mirror's `unproven` bucket on every tick, and
// re-fetched its whole repository every 300 s under the default policy.
//
// ABSENT = the serving backend does not know. Never guessed, never "".
optional string change_token = 6;
}
message ListArtifactsResponse {
repeated ArtifactEntry artifacts = 1;
string next_page_token = 2;
}
message PutArtifactRequest {
string repository = 1;
ArtifactId id = 2;
bytes data = 3;
// ASYNC SHAPE -- see `PromoteArtifactRequest.async_job`. Unset/false writes
// the bytes into the backend on this RPC, as always. Set, the FINALIZE half
// (the backend write, which for a znippy repo is an index re-seal and for a
// proxy an outbound round-trip) becomes a job: the body is staged and the
// answer is a job id. The INGEST GATES still run here, before admission --
// making an upload asynchronous must not make it ungated.
bool async_job = 4;
}
// One piece of a `PutArtifactStream` upload. `repository`, `id` and
// `async_job` are read from the FIRST message only; `data` from every one.
message PutArtifactChunk {
string repository = 1;
ArtifactId id = 2;
bytes data = 3;
bool async_job = 4;
}
message PutArtifactResponse {
bool success = 1;
string message = 2;
// Set ONLY when the request asked for the async shape. Empty otherwise.
string job_id = 3;
}
message ListArchiveFilesRequest {
string repository = 1;
string prefix = 2; // optional path prefix filter
}
message ListArchiveFilesResponse {
repeated string paths = 1;
int64 total_files = 2;
}
message ArchiveInfoRequest {
string repository = 1;
}
message ArchiveInfoResponse {
int64 file_count = 1;
int64 total_uncompressed_bytes = 2;
string archive_path = 3;
}
message HealthRequest {}
message HealthResponse {
string status = 1; // "ok"
string version = 2;
int64 uptime_seconds = 3;
}
message ListRepositoriesRequest {}
message RepositoryInfo {
string name = 1;
string repo_type = 2; // "rust", "maven3"
bool writable = 3;
bool has_archive = 4;
// Whether `RepositoryService.ListArtifacts` on this repository is a REAL
// enumeration (`RepositoryBackendTrait::supports_listing`). A counter that
// ignores this reads a full repository as empty; a consumer that meters by
// artifact count MUST refuse, by name, while this is false.
bool supports_listing = 5;
// A GROUP repository is a read-only VIEW over `members`: it owns no storage,
// and its `ArchiveInfo` is the SUM of its members'. A fleet total that adds a
// group to its members counts those bytes twice — skip every `is_group`.
bool is_group = 6;
// The member repository names of a group, in resolution order. Empty for a
// repository that owns its own storage.
repeated string members = 7;
// A SYSTEM repository of this estate (floored at Admin, e.g. the sealed
// `monetize-backup`): the estate's own, its name taken, never a tenant a
// buyer pays for. Set by `ListRepositories` only; false elsewhere.
bool system = 8;
}
message ListRepositoriesResponse {
repeated RepositoryInfo repositories = 1;
}
// Confer the TWIN role on this box. See AdminService.BecomeTwin.
message BecomeTwinRequest {
// The primary this box will follow, as an address a client would use
// ("grpc.holger.rs:50051"). MANDATORY: a twin that names no primary could
// neither refuse a write by name nor follow anything.
string primary = 1;
// Seconds between follow cycles. 0 = the server's default.
uint64 poll_seconds = 2;
// The MEASUREMENT that decided this election, in the caller's own words
// ("2 cores / 1 GB / 20 GB hdd vs 2 / 4 / 20 maxiops"). Recorded verbatim:
// this record is read by an operator on the worst day, and a number with no
// units is not a reason.
string reason = 3;
}
message BecomeTwinReply {
// Unix seconds the election was recorded at.
uint64 elected_at = 1;
// The authenticated identity that elected it — the server's own reading of
// the credential, not anything the caller said about itself.
string elected_by = 2;
// True: the LOOPBACK BIND of the public doors waits for the next start,
// because doors are bound at wiring time and a running server does not
// rebind them underneath its clients. The role itself is live — see
// `applied_live`.
bool restart_required = 3;
// True when every door refuses writes by name from this reply on and the
// follow has started, without a restart (stinky 14 §P3).
bool applied_live = 4;
}
// Which credential slot of THIS box a pairing step is about: "twin" (the
// twin following its primary; the default when empty) or "recharge" (a fresh
// main reading its twin).
message TwinCsrRequest {
string purpose = 1;
}
message TwinCsrReply {
// PKCS#10, PEM. Its key was minted on this box and never leaves it; asked
// again, the same request.
string csr_pem = 1;
// The CN it carries — the principal the source must issue for.
string principal = 2;
}
message IssueTwinCertificateRequest {
// `holger-twin` or `holger-recharge`; the request's own CN must be this.
string principal = 1;
string csr_pem = 2;
}
message IssueTwinCertificateReply {
// The client leaf: CN = principal, clientAuth only, 365 days.
string cert_pem = 1;
// This box's replication issuer — what the reader pins.
string issuer_pem = 2;
// The replication door's port and the addresses it answers on, in order.
uint32 port = 3;
repeated string hosts = 4;
// notAfter, Unix seconds.
int64 not_after = 5;
}
message InstallTwinCertificateRequest {
string purpose = 1;
string cert_pem = 2;
string issuer_pem = 3;
uint32 port = 4;
repeated string hosts = 5;
// RE-POINT (purpose "twin" only): the primary this twin followed is DEAD and
// a fresh main was recharged from it — a main with an issuer of its own (an
// issuer is never replicated). Every install check but the pin runs; the new
// issuer is pinned and the twin's role record names `primary` from now on.
// Admin and explicit: never a courier's retry.
bool repoint = 6;
string primary = 7;
}
message FencePrimaryRequest {
// The epoch the order names (0 = a local fence).
uint64 epoch = 1;
}
message FencePrimaryReply {
// The box is fenced now: every door refuses writes.
bool fenced = 1;
// The epoch it is fenced at.
uint64 epoch = 2;
// The write refusal its doors answer from now on.
string refusal = 3;
}
message RechargeStatusRequest {}
message RechargeStatusReply {
// "fresh" (holds nothing of its own — a recharge may start), "restoring" (a
// recharge began and is not whole — it resumes), "recharged" (done, once),
// "holding" (state of its own: a re-image makes it fresh).
string state = 1;
bool fresh = 2;
bool restoring = 3;
bool recharged = 4;
// False while the box refuses to serve (holger-twin-15).
bool serving = 5;
// What makes the box not fresh, by path; empty when fresh.
string held = 6;
// The source a pending recharge waits for, when one is declared.
string awaiting_from = 7;
// This box's control plane as a client dials it (its gRPC door; an
// unspecified bind is named by its replication door's first address) — what
// a twin re-pointed at it names as its primary.
string control = 8;
}
message TwinStatusRequest {}
message TwinStatusReply {
// "twin" or "primary".
string role = 1;
// The primary it follows (a twin).
string primary = 2;
// What it holds: estate files, and artifacts over every repository.
uint64 estate_files = 3;
uint64 artifacts = 4;
// True when its last follow cycle that REACHED its primary was whole — a dead
// primary makes every later cycle fail to connect, and that is not a loss.
bool complete = 5;
// Why not complete, in words; empty when complete.
string why = 6;
// Unix seconds of the last whole cycle; 0 = never.
int64 last_whole_unix = 7;
// How it last followed ("mtls <door>" / "plain <endpoint>").
string follow = 8;
// ★ What it last HEARD of its primary — the fence gate's evidence (A7 row 4,
// Gunnar R-13 fence before recharge): Unix ms of the last follow cycle that
// reached it (0 = never), how long ago that was on the TWIN's clock, what the
// primary listed then (estate files; 0 = an empty primary), and the twin's
// word: "following", "unreachable" (its last cycle did not reach it) or "idle".
int64 primary_answer_unix_ms = 9;
uint64 primary_answer_ago_ms = 10;
uint64 primary_head = 11;
string follow_state = 12;
}
message InstallTwinCertificateReply {
int64 days_left = 1;
// True when this replaced a certificate from the SAME issuer.
bool renewal = 2;
}
message RechargeRequest {}
message RechargeReply {
// The estate line, one line per repository, and what failed (empty = whole;
// the `holger-recharged` marker is written only then).
string estate = 1;
repeated string artifacts = 2;
repeated string failed = 3;
}
message FlowSvgRequest {
// The zoom, as `GET /-/flow.svg?open=` names it (HOL-FLOW-SMART): the machines to unfold,
// comma-separated, by id (`estate:main`) or short name (`main`, `twin`, a downstream's
// name). Empty is level 1 (the Overview: the machines and the upstream sources); some is
// level 2 (the Flow: those machines unfolded in place, with their part of the RON); `all`
// is level 3, the full RON.
string open = 1;
}
// The whole drawing, `image/svg+xml`, UTF-8, self-contained (no script, no external
// reference), and the model it was drawn from, JSON.
message FlowSvgReply {
string svg = 1;
string graph_json = 2;
// Where the drawing is clickable (HOL-FLOW-SMART): each machine that unfolds or folds,
// and the full-RON link. A console showing the drawing as an image (which follows no
// link inside it) lays a plain link over each; it computes nothing.
repeated FlowLink links = 3;
}
// One clickable place of the flow drawing, as the share (0..1) of the drawing's width and
// height it covers, and the relative link it follows (`flow?open=main`).
message FlowLink {
string href = 1;
// What following it does: `unfold main`, `fold twin`, `show the full RON`.
string label = 2;
double left = 3;
double top = 4;
double width = 5;
double height = 6;
}
message CapacityRequest {}
// One measurement of the filesystem under `path` (the data root).
message CapacityReply {
// The filesystem size: f_blocks * f_frsize.
uint64 total_bytes = 1;
// SERVABLE: what an unprivileged writer (holger) may still put there —
// f_bavail * f_frsize, i.e. without the root reserve. Not f_bfree.
uint64 available_bytes = 2;
// The sum of `ArchiveInfo.total_uncompressed_bytes` over every repository
// that owns storage. Groups are excluded (their info is a sum of members).
uint64 used_by_holger_bytes = 3;
// The data root the measurement was taken over.
string path = 4;
// ── the GROWTH VERDICT (2026-09-27, additive) ──
// Can this data set ever hold more than it does, and by whose hand — edda
// appliance-grow's ladder over MEASURED facts (the journal on the box, this
// build's engine, the kernel's PCI hot-plug, PID 1). Reported on every
// answer: "yes, and this is the last capacity" is a different sale. An OLD
// holger leaves it 0 (unspecified), which a reader must take as unknown.
SetGrowth growth = 5;
// The clause that says it, in words (an attached disk that is billed and
// invisible is said in those words).
string growth_reason = 6;
// What `path` sits on: `4×75G raid0 xfs`; empty when it is not a mount of its own.
string data_set = 7;
// The running kernel has PCI hot-plug (a runtime growth needs it).
bool hotplug = 8;
// ── the rest of the ELECTION's measurement (stinky SB #37, additive) ──
// The installer elects the weakest box the twin on cores, then RAM, then data
// room (nordisk-pairing `elect::weakest`); a capacity that answered the disk
// alone could not say which box that is. An OLD holger leaves both 0.
// CPUs this process may run on (available_parallelism).
uint32 cpu_cores = 9;
// MemTotal of /proc/meminfo, bytes.
uint64 ram_bytes = 10;
}
// gunnar's SetGrowth, the same numbers — monetize's `Growth` reads both.
enum SetGrowth {
SET_GROWTH_UNSPECIFIED = 0;
GROWTH_UNKNOWN = 1;
GROWTH_SEALED = 2;
GROWTH_OPERATOR_ONLY = 3;
GROWTH_AT_RESTART = 4;
GROWTH_AT_RUNTIME = 5;
}
message ServerProfileRequest {}
message PromoteArtifactRequest {
string from_repo = 1; // source, mutable DEV repository
string to_repo = 2; // target, immutable PROD repository
ArtifactId artifact = 3;
// ASYNC SHAPE (2026-09-21, stinky 01 §1.3), purely ADDITIVE: unset/false is
// the original synchronous promotion, byte for byte -- the seal runs on this
// RPC and the response carries `content_address`. Set true and the server
// ADMITS the promotion to its durable job queue and answers at once with
// `job_id`; the seal happens on the queue's gatling pool and is read back
// through `JobService.GetJob`. A server built WITHOUT the `jobs` feature
// refuses a true here BY NAME rather than silently promoting inline.
bool async_job = 4;
}
message PromoteArtifactResponse {
bool success = 1;
string message = 2;
string content_address = 3; // blake3 hex of the promoted bytes
string promoted_by = 4; // identity that performed the promotion
// Set ONLY when the request asked for the async shape: the admitted job's id.
// Empty on every synchronous promotion, so an old client sees no change.
string job_id = 5;
}
// ─── The intake chain's wire verb (separation of duties) ────────────
// What AdvanceArtifact does. ADVANCE moves to the next state on the chain (or
// to `to_state` when set, which must be the next state).
enum IntakeVerb {
INTAKE_VERB_ADVANCE = 0;
INTAKE_VERB_QUARANTINE = 1;
INTAKE_VERB_RELEASE = 2;
INTAKE_VERB_BLOCK = 3;
}
// One evidence slot: kind is scan | malware | license | provenance | approval |
// other; verdict is pass | warn | fail. A caller never supplies `approval`:
// the server mints it from the authenticated approver (holger-duty-7).
message IntakeEvidence {
string kind = 1;
string source = 2;
string verdict = 3;
string reference = 4;
}
message AdvanceArtifactRequest {
ArtifactId artifact = 1;
IntakeVerb verb = 2;
// Optional: the state ADVANCE is meant to reach (refused unless it is the next one).
string to_state = 3;
string reason = 4;
repeated IntakeEvidence evidence = 5;
}
message AdvanceArtifactResponse {
string from_state = 1;
// The state after the call. Equal to from_state when an approval vote was
// recorded short of the quorum.
string state = 2;
// Distinct approvals now recorded for the APPROVED hop, and how many it needs.
uint32 approvals = 3;
uint32 approvals_required = 4;
// The evidence the transition carried (the approval slots on APPROVED).
repeated IntakeEvidence evidence = 5;
string message = 6;
string sha256 = 7;
// The authenticated principal the server acted for.
string actor = 8;
}
message GetArtifactStateRequest {
ArtifactId artifact = 1;
}
message IntakeTransition {
string from_state = 1;
string to_state = 2;
string actor = 3;
int64 at = 4;
repeated IntakeEvidence evidence = 5;
string policy = 6;
string reason = 7;
}
message GetArtifactStateResponse {
string state = 1;
string sha256 = 2;
// The principal that ingested the artifact; it never approves it.
string importer = 3;
uint32 approvals = 4;
uint32 approvals_required = 5;
repeated IntakeTransition history = 6;
// Approval votes recorded short of the quorum.
repeated IntakeEvidence pending_approvals = 7;
// The source the artifact came from (tier 1 `sources:`); empty on a chain
// declared with the old `promotion:` spelling.
string source = 8;
// The artifact's effective policy (the floor plus its source's gates), as JSON:
// each rule with who set it (`floor`, `steps.gates`, `source '<name>'`).
string effective_policy = 9;
}
message ListIntakeArtifactsRequest {
// The queue whose rows to return: incoming | quarantined |
// awaiting-approval | blocked | available. Empty returns the counts only.
string queue = 1;
// At most this many rows (0 = the server's default, 200).
uint32 limit = 2;
}
// One queue of the security dashboard, as the server groups the states.
message IntakeQueue {
// The stable name: incoming | quarantined | awaiting-approval | blocked | available.
string name = 1;
// The words an operator reads: "Awaiting approval".
string title = 2;
// The intake states the queue holds, by name.
repeated string states = 3;
// The artifacts in the queue that the caller may read.
uint64 count = 4;
}
// One intake artifact where it stands now.
message IntakeArtifactRow {
ArtifactId artifact = 1;
string state = 2;
string sha256 = 3;
// The intake repository it stands in.
string repository = 4;
// The principal of the last transition, and when (Unix seconds).
string actor = 5;
int64 at = 6;
// The state a QUARANTINED artifact was held at.
string held_from = 7;
string importer = 8;
// Approval votes recorded short of the quorum, and the quorum.
uint32 approvals = 9;
uint32 approvals_required = 10;
// The policy's words and the reason of the last transition.
string policy = 11;
string reason = 12;
}
message ListIntakeArtifactsResponse {
repeated IntakeQueue queues = 1;
// The rows of the queue asked for.
repeated IntakeArtifactRow artifacts = 2;
// True when the queue held more rows than `limit`.
bool truncated = 3;
}
message GetArtifactEvidenceRequest {
ArtifactId artifact = 1;
}
message EvidenceHash {
// sha256 | blake3.
string alg = 1;
string hex = 2;
}
message EvidenceFinding {
string id = 1;
// info | low | medium | high | critical.
string severity = 2;
string summary = 3;
}
// One scanner's evidence about the artifact.
message EvidenceRecord {
string scanner_id = 1;
string scanner_version = 2;
// inspection | malware | vulnerability | license | secrets | provenance | sbom.
string kind = 3;
// pass | warn | block | error.
string verdict = 4;
repeated EvidenceFinding findings = 5;
repeated EvidenceHash inputs = 6;
string detail = 7;
// The operator requires this kind; an error from it blocks.
bool required = 8;
// No scanner is installed in this slot.
bool not_configured = 9;
// A document the scanner produced (the CycloneDX SBOM), as JSON. Empty when none.
string document_json = 10;
}
message GetArtifactEvidenceResponse {
// The intake state and the sha256 recorded at ingest.
string state = 1;
string sha256 = 2;
// False when no scan has recorded evidence for these bytes yet.
bool scanned = 3;
string file_name = 4;
string ecosystem = 5;
uint64 size = 6;
// The hashes of the bytes the scanners saw.
repeated EvidenceHash hashes = 7;
// The fold over every record: pass | warn | block | error.
string verdict = 8;
// One sentence per reason the verdict is not pass.
repeated string reasons = 9;
repeated EvidenceRecord records = 10;
// When the scan ran, Unix seconds.
int64 scanned_at = 11;
// The policy's words on the last transition.
string policy = 12;
// W5: every stored record about these bytes, oldest scan first, each with the
// envelope that signs it.
repeated ArtifactEvidenceRecord stored = 13;
// The attestation signer's certificate (DER) the envelopes verify against.
bytes signer_cert = 14;
// `/-/intake/sbom?sha256=<hex>` when the newest record carries an SBOM.
string sbom_link = 15;
}
// ── the dependency sets (PromotionService.*DependencySet*) ──
message ListDependencySetsRequest {
// Only the sets in this status (resolving | held | refused | awaiting-approval |
// approved | available); empty: every set the caller may read.
string status = 1;
}
message ListDependencySetsResponse {
// Newest first.
repeated DependencySet sets = 1;
// How many distinct approvers each member needs.
uint32 approvals_required = 2;
}
message GetDependencySetRequest {
string id = 1;
}
message JudgeDependencySetRequest {
string id = 1;
// Why: kept with every vote; a refusal names it on every blocked member (required).
string reason = 2;
// The `revision` of the set as the caller was shown it (SEC-REVIEW D4, required): a vote
// naming none, or another than the set has now, is refused (FAILED_PRECONDITION,
// holger-depset-4) and nothing is cast.
string revision = 3;
}
message JudgeDependencySetResponse {
string id = 1;
// Where the set stands after the call.
string status = 2;
// The members this call voted on (approve) or blocked (refuse).
uint32 moved = 3;
string message = 4;
DependencySet set = 5;
}
// A member or a coordinate, and why it refuses or holds a set.
message DependencySetReason {
string name = 1;
string why = 2;
}
// One member of a set, as an approver is shown it.
message DependencySetMember {
// The chain coordinate (`pypi/dep-1.0-py3-none-any.whl`).
string name = 1;
// requested | parent | bom | descriptor | dependency.
string role = 2;
// What needed it.
string via = 3;
// The intake state (POLICY_CHECKED …), or not-in-chain.
string state = 4;
string sha256 = 5;
// The votes cast on its current bytes, and by whom.
uint32 approvals = 6;
repeated string approvers = 7;
// Why it is refused or held, when it is.
string reason = 8;
// Advisories recorded as a warning.
repeated string warnings = 9;
}
// One dependency set: what a pull brought in, judged as one.
message DependencySet {
string id = 1;
// The pull-through source.
string source = 2;
// maven | npm | crates | python.
string ecosystem = 3;
// What was asked for, in the ecosystem's words (`app==1.0`).
string root = 4;
// When the pull ran, Unix seconds.
int64 at = 5;
// resolving | held | refused | awaiting-approval | approved | available.
string status = 6;
// The words for the status.
string summary = 7;
repeated DependencySetMember members = 8;
// The members that refuse the set.
repeated DependencySetReason blockers = 9;
// What holds the set.
repeated DependencySetReason holds = 10;
uint32 approvals_required = 11;
// The approvers who voted on every member still waiting.
repeated string approvers = 12;
// Members waiting at POLICY_CHECKED, and members AVAILABLE.
uint32 awaiting = 13;
uint32 available = 14;
repeated string notes = 15;
// What a vote names (SEC-REVIEW D4): a hash over the members and their sha256 as shown.
string revision = 16;
}
// One stored scan-evidence record (W5).
message ArtifactEvidenceRecord {
// The blake3 of the record's bytes: its name in the evidence store.
string id = 1;
// When the scan ran, Unix seconds.
int64 scanned_at = 2;
// The fold over every scanner: pass | warn | block | error.
string verdict = 3;
repeated string reasons = 4;
// The record itself (holger.scan-evidence/v1 JSON), byte-identical to the store.
bytes evidence_json = 5;
// The DSSE envelope (in-toto, predicate https://holger.rs/attestation/scan-evidence/v1)
// that verifies to this record; empty when the record is not signed.
bytes envelope = 6;
// Empty when a scan now would agree; otherwise why it is stale (a newer
// scanner version or intel database sequence).
string stale = 7;
}
// ─── AuditService messages ───────────────────────────────────────────
message GetArtifactTrailRequest {
// Lowercase-hex SHA-256 of the artifact's bytes (a `sha256:` prefix is allowed).
string sha256 = 1;
}
message ArtifactTrailEntry {
// The record's sequence number and chain hash in the audit log.
uint64 seq = 1;
string hash = 2;
int64 ts_nanos = 3;
// imported | hash_verified | scanned | policy_passed | policy_denied |
// approved | sealed | exported | airgap_imported
string kind = 4;
string ident = 5;
string repo = 6;
string artifact = 7;
uint32 status = 8;
string detail = 9;
}
message GetArtifactTrailResponse {
string sha256 = 1;
repeated ArtifactTrailEntry entries = 2;
// Whether the whole audit chain verified when the trail was read.
bool chain_intact = 3;
string chain_summary = 4;
}
message QueryDownloadsRequest {
// Who downloaded (the authenticated principal; `anonymous` for open reads).
// Empty = anybody.
string ident = 1;
// The repository. Empty = every repository.
string repo = 2;
// From (inclusive) and to (exclusive), nanoseconds since the Unix epoch.
// Zero = open.
int64 since_nanos = 3;
int64 until_nanos = 4;
// At most this many rows, the NEWEST that match. Zero = 1000; capped at 10000.
uint64 limit = 5;
}
message DownloadRow {
uint64 seq = 1;
string hash = 2;
int64 ts_nanos = 3;
string ident = 4;
string repo = 5;
string artifact = 6;
string source_ip = 7;
uint32 status = 8;
uint64 bytes = 9;
string sha256 = 10;
}
message QueryDownloadsResponse {
repeated DownloadRow rows = 1;
// How many rows matched in all, and whether the limit left some out.
uint64 matched = 2;
bool truncated = 3;
}
// Which records: every field optional, set fields AND together.
message AuditFilter {
// The actor (authenticated principal; "anonymous" for open reads).
string ident = 1;
string repo = 2;
// A PREFIX of the artifact: "acme/" a namespace, "acme/widget@" every version.
string artifact_prefix = 3;
// download | upload | delete | list | promote | entitlement | purge | scan |
// token | other. Any other word is refused.
string action = 4;
// The client address; without a port it matches every port of the address.
string source_ip = 5;
// The exact status. 0 = any.
uint32 status = 6;
// From (inclusive) and to (exclusive), nanoseconds since the Unix epoch. 0 = open.
int64 since_nanos = 7;
int64 until_nanos = 8;
}
message AuditRecordRow {
uint64 seq = 1;
string hash = 2;
int64 ts_nanos = 3;
string ident = 4;
string action = 5;
string repo = 6;
string artifact = 7;
string source_ip = 8;
uint32 status = 9;
uint64 bytes = 10;
string detail = 11;
string sha256 = 12;
string trail = 13;
}
message AuditQueryRequest {
AuditFilter filter = 1;
// Matching records to skip; pass back next_offset.
uint64 offset = 2;
// Page size. 0 = 100; capped at 1000 (limit_applied says what was used).
uint32 limit = 3;
}
message AuditQueryResponse {
// The page, oldest first.
repeated AuditRecordRow records = 1;
// Every match before paging: the number to show.
uint64 matched = 2;
uint64 next_offset = 3;
// Matches exist beyond this page.
bool more = 4;
uint64 scanned = 5;
uint32 limit_applied = 6;
}
message AuditAggregateRequest {
AuditFilter filter = 1;
// repo | action | day | ident | client | status. Empty = repo, action, day.
repeated string group_by = 2;
// Only 2xx statuses (what was served, not what was refused).
bool ok_only = 3;
// At most this many groups (the first in key order). 0 = 10000.
uint32 max_groups = 4;
}
message AuditAggregateRow {
// The keys not grouped on are empty (status 0).
string repo = 1;
string action = 2;
// YYYY-MM-DD, UTC.
string day = 3;
string ident = 4;
// The client address without its port. An address is not an identity.
string client = 5;
uint32 status = 6;
uint64 count = 7;
uint64 bytes = 8;
int64 first_ts_nanos = 9;
int64 last_ts_nanos = 10;
}
message AuditAggregateResponse {
repeated string group_by = 1;
repeated AuditAggregateRow rows = 2;
bool groups_truncated = 3;
uint64 rows_counted = 4;
uint64 scanned = 5;
}
message AuditExportRequest {
AuditFilter filter = 1;
// Only rows with seq >= since_seq (the next page: next_seq of the last reply).
uint64 since_seq = 2;
// At most this many rows, the oldest that match. 0 = 50000; capped at 50000.
uint32 limit = 3;
}
message AuditExportResponse {
// One Arrow IPC file.
bytes arrow_ipc = 1;
uint64 rows = 2;
bool truncated = 3;
uint64 next_seq = 4;
// "holger.audit.index/v1" and the schema, `name:type` per column.
string layout = 5;
string schema = 6;
}
message VerifyAuditChainRequest {}
message VerifyAuditChainResponse {
bool intact = 1;
uint64 records = 2;
uint64 checkpoints = 3;
uint64 signatures_verified = 4;
uint64 signatures_unchecked = 5;
uint64 unsigned_checkpoints = 6;
// The hash of the last line that verified.
string head = 7;
// The first break: 1-based line, its seq, and why. Zero / empty when intact.
uint64 break_line = 8;
uint64 break_seq = 9;
string break_reason = 10;
string summary = 11;
}
// ─── SearchService messages ──────────────────────────────────────────
message SearchRequest {
// Case-insensitive substring match on the artifact NAME (Maven artifactId,
// crate name, …). Empty = any name.
string name = 1;
// Case-insensitive substring match on the NAMESPACE (Maven groupId, npm
// scope). Empty = any / no namespace constraint.
string namespace = 2;
// Exact match on the artifact VERSION. Empty = any version.
string version = 3;
// Lowercase-hex sha256 of the artifact CONTENT (exact, case-insensitive).
// Setting this forces the server to hash candidate artifacts' bytes, so it is
// the one expensive axis — leave empty unless you are searching by digest.
string checksum = 4;
// Case-insensitive substring match on a RAW archive file path. Produces path
// hits (see `paths`), independent of the artifact axes above.
string path = 5;
// Restrict the search to these repository names. Empty = every configured repo.
repeated string repositories = 6;
// Cap the number of hits returned (per hit kind). 0 ⇒ a server default.
int32 limit = 7;
// Custom-PROPERTY filters (AND-composed with each other and the axes above). An
// artifact matches only if it carries every listed property. Empty = no
// property constraint. Requires the server to have a property store configured;
// with none, a property filter matches nothing (fail-closed).
repeated PropertyMatch properties = 8;
}
// One custom-property filter: the artifact must carry `key`, and — when `value`
// is non-empty — that exact value among the key's values (empty `value` = "the
// key is present, any value").
message PropertyMatch {
string key = 1;
string value = 2;
}
// One artifact matched by name / coordinate / checksum, tagged with the repo it
// was found in.
message ArtifactHit {
string repository = 1;
ArtifactId id = 2;
int64 size_bytes = 3;
string content_type = 4;
// sha256 hex of the content — set ONLY when a checksum query forced a hash
// (empty otherwise, to keep the common name/coordinate search cheap).
string checksum = 5;
}
// One raw archive path matched by the `path` axis, tagged with its repo.
message PathHit {
string repository = 1;
string path = 2;
}
message SearchResponse {
repeated ArtifactHit artifacts = 1;
repeated PathHit paths = 2;
// True when the result set hit the `limit` cap and more hits exist.
bool truncated = 3;
}
// ─── SbomService messages ────────────────────────────────────────────
message AttachSbomRequest {
// The repository the artifact lives in.
string repository = 1;
// The artifact coordinate the SBOM describes (namespace/name/version).
ArtifactId artifact = 2;
// The SBOM document bytes to host (a CycloneDX 1.5 JSON document). Served back
// byte-identical by FetchSbom / the HTTP door.
bytes document = 3;
}
message AttachSbomResponse {
bool success = 1;
string message = 2;
// blake3 hex of the attached document (the SBOM's content address).
string content_address = 3;
}
message FetchSbomRequest {
string repository = 1;
ArtifactId artifact = 2;
}
message FetchSbomResponse {
// The hosted SBOM document bytes (byte-identical to what was attached).
bytes document = 1;
int64 size_bytes = 2;
// blake3 hex of the document (its content address).
string content_address = 3;
}
// ─── PropertyService messages ────────────────────────────────────────
// One property: a key with its ordered set of values (the map's entry form on
// the wire, since proto has no first-class ordered multimap here).
message PropertyEntry {
string key = 1;
repeated string values = 2;
}
message SetPropertyRequest {
string repository = 1;
ArtifactId artifact = 2;
// The property key to set (or, with empty `values`, remove).
string key = 3;
// The key's values (deduped, order preserved). EMPTY ⇒ remove the key.
repeated string values = 4;
}
message SetPropertyResponse {
bool success = 1;
string message = 2;
// The coordinate's full property map AFTER the set/remove.
repeated PropertyEntry properties = 3;
}
message GetPropertiesRequest {
string repository = 1;
ArtifactId artifact = 2;
}
message GetPropertiesResponse {
// The coordinate's full property map (empty when none attached).
repeated PropertyEntry properties = 1;
}
// ─── ReplicationService messages ─────────────────────────────────────
message ReplicateRequest {
// Source repository — read-only here (only listed + fetched, never mutated).
string from_repo = 1;
// Destination repository — where artifacts land. MUST be writable.
string to_repo = 2;
// false (default) = dry-run PREVIEW (report what would copy, write nothing);
// true = actually copy the missing artifacts into the destination.
bool execute = 3;
// Cap the number of source artifacts examined; 0 = unlimited.
int32 limit = 4;
// ASYNC SHAPE -- see `PromoteArtifactRequest.async_job`. True admits it to the
// job queue and answers with `job_id`. An EXECUTING replication on a server
// with a job queue is queued even when this is unset; a dry run, or a server
// without a queue, runs on this RPC and the caller waits.
bool async_job = 5;
}
message ReplicateResponse {
// Total source artifacts examined.
int64 examined = 1;
// Artifacts actually written into the destination (0 on a dry run).
int64 copied = 2;
// Artifacts already present in the destination by content id (skipped).
int64 skipped = 3;
// Artifacts a dry run WOULD copy (0 on an executing run).
int64 would_copy = 4;
// Artifacts that could not be replicated (source unreadable / dest read-only).
int64 failed = 5;
// Sum of bytes copied (0 on a dry run).
int64 copied_bytes = 6;
// True when the `limit` cap was hit and more source artifacts remained.
bool truncated = 7;
// True when nothing failed — the replication is fully accounted for.
bool complete = 8;
string message = 9;
// Set ONLY when the request asked for the async shape: the admitted job's id.
// Every count above is 0 in that case -- nothing has run yet, and reporting a
// zero as a result would be a lie. Read the outcome with `JobService.GetJob`.
string job_id = 10;
}
// ─── MigrationService messages ───────────────────────────────────────
// Where a migration reads FROM, credentials included.
//
// ⚠ The credential is a PARAMETER and lives in memory for the life of the job
// alone. It is never written to disk, never echoed back by any RPC here, and
// never audited. A one-shot move does not need a stored secret, and a stored
// secret is a standing capability against someone else's registry that outlives
// the reason it was given. Persisting one is a separate decision with its own
// holder (`nornir-vault`), and it is not made here.
message MigrationSource {
enum Kind {
MIGRATION_SOURCE_UNSPECIFIED = 0;
NEXUS = 1;
ARTIFACTORY = 2;
}
Kind kind = 1;
// Nexus: the base URL. Artifactory: the URL up to (not including)
// `/artifactory`.
string base_url = 2;
// Basic-auth credential. In memory only — see the warning above.
string username = 3;
string password = 4;
// Migrate only this ONE source repository. Empty = the whole registry.
string repository = 5;
}
// One source repository and what taking it would mean for holger.
message MigrationRepoPlan {
string source_repo = 1;
// The format string the SOURCE used, verbatim (`maven2`, `pypi`, `generic`).
string source_format = 2;
string target_repo = 3;
// holger's `ron_repo_type` for the mapped format (`maven3`, `pip`, `raw`).
// Empty when holger has no format for what the source said.
string target_format = 4;
// `existing` | `create` | `collision` | `unmapped_format`.
string disposition = 5;
// Why this repository refuses, when it does. Empty otherwise.
string refusal_reason = 6;
// Assets the source LISTED. No artifact content was read to count them.
int64 assets = 7;
// Sum of the sizes the source's listing advertised. 0 where it advertised
// none — which is "not stated", not "empty".
int64 bytes = 8;
// The RON declaration an operator must add to the wired config, and restart,
// BEFORE this repository can be filled. Non-empty only for `create`.
//
// ⚠ holger cannot create a repository while it is running: the route table is
// built from the config at boot and is immutable for the process lifetime. So
// a `create` row is not a thing the migration will do — it is a thing a person
// must do first. Rendered by `traits::migrate::ron_declaration`, the same
// function the run-time refusal quotes, so the console and the terminal hand
// over one text and not two.
string declaration = 9;
}
message DiscoverMigrationRequest {
MigrationSource source = 1;
}
message DiscoverMigrationResponse {
repeated MigrationRepoPlan repos = 1;
int64 total_assets = 2;
int64 total_bytes = 3;
// True when nothing refuses — this plan would actually run.
bool would_run = 4;
// One line per refusing repository. Non-empty iff `would_run` is false.
repeated string refusals = 5;
// The same human preview the CLI's `--dry-run` prints, rendered ONCE in
// `traits::migrate` so the UI and the terminal cannot describe the same plan
// in two different ways.
string report = 6;
}
message StartMigrationRequest {
MigrationSource source = 1;
// In-flight asset transfers. 0 = the engine default.
int32 concurrency = 2;
// ⚠ Inverted ON PURPOSE. proto3 has no field presence for a bool, so a
// `verify_digests` a caller forgot to set would arrive as false and silently
// turn the check off. The safe state has to be the zero value: leave this
// alone and every asset the source published a sha256 for is verified before
// it is stored.
bool skip_digest_verification = 3;
}
message StartMigrationResponse {
// Follow it with `Watch`, stop it with `Cancel`.
string job_id = 1;
string message = 2;
// Echo of the plan the job started with, so a caller that skipped `Discover`
// still learns which repositories it is creating.
repeated MigrationRepoPlan repos = 3;
}
message WatchMigrationRequest {
string job_id = 1;
}
// One progress row. A watcher that joins late receives the latest row for each
// repository first, then follows live — so a UI that reconnects is not blind.
message MigrationProgressEvent {
string job_id = 1;
// The holger repository this row is about.
string repo = 2;
// Assets written into this repository so far.
int64 done = 3;
// Assets that could not be moved.
int64 failed = 4;
// Assets the destination already held — the idempotence count.
int64 skipped = 5;
// Assets the source listed for this repository: the denominator.
int64 total = 6;
int64 bytes = 7;
// Last row for this repository.
bool repo_finished = 8;
// Last row for the whole job. The stream ends after it.
bool job_finished = 9;
// Only meaningful when `job_finished`: nothing failed and it was not cut
// short. A migration that "succeeded" with failures is one an operator would
// wrongly believe is done.
bool complete = 10;
bool cancelled = 11;
string message = 12;
}
message CancelMigrationRequest {
string job_id = 1;
}
message CancelMigrationResponse {
// True when the job was running and is now winding down. False when it had
// already finished — which is not an error, just an answer.
bool cancelling = 1;
string message = 2;
}
message WhoAmIRequest {
// Optional. When set, the answer also carries the caller's role ON THIS
// repository (`repository_role`) — the per-repository ACL included, the same
// resolution the write and admin gates apply. A console asks this instead of
// deciding for itself what a person may do to a repository (holger-ui's
// thin-layer move, 2026-10-04: buying room for a repository is a write on it).
string repository = 1;
}
message WhoAmIResponse {
// True when the presented credential validated against a configured method.
bool authenticated = 1;
// The validated principal (OIDC sub / StaticToken subject / mTLS CN).
// Empty for anonymous callers.
string subject = 2;
// Which configured method validated the credential (e.g. "bearer", "oidc",
// "mtls"). Empty for anonymous callers.
string method = 3;
// The caller's effective GLOBAL role name ("denied"/"reader"/"writer"/
// "admin"), from the RBAC map or the default role. Per-repo ACLs can grant
// or deny beyond this — this is discoverability, not authorization.
string global_role = 4;
// Whether the opt-in read gate (`require_auth_for_reads`) is active — with
// it on, even reads need the reader floor.
bool read_gate = 5;
// The caller's effective role on `WhoAmIRequest.repository`
// ("denied"/"reader"/"writer"/"admin"), per-repository ACL included. Empty
// when no repository was asked about.
string repository_role = 6;
// What is open on this Holger, in plain words (HCON-5: the RON's `access`
// block and its public repositories), read only.
string access_words = 7;
}
message ServerProfileResponse {
// "static" (sealed / read-only) or "dynamic" (writable / proxy).
string profile = 1;
// True when the server accepts no writes on any repository — server truth,
// derived from the live route table (or an explicit sealed-mode override).
bool read_only = 2;
// Human label for the UI chrome/badge (e.g. the red "silent running" strip).
string label = 3;
// Number of configured repositories that accept writes (0 ⇒ read_only).
int32 writable_repo_count = 4;
// ── THE MODULE CENSUS ────────────────────────────────────────────────────
//
// An appliance has no shell. This is the only way an operator can ask "what
// is loaded here, and what may it do" — and the only way a test can ask it of
// a RUNNING box rather than of a build.
//
// A cargo feature's absence is provable in the symbol table. An uninstalled
// module's absence is only "a file that is not there", which is weaker. These
// three fields are where that strength is recovered.
repeated LoadedModule modules = 5;
// The union of every capability granted to any module on this boot. A set
// that does not contain `principal.read`, `digest.read`, `clock.read` or
// `audit.write` proves "and none can be" from CONFIGURATION, without looking
// at a directory at all.
repeated string granted_capabilities = 6;
// The host functions actually defined on this boot's wasm linkers. A module
// importing anything outside this set fails INSTANTIATION, because the import
// is not defined — it is not merely refused at the call.
repeated string linker_defines = 7;
}
// One guest this boot loaded.
message LoadedModule {
// The name the module's own manifest declares.
string name = 1;
// "linked" for a guest compiled into this binary, or the module's file name
// for one loaded from the plugin directory.
string origin = 2;
// "package-handler" | "meter".
string kind = 3;
// Hex blake3-256 of the module bytes, over exactly what the detached CMS
// signature covers. EMPTY for a linked guest, which has no module bytes —
// and empty is distinguishable from a digest, which is the point.
string digest = 4;
// The verified signer this module arrived under, when a trust policy was in
// force. Empty means no provenance was REQUIRED — never "it failed and we
// carried on", which is a boot error.
string receipt = 5;
// What the operator granted THIS module, by name.
repeated string granted = 6;
}
// ─────────────────────────────────────────────────────────────────────────────
// Usage — what a repository is CONSUMING, in holger's own units.
//
// **In the open build, always.** Counting is not money: a self-hoster wants the
// numbers too, and a box that cannot say how much it holds is worse for
// everyone. What is NOT here is any notion of a plan, a price or a cap — that
// contract lives in `holger-metered.proto` and is compiled only by the metered
// build.
//
// **The tenant is a repository** (`RepositoryInfo.name`): holger has no
// namespaces, and the repository is the unit an operator sells.
//
// ⚠ **Three of the six meters are SERVER-WIDE and say so.** Only `artifacts` and
// `archive_bytes` are measured per repository today; `bytes_served`, `requests`
// and `proxy_upstream_hits` are process counters (`metrics.rs`) with no
// per-repository breakdown. Reporting a server-wide number under one
// repository's name would be a number that is not true, and a bill drawn from
// it would be wrong, so each reading carries its own `scope` and a reader that
// ignores the field cannot mistake one for the other.
// ─────────────────────────────────────────────────────────────────────────────
service UsageService {
// One repository's meters. READER on that repository.
rpc ReadUsage(ReadUsageRequest) returns (ReadUsageResponse);
}
// What a single reading is a measurement OF.
enum MeterScope {
METER_SCOPE_UNSPECIFIED = 0;
// Measured for THIS repository alone.
METER_SCOPE_REPOSITORY = 1;
// Measured for the whole server. The same value comes back for every
// repository, and it is not divisible between them.
METER_SCOPE_SERVER = 2;
}
message MeterReading {
// The meter's name, as monetize's holger plugin declares it: `artifacts`,
// `archive_bytes`, `bytes_served`, `requests`, `proxy_upstream_hits`, `repos`.
string name = 1;
// ⚠ MEANINGLESS unless `counted` is true. See below.
uint64 value = 2;
MeterScope scope = 3;
// Where the number came from: the backend name, `metrics`, or whatever
// produced it. A reader that has to ask "who said this" has a number it
// cannot check.
string source = 4;
// **THE FALSE-ZERO GUARD.** `false` means this backend cannot answer this
// meter at all, and `value` is not a measurement — not a zero, not a
// default, not "empty".
//
// Four of holger's backends (`znippy-go`, `znippy-composer`, `znippy-docker`,
// `znippy-nuget`) never override `RepositoryBackendTrait::list` and answer the
// trait default, an empty listing. `list(...).unwrap_or(0)` stamped with a
// REPOSITORY scope reported a full repository as empty, and collapsed a
// genuine backend ERROR into the same 0 — so "the disk broke" and "nothing
// here" were one answer on the wire. A bill drawn from that is wrong in the
// customer's favour on a healthy box and wrong in ours on a broken one.
//
// A fold that treats `counted == false` as a zero is the bug this field
// exists to make impossible. monetize's own fold already counts an absent
// reading as a HOLE (`FleetTotal::tenants_without_reading`), which is the
// behaviour this field lets it keep.
bool counted = 5;
// Why not, by name, when `counted` is false. Empty when it is true.
string refusal_reason = 6;
}
// A ceiling an order attached to one meter, echoed back so a tenant can see its
// own. `limit` of 0 means NO CEILING -- it has never meant "may hold nothing".
message MeterCap {
string meter = 1;
uint64 limit = 2;
}
message ReadUsageRequest {
string repository = 1;
}
message ReadUsageResponse {
repeated MeterReading meters = 1;
// When the reading was taken, Unix milliseconds — `monetize_product::Usage`
// carries the same field and will not invent one.
int64 measured_at_unix_ms = 2;
// The ceilings in force for this repository, so a tenant can see its own.
// Empty in a build with no paid state, and empty is not "zero everywhere".
repeated MeterCap caps = 3;
}
// âââ JobService â the operator's view of the durable job queue ââââââââââ
//
// stinky 01 §6.4 measured the hole this fills: `jera::jobs::JobStore::list`
// existed, holger wrote rows to it, and NOTHING exposed them -- "the ledger is
// written and never shown".
//
// READ-ONLY, exactly like `AuditService` and for the same reason: a write door
// onto the job ledger would be a way to launder what the server says it did.
// There is no Cancel here; cancelling a seal in flight is a separate decision
// with its own consequences and is not made by this service.
//
// ADMIN only, gated like `AuditService`. The rows name authenticated principals
// and the coordinates they moved.
//
// Mounted only by a build carrying the `jobs` feature. A build without it does
// not merely refuse -- the service is not registered, and the async flags on
// PromoteArtifact/Replicate refuse BY NAME.
service JobService {
// One job, by id.
rpc GetJob(GetJobRequest) returns (GetJobResponse);
// The recent jobs, newest first.
rpc ListJobs(ListJobsRequest) returns (ListJobsResponse);
}
// One row of the queue, as a reader sees it.
message JobRow {
string job_id = 1;
// `holger_promote` | `holger_replicate` | `holger_upload` | `holger_gate`.
string kind = 2;
// What it acts on, human-readable (`dev->prod:name:version`).
string target = 3;
// `queued` | `running` | `done` | `failed` | `interrupted`.
//
// `interrupted` is holger's own and is the point of the whole lane: a job that
// was running when the process died is named, with a reason, instead of being
// left `running` for ever (stinky 01 §6.2) or laundered into `failed`, which
// would claim it was tried and refused.
string status = 4;
// The idempotency key -- two jobs with the same content id are the same work.
string content_id = 5;
// Terminal result pointer: a seal's content address, a replication's counts.
string result_ref = 6;
// Why it failed or was interrupted. Empty otherwise -- never a placeholder.
string reason = 7;
// The authenticated principal that asked.
string requested_by = 8;
int64 ts_start_micros = 9;
int64 ts_end_micros = 10;
int64 elapsed_ms = 11;
// What it is doing in the admin view's words: a pull-through (`holger_fetch`)
// or a mirror asset (`holger_mirror_asset`) reads `requested` -> `fetching` ->
// `done` | `failed`; every other kind reads its status word. A `failed` stage
// with `next_attempt_micros` set is a retry waiting (status `retry_wait`).
string stage = 12;
// Attempts made so far, and the ceiling (1 for the four original kinds).
uint32 attempts = 13;
uint32 max_attempts = 14;
// When a `retry_wait` job runs again (µs since the epoch); 0 = not waiting.
int64 next_attempt_micros = 15;
// Pending-list priority from the consumer's admission class: 0 Full,
// 1 Reduced, 2 Floor, 3 Queued. Lower runs first.
uint32 priority = 16;
}
message GetJobRequest {
string job_id = 1;
}
message GetJobResponse {
// False when the ledger holds no such job -- the row below is then empty, and
// "not found" is not spelled as an all-zero job.
bool found = 1;
JobRow job = 2;
}
message ListJobsRequest {
// Filter by kind; empty = every kind.
string kind = 1;
// Cap the rows returned; 0 = every row the ledger holds.
int32 limit = 2;
// Filter by state: empty = all; `waiting` (queued, or failed with a retry
// scheduled), `running`, `failed` (terminal failed or interrupted, AND
// retry_wait — every row with a reason), `done`.
string state = 3;
}
message ListJobsResponse {
repeated JobRow jobs = 1;
}
// ─── The admin view (AdminService.AdminSnapshot) ───────────────────────────
//
// What an administrator is shown and nobody else: operational detail about
// the machine. Every field is MEASURED at the call; a figure that could not be
// measured carries its reason in an `error`/`problems` field, never a zero that
// reads as a measurement.
message AdminSnapshotRequest {
// How many of the newest log lines to return; 0 = 200, capped at 2000.
uint32 log_lines = 1;
}
message AdminSnapshotReply {
// When this was taken, by the estate clock (Unix ms), and by whom.
int64 taken_unix_ms = 1;
string host = 2;
string version = 3;
uint64 uptime_seconds = 4;
// "primary" | "twin".
string role = 5;
AdminDisks disks = 6;
AdminTwin twin = 7;
AdminGrowth growth = 8;
AdminClock clock = 9;
AdminPlugins plugins = 10;
AdminAudit audit = 11;
AdminLogs logs = 12;
AdminPayments payments = 13;
// The disk speed test over the data root (edda disk-snapshot `speed`), run
// ONCE at start on its own thread. `measured` false = not run yet / skipped /
// failed, with the reason: the console says N/A, never a zero.
AdminDiskSpeed disk_speed = 14;
// Every repository (monetize's TENANT for holger): what it holds on disk,
// whether those bytes are a CACHE (a repository with an upstream: pull-through
// or proxy), and its paid `disk_bytes` cap (the metered build's entitlement).
repeated AdminTenant tenants = 15;
// The redb job queue's depth, by status.
AdminQueue queue = 16;
// Every paired downstream as this upstream last heard from it: the rows of
// holger.grow.v1 GrowService.Downstreams, field for field (the twins are
// `twin.twins`, GrowService.Twins). The console's Sync view reads both.
repeated AdminDownstreamRow downstreams = 17;
}
message AdminDiskSpeed {
bool measured = 1;
// The directory measured (the data root).
string path = 2;
// Sequential write + fsync, and the read back off the device, bytes/s.
double seq_write_bps = 3;
double seq_read_bps = 4;
// direct (O_DIRECT) | dropped (the page cache dropped first)
string read_how = 5;
// The read was faster than any disk (> 5 GB/s): the cache answered, so the
// read figure is not a disk speed.
bool read_cached = 6;
// 4 KiB overwrites, each fdatasync'd, per second.
double sync_4k_ops_per_s = 7;
double seconds = 8;
int64 at_unix_ms = 9;
// Why there is no figure (not run yet, skipped for free space, failed).
string why = 10;
}
message AdminTenant {
string repository = 1;
// The repository has an upstream: what it holds is a cache.
bool cache = 2;
repeated string upstreams = 3;
// `disk_bytes` as the meter counts it; `used_measured` false with `used_why`
// when it could not be counted (a group owns none, no storage yet …).
bool used_measured = 4;
uint64 used_bytes = 5;
string used_why = 6;
// The paid cap (quota): `has_cap` false with `cap_why` when none is known.
bool has_cap = 7;
uint64 cap_bytes = 8;
string cap_why = 9;
// The entitlement's state (active | grace | suspended …) when one exists.
string state = 10;
}
message AdminQueue {
// False when this server carries no job queue (built or configured without it).
bool present = 1;
string why = 2;
// Jobs not in a terminal state (queued, running, waiting to retry).
uint64 open = 3;
// status word → count, over the newest `scanned` jobs.
map<string, uint64> by_status = 4;
uint64 scanned = 5;
uint32 workers = 6;
}
message AdminDisks {
// Every storage mount (pseudo filesystems left out), with holger's labels
// on the mount each labelled path sits on: `system` (/), `data` (the data
// root), `storage:<repo>` (a repository's archive or files).
repeated AdminDisk mounts = 1;
repeated AdminBlockDevice block_devices = 2;
// A labelled path that sits on no mount, with the reason.
repeated AdminUnresolved unresolved = 3;
// Why the mount table or /sys/block could not be read.
repeated string problems = 4;
// The rule the floor figures were computed with, in words — and whether
// holger enforces it (it does not, today: the floor is advisory).
string floor_rule = 5;
}
message AdminDisk {
string source = 1;
string mount_point = 2;
string fstype = 3;
bool read_only = 4;
repeated string labels = 5;
repeated string paths = 6;
// statvfs(3), in bytes. All 0 with `error` set when it could not be taken.
uint64 total_bytes = 7;
uint64 free_bytes = 8;
uint64 available_bytes = 9;
uint64 used_bytes = 10;
uint64 reserved_bytes = 11;
uint64 inodes_total = 12;
uint64 inodes_free = 13;
uint64 block_size = 14;
uint64 floor_bytes = 15;
// available − floor.
uint64 servable_bytes = 16;
uint32 used_permille = 17;
string error = 18;
}
message AdminPartition {
string name = 1;
uint64 size_bytes = 2;
}
message AdminBlockDevice {
string name = 1;
uint64 size_bytes = 2;
bool rotational = 3;
bool removable = 4;
bool read_only = 5;
repeated AdminPartition partitions = 6;
// What holds it or one of its partitions (md127, dm-0).
repeated string holders = 7;
// What it is built from (an array's members).
repeated string members = 8;
string raid_level = 9;
}
message AdminUnresolved {
string label = 1;
string path = 2;
string why = 3;
// True for a repository's storage folder (`storage:<repo>`) that does not exist yet:
// holger creates it at the first upload, so it is no fault, and `why` says that
// (lane HOL-UI-FIX). False for every other path that sits on no mount.
bool made_at_first_upload = 4;
}
message AdminTwin {
// "primary" | "twin".
string role = 1;
// The primary a twin follows.
string primary = 2;
bool serving = 3;
bool accepts_writes = 4;
// A box awaiting its recharge serves nothing; the source it waits for.
string awaiting_recharge_from = 5;
// A twin: is it alive and whole — how it last followed, whether its last
// cycle that reached the primary was whole, when the last whole one ended,
// and the lag: now − that end, seconds (−1 = never whole).
string follow = 6;
bool complete = 7;
string why = 8;
int64 last_whole_unix = 9;
int64 lag_seconds = 10;
// The storage invariant's verdict warning (empty when none).
string store_warning = 11;
// The replication door on this box: refusals by name and admitted peers.
uint64 door_refused = 12;
string door_last_refusal = 13;
map<string, uint64> door_admitted = 14;
// The mTLS credentials this box holds (purpose "twin", "recharge").
repeated AdminTwinCredential credentials = 15;
// A primary: every enrolled twin, as its reports say.
repeated AdminTwinRow twins = 16;
string twins_error = 17;
}
message AdminTwinCredential {
string purpose = 1;
bool installed = 2;
// The leaf's CN, the pinned issuer's CN.
string principal = 3;
string issuer = 4;
int64 not_after_unix = 5;
int64 days_left = 6;
repeated string doors = 7;
// Why it could not be read (a half-installed credential is an error).
string error = 8;
}
message AdminTwinRow {
string principal = 1;
string address = 2;
// ok | behind | stalled | silent — stalled: still reporting, the applied
// position unchanged with the head ahead for longer than TWIN_STALLED_AFTER_MS
string state = 3;
int64 last_seen_unix_ms = 4;
uint64 lag_entries = 5;
uint64 disk_total_bytes = 6;
uint64 disk_used_bytes = 7;
uint32 fill_permille = 8;
// The last synced point and today's pulls (holger.grow.v1 TwinRow 9, 10, 15, 16).
uint64 applied_seq = 9;
uint64 head_seq = 10;
uint64 pulls_today = 11;
int64 last_pull_unix = 12;
}
// One paired downstream (holger.grow.v1 DownstreamRow, field for field).
message AdminDownstreamRow {
string name = 1;
// `sha256:<64 hex>` of the paired client certificate.
string fingerprint = 2;
// ok | behind | stalled | silent — the worst repository's; `silent` when never heard.
string state = 3;
bool stalled = 4;
// The largest repository lag.
uint64 lag_entries = 5;
int64 last_seen_unix_ms = 6;
uint64 seen_ago_ms = 7;
uint32 every_secs = 8;
repeated AdminDownstreamRepo repositories = 9;
// Today's pulls (UTC day, since this process started) and the last one.
uint64 requests_today = 10;
uint64 bytes_today = 11;
int64 last_pull_unix = 12;
}
// One paired repository's sync point (holger.grow.v1 DownstreamRepoRow).
message AdminDownstreamRepo {
string repository = 1;
// ok | behind | stalled | silent — TwinRow.state's words and rule.
string state = 2;
uint64 lag_entries = 3;
uint64 applied_seq = 4;
uint64 head_seq = 5;
// When the last complete cycle of this repository ended; 0 = never.
int64 last_complete_unix_ms = 6;
}
message AdminGrowMember {
uint32 index = 1;
string device = 2;
string partition = 3;
uint64 disk_bytes = 4;
uint64 set_bytes = 5;
uint64 offered_bytes = 6;
bool larger = 7;
}
message AdminGrowth {
// False when this process holds no growth plane (a bare service).
bool present = 1;
string phase = 2;
string why = 3;
uint64 epoch = 4;
bool serving = 5;
bool engine_present = 6;
bool hotplug = 7;
uint64 in_flight = 8;
int64 since_unix_ms = 9;
string detail = 10;
string set_uuid = 11;
uint64 total_bytes = 12;
repeated AdminGrowMember members = 13;
string members_verdict = 14;
// THE HISTORY: the boot stack's grow journal — its phase, the steps it
// took in order, the members a partial growth left behind, where it lives.
string journal_phase = 15;
repeated string journal_steps = 16;
repeated uint32 journal_partial_members = 17;
string journal_detail = 18;
string journal_path = 19;
// The capacity ladder's verdict over this machine.
SetGrowth capacity_growth = 20;
string capacity_growth_reason = 21;
string data_set = 22;
}
message AdminClockSource {
string source = 1;
bool answered = 2;
int64 offset_ms = 3;
string error = 4;
}
message AdminClockSync {
// timer | boot | fed | …
string trigger = 1;
int64 at_unix_ms = 2;
// stepped | slewed | already-right | apply-failed | no-quorum | …
string outcome = 3;
bool vouches = 4;
int64 offset_ms = 5;
string detail = 6;
repeated AdminClockSource sources = 7;
}
message AdminClock {
// Whether a verified answer from the front stands behind holger's clock.
bool trusted = 1;
// The correction held in-process (the machine's clock was not moved), ms.
int64 owed_ms = 2;
// The configuration in force, in words (or why it is refused).
string config = 3;
int64 estate_unix_ms = 4;
int64 system_unix_ms = 5;
// Absent (not synced yet) when `has_last` is false.
bool has_last = 6;
AdminClockSync last = 7;
bool has_boot = 8;
AdminClockSync boot = 9;
}
message AdminSelfRegistration {
// False when this build or box does not register a plugin at all.
bool active = 1;
// The last line the registration said, and whether it was taken.
string line = 2;
bool registered = 3;
bool warning = 4;
int64 at_unix_ms = 5;
}
message AdminEntitlement {
string repository = 1;
// The fact's caps, `name=value`, as signed.
repeated string caps = 2;
// active | grace | suspended | … (monetize's State), when paid until (Unix
// ms; 0 = open-ended), and where the fact came from (operator | payment:…).
string state = 3;
uint64 paid_until_unix_ms = 4;
string source = 5;
}
message AdminPlugins {
// What this boot loaded (ServerProfile's census, verbatim).
repeated LoadedModule modules = 1;
repeated string granted_capabilities = 2;
uint32 meters = 3;
// True when this is the metered build (it carries monetize's seam).
bool metered_build = 4;
// The metered build's entitlement store: configured with monetize's key?
bool entitlements_configured = 5;
repeated AdminEntitlement entitlements = 6;
AdminSelfRegistration self_registration = 7;
}
message AdminAudit {
// everything | aggregated | off
string mode = 1;
// "directory <path>" | "memory" | "none".
string source = 2;
uint64 segments = 3;
uint64 total_rows = 4;
uint64 total_bytes = 5;
int64 first_ts_nanos = 6;
int64 last_ts_nanos = 7;
string error = 8;
}
message AdminLogs {
// False when this process carries no installer log (not an installer-built box).
bool present = 1;
// The installer's log door: "install" (the installer is still at work) or
// "log-door" (closed: the same listener serves only GET /install/log to the
// installer's certificate).
string door = 2;
bool closed = 3;
uint64 lines_total = 4;
uint64 lines_dropped = 5;
// The newest lines, oldest first.
repeated string tail = 6;
}
message AdminPayments {
// The Stripe webhook's last ACCEPTED delivery is kept by monetize on the
// front, not by holger; 0 with `note` saying where it is not measured.
int64 stripe_last_delivery_unix_ms = 1;
string note = 2;
}
// ─── AdmissionService — who may take how much of the door ──────────────────
//
// The download/upload admission (`server/lib/src/admission.rs`) over the shared
// trait `edda_admit::Admit`: the policy (`pass-through` | `usage-share`), each
// consumer's usage over the window, and what `Admit::plan` answers for one.
// READ-ONLY and ADMIN only, like `JobService`: the rows name principals and
// paired peers (an anonymous consumer is only ever its per-day token label).
service AdmissionService {
// `admit plan` for one consumer and one kind of work, now.
rpc AdmitPlan(AdmitPlanRequest) returns (AdmitPlanResponse);
// Every consumer the usage book holds, heaviest window first.
rpc ListUsage(ListUsageRequest) returns (ListUsageResponse);
}
// The six units a box runs out of (`edda_admit::Cost`). Bytes except cpu_ms.
message CostRow {
uint64 cpu_ms = 1;
uint64 disk_read = 2;
uint64 disk_write = 3;
uint64 net_in = 4;
uint64 net_out = 5;
uint64 upstream_fetch = 6;
}
// One work kind's numbers for one consumer.
message WorkUsageRow {
// `download` | `upload` | `pull_through` | `mirror_asset`.
string work = 1;
uint64 requests = 2;
uint64 refused = 3;
CostRow estimated = 4;
CostRow actual = 5;
}
message UsageRow {
// `principal:<sub>` | `peer:<name>` | `anon:<day>:<8 hex>`.
string consumer = 1;
// Actual cost over the policy's window, and today (UTC).
CostRow window = 2;
CostRow today = 3;
repeated WorkUsageRow by_work = 4;
// The class the last plan gave it (`full` | `reduced` | `floor` | `queued`,
// empty before any plan), and the plan's sentence.
string class = 5;
string why = 6;
uint32 in_flight = 7;
}
// The policy's learnt estimate for one work kind (`Admit::estimates`).
message EstimateRow {
string kind = 1;
uint64 samples = 2;
uint64 hinted = 3;
CostRow mean = 4;
// Cost of one GB of hinted size; absent before any hinted sample.
optional CostRow per_gb = 5;
}
message AdmitPlanRequest {
// `principal:<sub>` | `peer:<name>` | `anonymous` (a probe token for today).
string consumer = 1;
// `download` (default) | `upload` | `pull_through` | `mirror_asset`.
string work = 2;
// Size hint in bytes; 0 = none.
uint64 size_hint = 3;
}
message AdmitPlanResponse {
string consumer = 1;
string work = 2;
string class = 3;
CostRow estimate = 4;
string why = 5;
// Bytes left in the window; absent = unlimited.
optional uint64 budget_left = 6;
// Concurrent requests this class gets at the door; absent = no share
// enforced (no `admission:` block).
optional uint32 share = 7;
string policy = 8;
repeated EstimateRow estimates = 9;
// The consumer's usage row (absent when the book has none).
optional UsageRow usage = 10;
}
message ListUsageRequest {
// Cap the rows; 0 = all.
int32 limit = 1;
}
message ListUsageResponse {
string policy = 1;
repeated UsageRow consumers = 2;
// Requests in flight through the door right now, all consumers.
uint32 in_flight_total = 3;
// The door's ceiling and the per-class shares; absent = none enforced.
optional uint32 max_in_flight = 4;
optional uint32 share_full = 5;
optional uint32 share_reduced = 6;
optional uint32 share_floor = 7;
// Whether proxy pull-through runs on the job queue.
bool pull_through_queue = 8;
}
// ── the security page (lane HOL-SEC, 2026-10-27) ──────────────────────────────
message SecuritySummaryRequest {}
message SecuritySummaryReply {
// One line over the page, in the server's words.
string headline = 1;
// The sections, in the order the page shows them (keys: chain, sources, scanners,
// approvals, held, access, sync, events).
repeated SecuritySection sections = 2;
}
message SecuritySection {
// A stable key (`chain`, `scanners`, …); the console keys its markup on it, never its words.
string key = 1;
string title = 2;
// One or two sentences under the title.
string summary = 3;
// What to say when there are no rows.
string empty = 4;
repeated SecurityRow rows = 5;
}
message SecurityRow {
// What the row is about: a step, a source, a scanner, an artifact, a repository, an event.
string name = 1;
// One short word for its state (`required`, `quarantined`, `public`, …).
string state = 2;
// `good` | `warn` | `bad` | `info`: how the state reads.
string tone = 3;
// The rest, in plain words: why, what it checks, who, when.
string detail = 4;
// The artifact the row is about, when it is one (the console links its security view).
ArtifactId artifact = 5;
}
// ── the estate board (lane E3, 2026-10-19; Gunnar's Admin.Topology pattern) ──
message TopologyRequest {}
message TopologyReply {
// This box first, then the others in a stable order: front, primary, twins,
// downstreams, each by name.
repeated TopologyBox boxes = 1;
// This server's own clock when it answered (Unix ms).
int64 server_unix_ms = 2;
// The silence bound every `state` below was judged against, in ms.
uint64 silent_after_ms = 3;
// The estate's name from the pairing feed; empty on a holger nobody paired.
string estate = 4;
// The links between the boxes: front → primary, primary → twin, primary →
// each downstream. Each carries the state of the box it leads INTO.
repeated TopologyLink links = 5;
// RETIRED (Holger v2.0 H2): always empty. The server draws nothing; holger-ui
// draws the board from `boxes` and `links`. Kept so the tag is never reused.
string svg = 6;
// **Every consumer this box serves** (lane A13): named and anonymous, as the
// door admission's book counts them. Unset = this build keeps no book.
TopologyConsumers consumers = 7;
// **The board in one line, holger's words** (lane A13): the line drawn under
// the title — how many upstreams and downstreams, and every consumer — for a
// console's text alternative, rendered as it came.
string summary = 8;
}
// **Who consumes this box** (lane A13, 2026-10-23), from the door admission's
// usage book: every request asks it once, whatever the policy.
message TopologyConsumers {
// False when this build keeps no book: then the numbers below are not counts.
bool counted = 1;
// Authenticated principals with a request in the window (durable: the book's
// ledger rows survive a restart).
uint64 principals = 2;
// Paired downstream holgers with a request in the window.
uint64 peers = 3;
// Anonymous clients TODAY (UTC day): each is a per-day keyed hash that dies
// at midnight, so there is no window for them, by design. Since this process
// started when it started today.
uint64 anonymous_today = 4;
// The window the named counts cover, in days.
uint32 window_days = 5;
}
// **One box.** Every field is evidence this holger holds, never a guess: a box it
// has no channel to read stays `unknown`, which is a different fact from `silent`.
message TopologyBox {
// A short name: the pairing name (`front`, `main`, `twin`), a twin's principal,
// a downstream's pairing name, or this box's own pairing name.
string name = 1;
// `front` | `primary` | `twin` | `downstream` | `upstream` (lane A13: a
// registry this box proxies, or a holger it mirrors).
string role = 2;
// Where the evidence says it is (`host` or `host:port`); empty when unknown.
string address = 3;
// `up` (it answered, or reported, within the bound) · `behind` (a twin or a
// downstream that reports and does not catch up) · `silent` (expected, and not
// heard within the bound, or never — and a box HELD for its recharge, which
// serves nothing) · `unknown` (nothing here can ask it).
string state = 4;
// One sentence for a human: how holger knows, and since when.
string why = 5;
// True for the box that answered this call.
bool this_box = 6;
// Wall clock of the newest evidence (Unix ms); 0 = none.
int64 last_seen_unix_ms = 7;
// `server_unix_ms - last_seen_unix_ms` at answer time; 0 when never seen.
uint64 seen_ago_ms = 8;
// The box's data volume, as measured (this box: statvfs of its data root; a
// twin: its own, carried on its report). False = not measured, and then the
// two numbers are 0 and must not be read.
bool disk_measured = 9;
uint64 disk_total_bytes = 10;
uint64 disk_used_bytes = 11;
// Where the evidence came from: `self`, `pairing`, `report` (a twin's
// TwinReport, or on a twin its own follow of the primary), `downstream`.
repeated string sources = 12;
// The WireGuard overlay address (wg0) and the utility address, from the pairing
// feed; empty when not fed.
string overlay_address = 13;
string utility_address = 14;
// **The state in a few words, holger's**: `answering`, `following`, `behind 120
// entries`, `silent for 4 min`, `never heard`, `not asked`, `held for its
// recharge from twin`, `fenced`. Coarse on purpose (whole minutes once past
// one), so the answer only changes when the state does.
string headline = 15;
// A twin or a downstream: the twin's own word (TwinRow.state: ok · behind ·
// stalled · silent, M2's `twin_state`) and its lag in log entries (M2's
// `lag_entries`); empty / 0 for any other box.
string twin_state = 16;
uint64 lag_entries = 17;
// True when the box is HELD: a main awaiting its recharge from its twin (M3's
// boot hold), or a fenced primary. Drawn dark, with the word `held`.
bool held = 18;
// When this box's mutual-TLS credential to its peer expires (Unix s; the
// twin's `twin` credential on a twin); 0 = none held or not readable here.
int64 cert_not_after_unix = 19;
// When the pairing itself expires (the estate's key, lane E2); 0 = not known.
int64 pairing_not_after_unix = 20; // **Packages fetched** (lane A13): a downstream — artifacts delivered to it,
// ever (durable) and today; an upstream — artifacts this box took from it
// since this boot (`fetched_today` unused). 0 for any other box.
uint64 fetched_total = 21;
uint64 fetched_today = 22;
// An upstream only: `proxy` (a repository another one names in
// `ron_upstreams`: crates.io, PyPI, npm, a remote harbour) or `mirror` (a
// `mirror.ron_sources` entry: another holger). Empty for any other box.
string upstream_kind = 23;
// An upstream only: the local repositories it feeds.
repeated string feeds = 24;
// A downstream only: how it reaches this holger, `front` (the front's public
// door) or `wireguard` (the front's downstream interface; lane HOL-WGDS).
string via = 25;
}
message TopologyLink {
// Box names, as in TopologyBox.name.
string from = 1;
string to = 2;
// `serves` (front → primary) · `follows` (primary → twin) · `pulls` (primary →
// downstream) · `feeds` (upstream → this box, lane A13).
string kind = 3;
// The state of the box the link leads into (TopologyBox.state).
string state = 4;
}
// ── the box's import doors (lane W4, 2026-10-19) ──
// One piece of a streamed bundle. `first_import` is read from every chunk: any
// chunk that sets it establishes an absent anti-rollback floor with this bundle.
message ImportBundleChunk {
bytes data = 1;
bool first_import = 2;
}
message ImportIntelBundleReply {
// `new` or `idempotent-reimport`.
string outcome = 1;
string bundle_id = 2;
uint64 sequence = 3;
// YYYY-MM-DD of the advisory snapshot.
string snapshot_at = 4;
uint64 advisories = 5;
string message = 6;
}
// One artifact a bundle import landed.
message ImportedArtifactRow {
string artifact = 1;
string sha256 = 2;
// AVAILABLE.
string state = 3;
string repository = 4;
// `landed` or `already-available`.
string outcome = 5;
}
message ImportBundleReply {
// `new` or `idempotent-reimport`.
string outcome = 1;
string bundle_id = 2;
uint64 sequence = 3;
// `airgap-export` or `update-bundle`.
string kind = 4;
repeated ImportedArtifactRow artifacts = 5;
string message = 6;
}