proto/holger-grow.proto
syntax = "proto3";
// holger.grow.v1 — the data set grows WITHOUT a restart (2026-09-27).
//
// holger's half of monetize's growth: the batcher on vm1 reads
// AdminService.Capacity (total / used / `growth`), orders a `watermark:`
// growth, and upcloud.wasm detaches → resizes → attaches the volumes while this
// door reports `unmounted | waiting-members`. The phase machine is edda's
// `appliance-grow` (gunnar's, moved AS IS); the field set below is gunnar's
// `GrowStartRequest` / `GrowStatusResponse` / `FlushReplicationResponse` field
// for field, so monetize reads the same facts from both products.
//
// Its own package, like holger.estate.v1: holger.proto — and every downstream
// copy of it — moves only by the growth verdict on CapacityReply.
package holger.grow.v1;
service GrowService {
// Drain, unmount, wait for the members. ADMIN (on a twin too: a twin grows
// exactly like main). Answers the status AFTER the call, never when the
// growth has finished.
rpc Start(GrowStartRequest) returns (GrowStatusReply);
// The phase, NO CREDENTIAL: the actor outside the box that owns the volumes
// must read `waiting-members` / `failed` with nothing to sign with, while the
// stores are closed — gunnar's Bootstrap.GrowStatus, one door here.
rpc Status(GrowStatusRequest) returns (GrowStatusReply);
// While `waiting-members`: look again now (`give_up`: serve what is there).
// On a growth that is already `done` it answers the status. ADMIN.
rpc Resume(GrowResumeRequest) returns (GrowStatusReply);
// The watermark (T11): wait up to `timeout_secs` for every enrolled twin to
// have applied this box's head. ADMIN.
rpc Flush(GrowFlushRequest) returns (GrowFlushReply);
// A TWIN's word to its primary, each follow cycle: how far into the primary's
// sequence it has applied, and how full its own disk is. The primary answers
// its current position. ADMIN-READ (the twin's follow bearer).
rpc TwinReport(TwinReportRequest) returns (TwinReportReply);
// Every enrolled twin as this primary last heard from it — monetize's twin
// sizing reads the fill. ADMIN-READ.
rpc Twins(TwinsRequest) returns (TwinsReply);
// A paired DOWNSTREAM holger's word to this upstream after each completed
// mirror cycle — the twin's TwinReport, per paired repository: how far into
// this box's sequence it has applied. Authenticated by the paired mTLS
// certificate ALONE (the pinned fingerprint of `holger-peers/downstream-*`);
// an unpaired caller is refused by name (holger-peer-4), a repository the
// pairing does not list by holger-peer-2. The answer is this box's position,
// which the downstream's next cycle starts from.
rpc DownstreamReport(DownstreamReportRequest) returns (TwinReportReply);
// Every paired downstream as this upstream last heard from it (kept under the
// data root, so a restart keeps the last word). ADMIN-READ.
rpc Downstreams(DownstreamsRequest) returns (DownstreamsReply);
}
message GrowStartRequest {
// Per-member target in 512-byte sectors; 0 = whatever every member comes
// back larger at.
uint64 target_sectors_per_member = 1;
// The go-ahead JSON ({v, signer, target_sectors, nonce, issued_unix_ms,
// signature}); `monetize` signed under the box's monetize key (metered), or
// `operator` on this admin door.
bytes go_ahead = 2;
// 0 = the box's own ceilings (600 s drain, 3600 s members).
uint32 drain_timeout_secs = 3;
uint32 wait_members_timeout_secs = 4;
}
message GrowStatusRequest {}
message GrowResumeRequest {
bool give_up = 1;
}
message GrowMember {
uint32 index = 1;
string device = 2;
string partition = 3;
uint64 disk_bytes = 4;
uint64 set_bytes = 5;
uint64 offered_bytes = 6;
bool larger = 7;
}
// The boot stack's grow journal (/serialdat/grow-journal), summarised.
message GrowState {
bool engine_present = 1;
// idle | in_progress | done | partial | aborted | failed
string phase = 2;
uint64 epoch = 3;
uint64 target_sectors = 4;
repeated string steps = 5;
repeated uint32 partial_members = 6;
string detail = 7;
string journal_path = 8;
}
message GrowStatusReply {
// idle | draining | unmounted | waiting-members | growing | mounting | done | failed
// `unmounted` is said only once /data is unmounted AND the array stopped.
string phase = 1;
string why = 2;
uint64 epoch = 3;
uint64 target_sectors_per_member = 4;
repeated GrowMember members = 5;
string members_verdict = 6;
GrowState journal = 7;
bool engine_present = 8;
bool hotplug = 9;
// /data is mounted and the stores are open — true ONLY then. monetize orders
// a recharge on a failed run that reads `serving: false`.
bool serving = 10;
bool identity_in_ram = 11;
uint64 in_flight = 12;
int64 since_unix_ms = 13;
int64 server_unix_ms = 14;
string detail = 15;
string go_ahead_signer = 16;
string set_uuid = 17;
uint64 total_bytes = 18;
// This box is a twin (it grows like main; its drain proves no standby).
bool twin = 19;
}
message GrowFlushRequest {
uint32 timeout_secs = 1;
}
message GrowFlushReply {
bool caught_up = 1;
uint64 in_flight = 2;
uint64 pending = 3;
uint64 bus_gaps = 4;
bool needs_full_resync = 5;
int64 last_success_unix_ms = 6;
int64 flushed_at_unix_ms = 7;
uint64 waited_ms = 8;
uint64 catalog_repos = 9;
uint64 standbys = 10;
string verdict = 11;
}
message TwinReportRequest {
// Who the twin is (its principal on the primary's roster).
string principal = 1;
// Where the twin serves (for the operator; the primary never dials it).
string address = 2;
// The primary boot the position below belongs to; a position from another
// boot says nothing about this one.
uint64 applied_boot = 3;
// The primary's head at the START of the twin's last complete follow cycle.
uint64 applied_seq = 4;
// The twin's own data volume, statvfs.
uint64 disk_total_bytes = 5;
uint64 disk_used_bytes = 6;
// WHAT the twin holds, as it proved it at the end of its last WHOLE follow
// cycle (2026-09-28, the estate's read of its twin — gunnar's pattern: a twin
// publishes no door outside, its PRIMARY keeps what the twin says of itself,
// and the front carries the primary's door): the root of the primary's estate
// manifest the twin pulled and holds blake3-identical
// (`estate_wire::manifest_root`), empty until one whole cycle;
string estate_root = 7;
// how many files that manifest named;
uint64 estate_files = 8;
// the artifacts the twin wrote most recently, with the sha256 of the bytes it
// wrote (a bounded ring, oldest first).
repeated HeldArtifact held = 9;
}
// One artifact a twin wrote into its own repository.
message HeldArtifact {
string repository = 1;
// The mirror coordinate (`namespace/name@version`).
string coordinate = 2;
// Lowercase hex sha256 of the bytes the twin wrote.
string sha256 = 3;
uint64 size = 4;
}
message TwinReportReply {
uint64 boot = 1;
uint64 head_seq = 2;
}
message TwinsRequest {}
message TwinRow {
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 twin's applied position and this primary's head — `lag_entries` is
// computed from them (applied 0: a position from another boot, or none).
uint64 applied_seq = 9;
uint64 head_seq = 10;
uint64 seen_ago_ms = 11;
// What the twin last said it holds (TwinReportRequest 7-9).
string estate_root = 12;
uint64 estate_files = 13;
repeated HeldArtifact held = 14;
// Today's pulls (UTC day, since this process started) and the last one —
// DownstreamRow 10/12's words: a twin reports once at the end of each whole
// follow cycle, so each report is one pull. The last synced point is
// applied_seq above, judged by the one follow_point rule.
uint64 pulls_today = 15;
int64 last_pull_unix = 16;
}
message TwinsReply {
repeated TwinRow twins = 1;
}
// One paired repository's sync point, as the downstream says it.
message DownstreamPoint {
// The repository on THIS (the upstream) box — the pairing's name for it.
string repository = 1;
// The upstream boot the position below belongs to (TwinReportRequest 3).
uint64 applied_boot = 2;
// The upstream's head at the START of the downstream's last complete cycle
// of this repository (TwinReportRequest 4); 0 = none yet.
uint64 applied_seq = 3;
// When that cycle completed; 0 = never.
int64 last_complete_unix_ms = 4;
}
message DownstreamReportRequest {
repeated DownstreamPoint points = 1;
// The downstream's declared cycle (`ron_every_secs`): the upstream reads
// `silent` after 3 of them and `stalled` after 5 (the twin's rule). 0 = the
// mirror default.
uint32 every_secs = 2;
// What the downstream is, from its own RON (HOL-FLOW-SMART): the upstream's flow
// unfolds it to this. Absent from an older downstream. NOT in the text the report's
// proof signs (that text is the same in every holger version, HOL-SYNC-SKEW): a sync
// downstream proves it with `summary_proof`, a paired one by its certificate.
DownstreamSummary summary = 3;
// The summary's own proof (HOL-SYNC-SKEW): base64 of the signed envelope over the
// summary's text, which holds the report's text too, so it belongs to this report only.
// An older upstream does not know this field and skips it, with the summary.
string summary_proof = 4;
// The proof of the summary's `console_url` (HOL-FLOW-FRONT): base64 of the signed envelope
// over its own text (the report's text and the address), so an upstream that checks the
// summary as `summary_proof` signs it (holger 3.1.0) still opens it, and a newer upstream
// keeps the address only when this proof holds.
string console_proof = 5;
}
// ★ **A downstream's own RON, summarised** (HOL-FLOW-SMART): names, words and folders,
// never a key, token or secret.
message DownstreamSummary {
// `OneWay` or `Fetch`.
string boundary = 1;
// Its repositories, by name.
repeated string repositories = 2;
// Its one-way inbox folder; empty when it fetches.
string inbox = 3;
// Who allows a vendor's first import (`admin`).
string first_import = 4;
// Its security domain; empty when it declares none.
string domain = 5;
// How many downstreams it declares itself (it is upstream of N).
uint32 downstreams = 6;
// Its console's address (an https URL: the site's www name), as its own front reported it
// (HOL-FLOW-FRONT): the upstream's flow links the downstream's box to that server's own
// flow. Empty when it knows none. Not in `summary_proof`'s text: `console_proof` proves it.
string console_url = 7;
}
message DownstreamsRequest {}
message DownstreamRepoRow {
string repository = 1;
// ok | behind | stalled | silent — TwinRow.state's words and rule.
string state = 2;
// Writes this upstream admitted since the reported point (head − applied; the
// whole head for a point from another boot). The upstream's sequence counts
// every write, so this is an upper bound for one repository.
uint64 lag_entries = 3;
uint64 applied_seq = 4;
uint64 head_seq = 5;
int64 last_complete_unix_ms = 6;
}
message DownstreamRow {
string name = 1;
// `sha256:<64 hex>` of the paired client certificate.
string fingerprint = 2;
// The worst repository's state; `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 DownstreamRepoRow 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;
// **Packages this downstream has fetched, ever** (lane A13, 2026-10-23): one
// per artifact delivered to its paired certificate, and their bytes. Kept in
// its durable sync record (`holger-peers/sync-<name>.ron`), so a restart
// keeps the count; pulls in the last minute before a crash may be lost.
uint64 fetched_total = 13;
uint64 bytes_total = 14;
// Artifacts delivered to it today (UTC day, since this process started).
uint64 fetched_today = 15;
// What it said of its own RON in its last report (HOL-FLOW-SMART); absent when it
// said nothing.
DownstreamSummary summary = 16;
}
message DownstreamsReply {
repeated DownstreamRow downstreams = 1;
uint64 boot = 2;
uint64 head_seq = 3;
}