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/clans.proto

syntax = "proto3";

package gunnar.v1;

import "gunnar/v1/common.proto";

// **Clans** (owner, 2026-10-03: "ordinary accounts can group with other in a
// clan, where group created repos are administered by the group admin";
// 2026-10-05: "when built, each member contributes storage to the clan").
//
// A clan is a group of ORDINARY accounts with a namespace of its own — the
// clan's name. Like every account's, that namespace and its events are NOT
// public until the clan's admin says so (`Repos.SetNamespaceSettings`).
//
// * **Who.** The creator is the clan's first admin. An admin invites an
//   account; the account accepts (or declines). Only a person's own account
//   joins — never an enterprise account's EMPLOYEE (its account admin's rules
//   win), never a machine. An admin may make another member admin; the LAST
//   admin can neither leave nor be made a plain member.
// * **What.** A member reads and writes every repository in the clan's
//   namespace and creates PRIVATE ones there; the clan's admin administers
//   them: visibility (only the admin makes one public, as on an account), who
//   else may reach them (grants), deletion.
// * **Room.** Each member contributes part of its OWN room (what its account
//   holds and has neither stored nor promised); the clan's room is the sum. A
//   contribution goes down only as far as what the clan stores leaves room
//   for; leaving returns it, refused while it is in use. Nothing is bought
//   here.
//
// AUTHORISATION: every verb is the caller's own (the self-scope: the console
// signs as the person) and gunnar decides from the clan — an admin's verbs
// refuse anybody who is not the clan's admin, a server grant included. To
// anybody neither in a clan nor invited to it, the clan does not exist.
service Clans {
  // Make a clan: its name (= its new namespace, private) and the caller's
  // contribution. The caller becomes its admin.
  rpc Create(CreateClanRequest) returns (ClanResponse);
  // One clan, as the caller may see it.
  rpc Get(GetClanRequest) returns (ClanResponse);
  // The caller's clans and invitations.
  rpc List(ListClansRequest) returns (ListClansResponse);
  // An admin invites an ordinary account.
  rpc Invite(InviteToClanRequest) returns (ClanResponse);
  // The invited account joins, giving `contribution_bytes` of its own room.
  rpc Accept(AcceptClanInviteRequest) returns (ClanResponse);
  // The invited account declines; or an admin withdraws an invitation.
  rpc Decline(DeclineClanInviteRequest) returns (ClanResponse);
  // A member sets its own contribution.
  rpc SetContribution(SetClanContributionRequest) returns (ClanResponse);
  // A member leaves; its contribution goes back to its own account.
  rpc Leave(LeaveClanRequest) returns (ClanResponse);
  // An admin removes a member (not itself).
  rpc RemoveMember(RemoveClanMemberRequest) returns (ClanResponse);
  // An admin makes a member admin, or not.
  rpc SetAdmin(SetClanAdminRequest) returns (ClanResponse);
  // An admin dissolves a clan that holds no repository: every contribution
  // goes back and the namespace goes.
  rpc Dissolve(DissolveClanRequest) returns (DissolveClanResponse);

  // **Move a repository out of the clan** (owner, 2026-10-05: "to dissolve,
  // the admin first moves the clan's repos out"). The clan's admin offers one
  // of the clan's repositories to an account; the ACCOUNT's admin (Admin over
  // its namespace) accepts — then gunnar moves it, everything it holds, to
  // `<account>/<repo>` and the clan's room is freed — or declines. Both are
  // in the audit trail. One open offer per repository.
  rpc OfferRepo(OfferRepoRequest) returns (ClanResponse);
  // The offered account's admin accepts (the move happens now) or declines;
  // the clan's admin withdraws (`accept` false).
  rpc AnswerRepoOffer(AnswerRepoOfferRequest) returns (AnswerRepoOfferResponse);
}

// Where an account stands in a clan.
enum ClanRole {
  CLAN_ROLE_UNSPECIFIED = 0;
  CLAN_ROLE_MEMBER = 1;
  CLAN_ROLE_ADMIN = 2;
  CLAN_ROLE_INVITED = 3;
}

message ClanMember {
  // The member's account (principal id).
  string principal_id = 1;
  bool admin = 2;
  // Bytes of its own room it gives the clan.
  uint64 contribution_bytes = 3;
  int64 joined_at_unix_ms = 4;
  // **Its share is not in force** (owner, 2026-10-05: a member who stops
  // paying — the clan keeps its share until its paid period ends, then the
  // share drops). The row stays; the door no longer counts it.
  bool lapsed = 5;
}

