Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-server/proto/gunnar/v1/enterprise.proto

syntax = "proto3";

package gunnar.v1;

// **The owner's enterprise list** (2026-10-05): the account admin's audit
// log, the operator's aggregates and the account admin's grant of a look,
// and the name filter. Nothing here is bought: every account has it.
//
// The account model (owner, 2026-10-03): one account type; the person who
// starts it is its admin; employees are NEW users the admin enrolls; private
// by default; only the account admin makes things public. The OPERATOR (the
// estate's server admin) runs the box and sees totals — never an account's
// repository names, member names or events — unless the account's admin
// approves a time-limited look (`ApproveOperatorAccess`). That rule is in
// gunnar's ACL (a server-scope grant stops at an account's door), not in a
// console: every listing the operator calls is narrowed by the same rule.
//
// gunnar decides everything here; a console only relays and renders.
service Enterprise {
  // ── The account admin's audit log ──────────────────────────────────────
  //
  // Every security-relevant event in the account: logins (passkey, SSO),
  // credentials added and removed, invites, enrolments, members added and
  // deactivated, visibility changes, repositories created and deleted,
  // branch protection, approvals, billing, the operator's looks, names the
  // filter stopped. Append-only and hash-chained per account (each entry
  // names the one before it, the account hashed in); kept a year by default.
  //
  // AUTHORISATION: an ADMIN OF THE ACCOUNT — its own person, or a principal
  // its admin made Admin over the namespace. A server-scope grant is not:
  // the operator never reads an account's log, approved look or not.
  rpc AuditLog(AuditLogRequest) returns (AuditLogResponse);
  // The same entries, whole, as CSV or JSON (a file to keep). The export is
  // itself an entry in the log.
  rpc ExportAuditLog(ExportAuditLogRequest) returns (ExportAuditLogResponse);

  // ── The operator's view ────────────────────────────────────────────────
  //
  // **Totals only**: how many accounts, members, repositories; storage used
  // and sold; health. No name of anything inside an account. Server `Read`.
  rpc Aggregates(AggregatesRequest) returns (AggregatesResponse);

  // ── The operator's look, which the account admin grants ────────────────
  //
  // The operator ASKS (server `Admin`): an account, a reason, how long. It is
  // pending — nothing changes — until the account's admin approves it with a
  // FRESH user-verified step-up of their own passkey (verified here, as a
  // machine enrolment's approval is), or refuses it. An approved look lets
  // the operator READ inside the account until it ends on its own or the
  // admin ends it. Every step is in the account's audit log.
  rpc RequestOperatorAccess(RequestOperatorAccessRequest) returns (RequestOperatorAccessResponse);
  // `account` set: that account's looks (its admin). Empty: the caller's
  // own requests (the operator).
  rpc ListOperatorAccess(ListOperatorAccessRequest) returns (ListOperatorAccessResponse);
  // The account admin, with the step-up.
  rpc ApproveOperatorAccess(ApproveOperatorAccessRequest) returns (ApproveOperatorAccessResponse);
  // The account admin.
  rpc RefuseOperatorAccess(RefuseOperatorAccessRequest) returns (RefuseOperatorAccessResponse);
  // The account admin, or the operator that asked: ends it now.
  rpc RevokeOperatorAccess(RevokeOperatorAccessRequest) returns (RevokeOperatorAccessResponse);

  // ── The name filter ────────────────────────────────────────────────────
  //
  // Account, namespace, member and repository names are put to it where
  // gunnar makes them (AddPrincipal, RenamePrincipal, EnrollEmployee,
  // CreateNamespace, Repos.Create). A blocked or reserved name is refused
  // with a sentence and the account admin is told; a borderline one is held
  // for the ACCOUNT ADMIN's review (never the operator's).
  //
  // Ask without making anything (any authenticated caller): what would the
  // filter say?
  rpc CheckName(CheckNameRequest) returns (CheckNameResponse);
  // The lists in force — gunnar's configuration (server `Read`).
  rpc GetNameLists(GetNameListsRequest) returns (NameLists);
  // Replace them (server `Admin`).
  rpc SetNameLists(SetNameListsRequest) returns (NameLists);
  // The account's queue: held names to decide, refused names to know about
  // (an admin of the account).
  rpc ListNameReviews(ListNameReviewsRequest) returns (ListNameReviewsResponse);
  // Allow or refuse a held name (an admin of the account). Allowed, the same
  // name passes in this account from now on.
  rpc DecideNameReview(DecideNameReviewRequest) returns (DecideNameReviewResponse);
}

