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;
}