crates/gunnar-grpc/src/clans.rs
//! **Clans at the seam** (owner, 2026-10-05): what the `Clans` service asks of
//! a plane that keeps an identity table.
//!
//! The decisions — who is the clan's admin, whether an account is an ordinary
//! one, whether a contribution fits or goes below what the clan stores — are
//! the identity table's (`gunnar_core::AuthTable`, in one compare-and-swap with
//! the grants they widen). The service measures what only the stores know (the
//! caller's room, what the clan stores) and hands it in, as
//! `Accounts.EnrollEmployee` hands in its room.
//!
//! A plane without clans answers [`crate::seam::ControlPlane::clans`] with
//! `None`, and every `Clans` verb is then UNIMPLEMENTED — never an empty list
//! that would read as "you are in no clan".
use crate::seam::ControlResult;
/// **A member's own account's room**, measured by the service: its cap
/// (`None`: nothing caps it on this server) and what it stores.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ClanRoom {
pub cap: Option<u64>,
pub stored: u64,
}
/// **The clan half of a plane.** Every write answers the clan as it stands
/// after it, with `name`, `created_by`, `created_at_unix_ms`, `members`,
/// `invites` and `room_bytes` filled; the service fills what the caller sees
/// of it (`used_bytes`, `your_*`) and narrows `invites`.
pub trait ClanPlane: Send + Sync {
/// Make clan `name` (and its namespace, private), `creator` its admin,
/// giving `contribution` bytes. `AlreadyExists` for a taken name.
fn clan_create(
&self,
name: &str,
creator: &str,
contribution: u64,
room: ClanRoom,
) -> ControlResult<gunnar_proto::Clan>;
/// One clan, if there is one.
fn clan_get(&self, name: &str) -> ControlResult<Option<gunnar_proto::Clan>>;
/// The clans `principal` is in or invited to.
fn clans_of(&self, principal: &str) -> ControlResult<Vec<gunnar_proto::Clan>>;
/// An admin (`by`) invites `principal`.
fn clan_invite(
&self,
clan: &str,
by: &str,
principal: &str,
) -> ControlResult<gunnar_proto::Clan>;
/// `principal` answers its invitation; joining gives `contribution`.
fn clan_answer(
&self,
clan: &str,
principal: &str,
accept: bool,
contribution: u64,
room: ClanRoom,
) -> ControlResult<gunnar_proto::Clan>;
/// An admin withdraws `principal`'s invitation.
fn clan_withdraw(
&self,
clan: &str,
by: &str,
principal: &str,
) -> ControlResult<gunnar_proto::Clan>;
/// `member` sets its own contribution; `used`: what the clan stores
/// (`None`: not measured, and a lowering is refused).
fn clan_set_contribution(
&self,
clan: &str,
member: &str,
bytes: u64,
room: ClanRoom,
used: Option<u64>,
) -> ControlResult<gunnar_proto::Clan>;
/// `member` leaves.
fn clan_leave(
&self,
clan: &str,
member: &str,
used: Option<u64>,
) -> ControlResult<gunnar_proto::Clan>;
/// An admin removes `member`.
fn clan_remove(
&self,
clan: &str,
by: &str,
member: &str,
used: Option<u64>,
) -> ControlResult<gunnar_proto::Clan>;
/// An admin makes `member` admin, or not.
fn clan_set_admin(
&self,
clan: &str,
by: &str,
member: &str,
admin: bool,
) -> ControlResult<gunnar_proto::Clan>;
/// An admin dissolves the clan; refused while its namespace holds any
/// repository (a deleted one still held included). The namespace goes.
fn clan_dissolve(&self, clan: &str, by: &str) -> ControlResult<gunnar_proto::Clan>;
/// What `account` could still give a clan (`None`: uncapped).
fn clan_room_left(&self, account: &str, room: ClanRoom) -> ControlResult<Option<u64>>;
/// What `account` gives all its clans, bytes — its own room is its cap
/// less this (`Usage.Read`'s allowance, the door's ceiling).
fn clan_given(&self, account: &str) -> ControlResult<u64>;
/// Where `principal` stands in the clan whose namespace is `namespace`
/// (`None`: not a clan, or not in it) — what `Repos.Create` asks.
fn clan_role(
&self,
principal: &str,
namespace: &str,
) -> ControlResult<Option<gunnar_proto::ClanRole>>;
// ── round 2 (owner, 2026-10-05): moving a repository out of a clan ─────
//
// The clan's admin offers; the offered ACCOUNT's admin takes or declines
// (the service asks gunnar's own rule — Admin over the account's
// namespace — before it calls `clan_take_offer` or `clan_decline_offer`).
/// The clan's admin `by` offers `clan/repo` to account `to`. `NotFound`
/// for a repository the clan does not hold.
fn clan_offer_repo(
&self,
clan: &str,
by: &str,
repo: &str,
to: &str,
) -> ControlResult<gunnar_proto::Clan>;
/// The open offer of `clan/repo`, with what the repository stores.
fn clan_repo_offer(
&self,
clan: &str,
repo: &str,
) -> ControlResult<Option<gunnar_proto::RepoOffer>>;
/// Every open offer to account `to`.
fn clan_offers_to(&self, to: &str) -> ControlResult<Vec<gunnar_proto::RepoOffer>>;
/// The clan's admin withdraws the offer of `clan/repo`.
fn clan_withdraw_offer(
&self,
clan: &str,
by: &str,
repo: &str,
) -> ControlResult<gunnar_proto::Clan>;
/// Account `to` declines the offer of `clan/repo`.
fn clan_decline_offer(
&self,
clan: &str,
repo: &str,
to: &str,
) -> ControlResult<gunnar_proto::RepoOffer>;
/// Account `to` takes it: the repository MOVES to `<to>/<repo>` — its
/// objects, references, HEAD, LFS objects and branch policy, through the
/// paths every push takes — and the clan's copy is purged. The offer as
/// it was, and the repository at its new place.
fn clan_take_offer(
&self,
clan: &str,
repo: &str,
to: &str,
by: &str,
) -> ControlResult<(gunnar_proto::RepoOffer, gunnar_proto::Repo)>;
}