// One entry of an account's audit log.
message AuditEntry {
  // 0 for the account's first entry, never reused.
  uint64 seq = 1;
  int64 at_unix_ms = 2;
  // `family.verb`: `login`, `credential.added`, `visibility.changed` …
  string kind = 3;
  // The principal that did it; empty for the system.
  string actor = 4;
  // What the actor is to this account: `admin`, `member`, `operator`,
  // `machine`, `outsider`, `system`.
  string actor_role = 5;
  // What it was done to: a repository, a principal, a credential.
  string target = 6;
  string detail = 7;
  // `ok` or `refused`.
  string outcome = 8;
  // The previous entry's hash, and this entry's: the chain.
  string prev = 9;
  string hash = 10;
}

// What to show. Every field optional.
message AuditFilter {
  // Kinds or families (`login`, `visibility`, `repo.deleted`).
  repeated string kinds = 1;
  string actor = 2;
  int64 since_unix_ms = 3;
  int64 until_unix_ms = 4;
  // A substring of the target, the detail, the actor or the kind.
  string text = 5;
}

// Does the account's chain hold, end to end?
message AuditChain {
  bool holds = 1;
  uint64 entries = 2;
  // The newest entry's hash: what an admin records to notice a cut tail.
  string tip_hash = 3;
  // The last entry retention dropped, whose hash the first kept one names.
  optional uint64 anchor_seq = 4;
  optional uint64 broken_at_seq = 5;
  string what = 6;
}

message AuditLogRequest {
  // The account (its namespace).
  string account = 1;
  AuditFilter filter = 2;
  // 0 = 50; at most 1000.
  uint32 page_size = 3;
  // The previous page's `next_before_seq`: only older entries.
  optional uint64 before_seq = 4;
}
message AuditLogResponse {
  string account = 1;
  // Newest first.
  repeated AuditEntry entries = 2;
  // Ask with `before_seq` = this for the next page; unset on the last.
  optional uint64 next_before_seq = 3;
  AuditChain chain = 4;
  // How long entries are kept, days.
  uint32 retention_days = 5;
  // Entries ever written to this account's log.
  uint64 written = 6;
}

enum AuditExportFormat {
  AUDIT_EXPORT_FORMAT_UNSPECIFIED = 0;
  AUDIT_EXPORT_FORMAT_CSV = 1;
  AUDIT_EXPORT_FORMAT_JSON = 2;
}

message ExportAuditLogRequest {
  string account = 1;
  AuditFilter filter = 2;
  // UNSPECIFIED is CSV.
  AuditExportFormat format = 3;
}
message ExportAuditLogResponse {
  bytes body = 1;
  // `text/csv; charset=utf-8` or `application/json`.
  string content_type = 2;
  // `audit-<account>-<UTC date>.csv`
  string file_name = 3;
  uint64 entries = 4;
}

message AggregatesRequest {}
message AggregatesResponse {
  // Accounts: people who own a namespace (employees are members, not accounts).
  uint64 accounts = 1;
  // Accounts whose admin has enrolled employees.
  uint64 enterprise_accounts = 2;
  // Employees across every account.
  uint64 members = 3;
  // Repositories on the whole estate — inside accounts AND the operator's own
  // namespaces — and how many of them are public.
  uint64 repos = 4;
  uint64 public_repos = 5;
  // Bytes stored on the whole estate (gunnar's measure, pack + LFS), and the
  // room held (the sum of every plan's pack cap; 0 on a box that sells none).
  uint64 stored_bytes = 6;
  uint64 sold_bytes = 7;
  // Accounts holding a paid plan.
  uint64 paid_accounts = 8;
  // The operator's looks: approved and live, and waiting for an admin.
  uint64 looks_live = 9;
  uint64 looks_pending = 10;
  // `ok`, or `alarm` with how many health alarms the box raises.
  string health = 11;
  uint32 health_alarms = 12;
  int64 at_unix_ms = 13;
  // The operator's SHARE of `repos`: what is NOT inside an account (the
  // operator's own namespaces). Shown by name elsewhere; already in `repos`.
  uint64 operator_repos = 14;
}

