crates/gunnar-ui-server/proto/gunnar/v1/common.proto
syntax = "proto3";
// gunnar's control plane. gRPC is the ONLY control plane; there is no REST and
// no web UI in this repo: gRPC is the only control plane.
//
// Two rules hold across every file in this package:
//
// 1. NAMESPACES ARE FIRST CLASS. Multi-tenancy is in from day 1. Every
// repository is addressed as `<namespace>/<repo>`, never as a bare name,
// never as an opaque id. `RepoPath` carries both halves and appears in
// every repo-scoped request and every repo-scoped event.
//
// 2. NO `google.protobuf.Any`. Every payload is an explicit, named message.
// A control plane whose wire types are opaque cannot be reviewed, and a
// UI cannot render what it cannot name.
//
// Timestamps are explicit `int64 *_unix_ms` (milliseconds since the Unix
// epoch, UTC) rather than `google.protobuf.Timestamp`: it keeps the server's
// dependency budget one crate lighter and there is nothing a Timestamp
// buys here that a millisecond count does not.
package gunnar.v1;
// The hash function a repository's objects are named by (sha256 is the
// default for new repos; sha1 stays supported). `object-format` on the wire is
// echoed from this. Answering sha1 to a sha256 repo yields an empty response,
// a bug that has already cost one session.
enum HashKind {
HASH_KIND_UNSPECIFIED = 0;
HASH_KIND_SHA1 = 1;
HASH_KIND_SHA256 = 2;
}
enum Visibility {
VISIBILITY_UNSPECIFIED = 0;
// Readable only by principals with an explicit grant.
VISIBILITY_PRIVATE = 1;
// Readable by any authenticated principal on this server.
VISIBILITY_INTERNAL = 2;
// Readable without a grant.
VISIBILITY_PUBLIC = 3;
}
// gunnar owns its own identity table. There are no OS users.
enum PrincipalKind {
PRINCIPAL_KIND_UNSPECIFIED = 0;
PRINCIPAL_KIND_USER = 1;
PRINCIPAL_KIND_MACHINE = 2;
PRINCIPAL_KIND_DEPLOY = 3;
// A group of principals (punkt 9). A group holds no keys; it is a name that
// grants attach to, and its members inherit those grants. Membership is
// tracked by the Access service (AddGroupMember), not on this message.
PRINCIPAL_KIND_GROUP = 4;
}
enum GrantLevel {
GRANT_LEVEL_UNSPECIFIED = 0;
// git-upload-pack (clone/fetch).
GRANT_LEVEL_READ = 1;
// git-receive-pack (push). Implies READ.
GRANT_LEVEL_WRITE = 2;
// Control-plane mutation. Implies WRITE.
GRANT_LEVEL_ADMIN = 3;
}
enum ObjectKind {
OBJECT_KIND_UNSPECIFIED = 0;
OBJECT_KIND_BLOB = 1;
OBJECT_KIND_TREE = 2;
OBJECT_KIND_COMMIT = 3;
OBJECT_KIND_TAG = 4;
}
// The address of a repository: `<namespace>/<repo>`, matching the SSH path
// `git@host:<namespace>/<repo>.git`. Both halves are required; an empty
// namespace is INVALID_ARGUMENT, never a default.
message RepoPath {
string namespace = 1;
string repo = 2;
}
message Namespace {
string name = 1;
string description = 2;
int64 created_at_unix_ms = 3;
// Whose ACCOUNT this namespace is: a principal's id is the namespace it
// intrinsically owns, and this is that principal's kind. UNSPECIFIED: no
// principal of that id (a namespace made on its own). A reader that sells
// or bills per namespace (monetize's gunnar plugin) leaves out the
// accounts of MACHINE principals — the front's certify key, the console,
// the guest, the twin, the installer's seed, monetize itself, a fed
// reader — which are the estate's own parts, never a customer.
PrincipalKind owner_kind = 4;
// 5 was `bool enterprise`, a flag an operator set, on the release-3 merge
// branch only (never released): there is no such flag. An account is an
// enterprise account when its admin has enrolled employees (`employees`).
reserved 5;
reserved "enterprise";
// The namespace is PUBLIC: shown to everyone, even while it holds no public
// repository. False — the default for every account — is private: an
// outsider learns of it only through a public repository in it, and one
// holding none answers exactly as a namespace that does not exist. Only the
// account admin changes it (Repos.SetNamespaceSettings).
bool public = 6;
// The account's emitted events are PUBLIC. False — the default — shows them
// to nobody outside the account (Events.Watch). Only the account admin
// changes it (Repos.SetNamespaceSettings).
bool events_public = 7;
// How many employees the account's admin has enrolled (Accounts.
// EnrollEmployee). An account with any is an ENTERPRISE account: derived,
// never set. Told to whoever may list the namespace.
uint32 employees = 8;
// The namespace's owner holds a SERVER-scope grant — the operator, the SELLER (2026-10-08):
// the shop's own, never a customer. A selling shop gives every person's account a fact
// (no account there is "no fact = uncapped") and leaves these out, naming them in its log.
// Only an operator grants Scope::Server; nothing a customer does sets it.
bool owner_server_grant = 9;
}
message Repo {
RepoPath path = 1;
HashKind hash_kind = 2;
// e.g. "refs/heads/main", a full ref name, not a short branch name.
string default_branch = 3;
Visibility visibility = 4;
int64 created_at_unix_ms = 5;
string description = 6;
// An ENCRYPTED repository: the server refuses any push that would put
// plaintext on it, and holds no key. Only the account admin sets it
// (`Repos.SetEncrypted`). See gunnar's `.nornir/encrypted-repos.md`.
bool encrypted = 7;
}
// A resolved ref. `oid` is the hex object id the ref points at; for a symbolic
// ref (HEAD) `symbolic_target` carries the ref it points to. `peeled_oid` is
// set only for annotated tags, and is the commit the tag ultimately names.
message RefTarget {
string ref_name = 1;
string oid = 2;
string symbolic_target = 3;
string peeled_oid = 4;
}
// One ref moving. Shared by receive-pack reporting, the event stream,
// attestation and mirror sync: one shape, four consumers (one
// implementation). A deletion is new_oid = all-zeroes; a creation is old_oid =
// all-zeroes.
message RefUpdate {
string ref_name = 1;
string old_oid = 2;
string new_oid = 3;
}
message Principal {
string id = 1;
string name = 2;
PrincipalKind kind = 3;
int64 created_at_unix_ms = 4;
}
// Identity comes from the KEY, not from the SSH username.
message PublicKey {
// "SHA256:...", the OpenSSH-format sha256 fingerprint. The primary key.
string fingerprint = 1;
// "ssh-ed25519", "ecdsa-sha2-nistp256", ...
string algo = 2;
string principal_id = 3;
string comment = 4;
int64 added_at_unix_ms = 5;
optional int64 last_used_unix_ms = 6;
optional int64 expires_at_unix_ms = 7;
}
// A grant scopes to a NAMESPACE or to a single REPO. An empty `repo`
// means the grant covers the whole namespace.
message Grant {
string principal_id = 1;
string namespace = 2;
string repo = 3;
GrantLevel level = 4;
int64 granted_at_unix_ms = 5;
}
// git's author/committer identity line.
message Signature {
string name = 1;
string email = 2;
int64 time_unix_seconds = 3;
int32 tz_offset_minutes = 4;
}
message Commit {
string oid = 1;
repeated string parent_oids = 2;
string tree_oid = 3;
Signature author = 4;
Signature committer = 5;
string message = 6;
}
// One clock-sync attempt (`gunnar_clock::report::Report`), served on
// `Admin.Health` and `Bootstrap.Status`. Unset when no attempt has run in this
// process.
message ClockSync {
// Why it ran: "boot" | "skew-refusal" | "timer".
string trigger = 1;
// This server's clock when the attempt finished (after any step).
int64 at_unix_ms = 2;
// "not-configured" | "off" | "misconfigured" | "stepped" | "slewed" |
// "already-right" | "no-quorum" | "disagree" | "apply-failed"
string outcome = 3;
// The decided correction (source minus this clock); 0 when none was decided.
int64 offset_ms = 4;
// One sentence for a human.
string detail = 5;
repeated ClockSource sources = 6;
}
// One configured source's part in a ClockSync.
message ClockSource {
// As configured: "peer://10.13.8.99:50123", "https://…", "ntp://…".
string source = 1;
bool answered = 2;
// Source minus this clock, when it answered.
int64 offset_ms = 3;
// Why it contributed nothing, when it did not.
string error = 4;
}