// **A repository offered out of a clan** to an account, waiting for that
// account's admin.
message RepoOffer {
  // The clan it is offered out of.
  string clan = 1;
  // The repository's name in the clan (`<clan>/<repo>` now, `<to>/<repo>`
  // once accepted).
  string repo = 2;
  // The account it is offered to.
  string to_account = 3;
  string offered_by = 4;
  int64 at_unix_ms = 5;
  // What it stores (pack and LFS bytes), as gunnar measured it — what the
  // account's room must hold. Unset when not measured.
  optional uint64 stored_bytes = 6;
}

message ClanInvite {
  string principal_id = 1;
  string invited_by = 2;
  int64 at_unix_ms = 3;
}

// One clan, as the caller may see it. Every number is gunnar's; a console
// draws them and decides nothing.
message Clan {
  // The clan's name = its namespace.
  string name = 1;
  string created_by = 2;
  int64 created_at_unix_ms = 3;
  repeated ClanMember members = 4;
  // Pending invitations: all of them to an admin, the caller's own to an
  // invited account, none to a plain member.
  repeated ClanInvite invites = 5;
  // The clan's room: the sum of its members' contributions.
  uint64 room_bytes = 6;
  // What the clan stores (its namespace's pack bytes). Unset: not measured —
  // never 0 for "unknown".
  optional uint64 used_bytes = 7;
  // The caller's standing.
  ClanRole your_role = 8;
  // The caller's own contribution (0 unless a member).
  uint64 your_contribution_bytes = 9;
  // How low the caller's contribution may go while the clan stores what it
  // does. Unset when `used_bytes` is.
  optional uint64 your_floor_bytes = 10;
  // What the caller's own account could still give (its room less what it
  // stores, promises and gives its clans). Unset: nothing caps the caller's
  // account on this server.
  optional uint64 your_room_left_bytes = 11;
  // **The room the door holds the clan to now**: the shares of members whose
  // paid period has ended are dropped (`ClanMember.lapsed`). Equal to
  // `room_bytes` when no share has lapsed, and on a server that sells
  // nothing.
  uint64 room_in_force_bytes = 12;
  // Repositories offered out of the clan and not yet answered: to an admin
  // all of them, to a plain member none.
  repeated RepoOffer offers = 13;
}

message ClanResponse {
  Clan clan = 1;
}

message CreateClanRequest {
  // The clan's name, which becomes its namespace: a free name (no account's,
  // no namespace's, no clan's).
  string name = 1;
  // Bytes of the creator's own room it gives the clan (0 allowed).
  uint64 contribution_bytes = 2;
}

message GetClanRequest {
  string clan = 1;
}

message ListClansRequest {}
message ListClansResponse {
  // The clans the caller is a member of, and those it is invited to
  // (`your_role` says which), by name.
  repeated Clan clans = 1;
  // Repositories offered to the caller's OWN account, out of any clan —
  // its to accept or decline.
  repeated RepoOffer offers = 2;
}

message InviteToClanRequest {
  string clan = 1;
  // The account invited (its principal id).
  string principal_id = 2;
}

message AcceptClanInviteRequest {
  string clan = 1;
  uint64 contribution_bytes = 2;
}

message DeclineClanInviteRequest {
  string clan = 1;
  // Empty or the caller: the caller declines its own invitation. Another
  // account: an admin withdraws that account's invitation.
  string principal_id = 2;
}

message SetClanContributionRequest {
  string clan = 1;
  uint64 contribution_bytes = 2;
}

message LeaveClanRequest {
  string clan = 1;
}

message RemoveClanMemberRequest {
  string clan = 1;
  string principal_id = 2;
}

message SetClanAdminRequest {
  string clan = 1;
  string principal_id = 2;
  bool admin = 3;
}

message OfferRepoRequest {
  string clan = 1;
  // The repository's name in the clan.
  string repo = 2;
  // The account it is offered to (its name = its namespace).
  string account = 3;
}

message AnswerRepoOfferRequest {
  string clan = 1;
  string repo = 2;
  // True: the offered account's admin takes it (the move happens now).
  // False: that admin declines — or the clan's admin withdraws the offer.
  bool accept = 3;
}
message AnswerRepoOfferResponse {
  // The offer as it was answered.
  RepoOffer offer = 1;
  // The repository at its new place (accepted), unset otherwise.
  Repo repo = 2;
}

message DissolveClanRequest {
  string clan = 1;
}
message DissolveClanResponse {
  // The clan as it was.
  Clan clan = 1;
}