enum OperatorAccessState {
  OPERATOR_ACCESS_STATE_UNSPECIFIED = 0;
  OPERATOR_ACCESS_STATE_PENDING = 1;
  OPERATOR_ACCESS_STATE_APPROVED = 2;
  OPERATOR_ACCESS_STATE_REFUSED = 3;
  OPERATOR_ACCESS_STATE_REVOKED = 4;
  OPERATOR_ACCESS_STATE_EXPIRED = 5;
}

// One operator's look into one account.
message OperatorAccess {
  string request_id = 1;
  // For the eye (`ABC-123`): shown to the operator and beside the request.
  string code = 2;
  string account = 3;
  string operator = 4;
  string reason = 5;
  // How long the look lasts once approved.
  uint32 minutes = 6;
  int64 requested_at_unix_ms = 7;
  int64 request_expires_at_unix_ms = 8;
  OperatorAccessState state = 9;
  int64 granted_until_unix_ms = 10;
  string decided_by = 11;
  int64 decided_at_unix_ms = 12;
}

message RequestOperatorAccessRequest {
  string account = 1;
  // Required: the account admin reads it before deciding.
  string reason = 2;
  // 0 = an hour; at most a week (10080).
  uint32 minutes = 3;
}
message RequestOperatorAccessResponse {
  OperatorAccess request = 1;
}

message ListOperatorAccessRequest {
  string account = 1;
}
message ListOperatorAccessResponse {
  // Newest first.
  repeated OperatorAccess requests = 1;
}

message ApproveOperatorAccessRequest {
  string request_id = 1;
  // The code shown beside the request: the approval is for THAT request.
  string code = 2;
  // 0 = the minutes asked for; otherwise 1 to 10080.
  uint32 minutes = 3;
  // Was the approver's step-up assertion; the approver's identity-core
  // session must be stepped up instead.
  reserved 4;
}
message ApproveOperatorAccessResponse {
  OperatorAccess request = 1;
}

message RefuseOperatorAccessRequest {
  string request_id = 1;
}
message RefuseOperatorAccessResponse {
  OperatorAccess request = 1;
}

message RevokeOperatorAccessRequest {
  string request_id = 1;
}
message RevokeOperatorAccessResponse {
  OperatorAccess request = 1;
}

message CheckNameRequest {
  // `account`, `member`, `namespace` or `repo`.
  string kind = 1;
  string name = 2;
  // The account it would live in (a held name another admin allowed there
  // passes); empty for a new account's own name.
  string account = 3;
}
message CheckNameResponse {
  // `clean`, `reserved`, `blocked`, `borderline`.
  string verdict = 1;
  // The sentence a refused person reads; empty when clean.
  string sentence = 2;
}

// The filter's lists: gunnar's configuration.
message NameLists {
  repeated string blocked = 1;
  repeated string borderline = 2;
  repeated string reserved = 3;
  repeated string reserved_prefixes = 4;
  repeated string allowed = 5;
}
message GetNameListsRequest {}
message SetNameListsRequest {
  NameLists lists = 1;
}

// One name the filter stopped, in its account's queue.
message NameReview {
  string id = 1;
  string account = 2;
  string kind = 3;
  string name = 4;
  // Who asked for it.
  string actor = 5;
  // `blocked`, `reserved`, `borderline`.
  string verdict = 6;
  // `pending`, `allowed`, `refused`.
  string state = 7;
  int64 at_unix_ms = 8;
  string decided_by = 9;
  int64 decided_at_unix_ms = 10;
}
message ListNameReviewsRequest {
  string account = 1;
}
message ListNameReviewsResponse {
  // Newest first.
  repeated NameReview reviews = 1;
  uint32 pending = 2;
}
message DecideNameReviewRequest {
  string account = 1;
  string id = 2;
  bool allow = 3;
}
message DecideNameReviewResponse {
  NameReview review = 1;
}