gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **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)>;
}