proto/holger-estate.proto
// holger.estate.v1 — THE ESTATE, OVER THE WIRE (owner plan 2b, 2026-09-27).
//
// A twin (or a paired downstream acting as one) holds what its primary holds:
// the artifacts — which holger's mirror already pulls over RepositoryService —
// AND the rest of the data root: the config and the side stores
// (`replication::ESTATE`: audit, attestations, properties, SBOMs, the PROD store,
// passkeys, tenants, entitlements, meters, jobs, holger.ron …). Until now those
// were copied only disk to disk (`replicate_estate`, `estate --from --to`), so a
// twin on another machine could not hold them (stinky 02 S2-1). This service is
// that copy's wire: a manifest, and the bytes of one file at a time.
//
// READ-ONLY by construction — there is no verb here that writes to the box that
// serves it. The same two calls, asked the other way round, RECHARGE a new main
// from its twin: the new main pulls the twin's estate before it serves.
//
// Its own package (like holger.metered.v1) so the product's shared surface,
// holger.proto, and every downstream copy of it do not move.
syntax = "proto3";
package holger.estate.v1;
service EstateService {
// Every file of the estate, with its size and blake3 — what a follower
// compares against what it holds. Admin-read gated.
rpc Manifest(ManifestRequest) returns (ManifestResponse);
// A byte range of ONE file the manifest lists (paths outside the estate are
// refused by name). Admin-read gated.
rpc Fetch(FetchRequest) returns (FetchResponse);
}
message ManifestRequest {}
message EstateFile {
// Relative to the data root, `/`-separated, always under an estate item.
string path = 1;
uint64 size = 2;
// blake3 of the file's bytes, hex.
string blake3 = 3;
}
message ManifestResponse {
repeated EstateFile files = 1;
// The estate items the serving box does not have at all — a normal state
// ("nothing to copy"), said by name rather than by absence.
repeated string absent = 2;
}
message FetchRequest {
string path = 1;
uint64 offset = 2;
// At most this many bytes (the server caps it); 0 = the server's cap.
uint64 max_bytes = 3;
}
message FetchResponse {
bytes data = 1;
// The file's full size, so a follower knows when it has all of it.
uint64 size = 2;
}