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

nordisk/gunnar

crates/gunnar-grpc/src/service/accounts.rs

//! `Accounts`: the account gate, and the per-user hard purge.
//!
//! # Why every RPC here is Admin at SERVER scope
//!
//! [`crate::service::ServiceContext::require_repo_or_namespace`] exists because
//! a repo-scoped check on a repository the catalog has lost refuses everybody,
//! and the answer there was to widen to the namespace. **The pressure here runs
//! the other way.** An account IS a namespace — `AuthTable::owns_account` gives
//! every principal Admin on the namespace named after it, intrinsically and
//! unrevocably, so `Access::namespace(account)` is a check the subject of the
//! call always passes.
//!
//! For `Repos.Purge`, whose blast radius is one repository the caller already
//! owns, that is correct and it was chosen deliberately. For these verbs it
//! would mean:
//!
//! * **`Purge`** — any user can hard-purge their own account with one call, no
//!   window and no undo. `Repos.Delete` goes to lengths to make a fat finger
//!   recoverable; a self-serve verb that reclaims every repository, every
//!   tombstone and every orphan in one request would undo all of it, and a
//!   stolen SSH key would be able to end an account rather than merely read it.
//! * **`Gate`** — any user can lock themselves out, and then cannot `Ungate`,
//!   because [`crate::seam::Identity::may`] answers `false` for a gated
//!   principal at every level including `Server`/`Admin`. A self-brick with no
//!   exit but another operator.
//!
//! So `Access::Server` at `GrantLevel::Admin` is not the *wide* choice here, it
//! is the narrowest scope that is **not self-granting**. `Repos.ListOrphans`
//! and `Repos.ReclaimOrphan` reached the same place from the other direction:
//! the answer spans tenants, so the grant must too.
//!
//! `Repos.Gc` argued its way DOWN to `Write` because it destroys nothing and a
//! verb gated tighter than its blast radius gets used never. Every verb here
//! destroys something no backup on this box can restore, so the same reasoning
//! points the opposite way.

use gunnar_proto::accounts_server::Accounts;
use gunnar_proto::GrantLevel;
use gunnar_proto::{
    AccountGate, ClaimOperatorRequest, ClaimOperatorResponse, EnrollEmployeeRequest,
    EnrollEmployeeResponse, GateAccountRequest, GateAccountResponse, GateReason,
    ListAccountGatesRequest, ListAccountGatesResponse, ListEmployeesRequest, ListEmployeesResponse,
    PurgeAccountRequest, PurgeAccountResponse, PurgeAccountStatusRequest,
    PurgeAccountStatusResponse, PurgeJob, PurgeState, UngateAccountRequest, UngateAccountResponse,
};
use tonic::{Request, Response, Status};

use crate::auth::Access;
use crate::seam::{self, EmployeeAsk, PurgeAccountOptions};
use crate::service::AccountsService;

/// The seam's gate onto the wire.
fn gate_out(gate: seam::AccountGate) -> AccountGate {
    AccountGate {
        account: gate.account,
        reason: match gate.reason {
            seam::GateReason::Administrative => GateReason::Administrative,
            seam::GateReason::Purge => GateReason::Purge,
        } as i32,
        holder: gate.holder,
        actor: gate.actor,
        note: gate.note,
        at_unix_ms: gate.at_unix_ms,
        expires_unix_ms: gate.expires_unix_ms,
    }
}

/// The seam's job onto the wire.
fn job_out(job: seam::PurgeJob) -> PurgeJob {
    PurgeJob {
        job_id: job.job_id,
        account: job.account,
        state: match job.state {
            seam::PurgeState::Queued => PurgeState::Queued,
            seam::PurgeState::Running => PurgeState::Running,
            seam::PurgeState::Done => PurgeState::Done,
            seam::PurgeState::Failed => PurgeState::Failed,
        } as i32,
        queue_position: job.queue_position,
        stores_found: job.stores_found,
        stores_purged: job.stores_purged,
        bytes_reclaimed: job.bytes_reclaimed,
        principal_removed: job.principal_removed,
        gate_released: job.gate_released,
        error: job.error.unwrap_or_default(),
        failures: job.failures,
        enqueued_at_unix_ms: job.enqueued_at_unix_ms,
        started_at_unix_ms: job.started_at_unix_ms,
        finished_at_unix_ms: job.finished_at_unix_ms,
    }
}

/// An account name that is present and not blank, or `INVALID_ARGUMENT`.
fn account_of(raw: &str, field: &'static str) -> Result<String, Status> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Err(Status::invalid_argument(format!("{field} is required")));
    }
    Ok(trimmed.to_owned())
}

/// A note or reason that is present, or `INVALID_ARGUMENT`.
///
/// **Required, and refused rather than defaulted.** The note is what the
/// locked-out user is shown in every refusal they get from that moment on. A
/// default of "locked" would produce a lockout nobody — not the user, not the
/// operator reading `ListAccountGates` next month — can explain, and an
/// unexplained lockout is indistinguishable from a bug in the ACL.
fn reason_of(raw: &str, field: &'static str) -> Result<String, Status> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Err(Status::invalid_argument(format!(
            "{field} is required: it is what the locked-out user is shown in every refusal, and \
             a lockout with no reason is a support ticket"
        )));
    }
    Ok(trimmed.to_owned())
}

/// **The most users one request adds** (owner, 2026-10-04: "can be raised
/// up to 10 000").
pub const MOST_USERS: u32 = 10_000;

/// **The room each user holds when none is asked for**, and the least one
/// may hold (owner, 2026-10-04: "default 10 GB, can be raised"): 10 GiB.
pub const ROOM_PER_USER_BYTES: u64 = 10 << 30;

const GIB: u64 = 1 << 30;

/// **The rules a person types against**, for every door at once: the number
/// (1 when unset, at most [`MOST_USERS`]), the name (required for ONE user,
/// not asked for more), the room (default and floor [`ROOM_PER_USER_BYTES`]),
/// and a seat claim (a name, no number, no room: the seat decides).
pub fn users_asked(request: &EnrollEmployeeRequest) -> Result<EmployeeAsk, Status> {
    let count = request.count.max(1);
    if count > MOST_USERS {
        return Err(Status::invalid_argument(format!(
            "`count`: at most {MOST_USERS} users at once, not {count}"
        )));
    }
    let name = request.name.trim();
    let id = match request.id.trim() {
        "" => name,
        id => id,
    };
    if request.from_seat {
        if count > 1 || request.room_bytes > 0 {
            return Err(Status::invalid_argument(
                "`from_seat` fills ONE seat with the room the seat holds: send no `count` and no \
                 `room_bytes`",
            ));
        }
        if name.is_empty() {
            return Err(Status::invalid_argument(
                "`name`: the user who fills the seat needs a name",
            ));
        }
    } else if count == 1 && name.is_empty() {
        return Err(Status::invalid_argument("`name`: one user needs a name"));
    } else if count > 1
        && (!name.is_empty()
            || !request.id.trim().is_empty()
            || !request.public_key.trim().is_empty())
    {
        return Err(Status::invalid_argument(format!(
            "{count} users get no names here: each seat is named when an invite or an import fills \
             it. Send `name` only for one user"
        )));
    }
    let room_bytes = match request.room_bytes {
        _ if request.from_seat => 0,
        0 => ROOM_PER_USER_BYTES,
        room if room < ROOM_PER_USER_BYTES => {
            return Err(Status::invalid_argument(format!(
                "`room_bytes`: each user holds at least {} GiB (it can be raised, not lowered); {} \
                 was asked",
                ROOM_PER_USER_BYTES / GIB,
                human_bytes(room)
            )))
        }
        room => room,
    };
    let named = request.from_seat || count == 1;
    Ok(EmployeeAsk {
        id: if named { id.to_owned() } else { String::new() },
        name: if named {
            name.to_owned()
        } else {
            String::new()
        },
        room_bytes,
        seats: if named { 0 } else { count },
        from_seat: request.from_seat,
        cap: None,
        stored: 0,
        order: request.order.trim().to_owned(),
        dry_run: request.dry_run,
    })
}

/// **The refusal of a short account**: what it has unused, what was asked,
/// and how much is missing — the one sentence every plane shares.
fn room_short(account: &str, ask: &EmployeeAsk, answer: &crate::seam::EmployeeAnswer) -> String {
    let asked = match ask.seats {
        0 => format!("1 user × {}", human_bytes(answer.room_bytes)),
        seats => format!("{seats} users × {}", human_bytes(answer.room_bytes)),
    };
    format!(
        "{account} has {} of room unused; {asked} need {}: {} short. Add room to the account, or \
         ask for fewer users or less room per user",
        human_bytes(answer.unused_bytes.unwrap_or(0)),
        human_bytes(answer.need_bytes),
        human_bytes(answer.shortfall_bytes),
    )
}

/// Bytes as a person reads them: `10 GiB`, `1.5 GiB`, `512 MiB`.
fn human_bytes(bytes: u64) -> String {
    const UNITS: [(u64, &str); 4] = [
        (1 << 40, "TiB"),
        (1 << 30, "GiB"),
        (1 << 20, "MiB"),
        (1 << 10, "KiB"),
    ];
    for (unit, name) in UNITS {
        if bytes >= unit {
            let tenths = (u128::from(bytes) * 10 + u128::from(unit) / 2) / u128::from(unit);
            return match tenths % 10 {
                0 => format!("{} {name}", tenths / 10),
                rest => format!("{}.{rest} {name}", tenths / 10),
            };
        }
    }
    format!("{bytes} bytes")
}

impl AccountsService {
    /// A NEW employee's first-login link, the identity core's (IDENTITY-HOST):
    /// seven days, no server-wide grant, made by the account's admin; `(id,
    /// secret, expires)`, the secret here once and only its hash kept.
    fn invite_for(
        &self,
        by: &str,
        principal: &str,
        account: &str,
    ) -> Result<(String, String, i64), Status> {
        let identity = self.ctx.identity.clone().ok_or_else(|| {
            Status::unavailable("this server serves no identity core, so it mints no link")
        })?;
        let made = identity
            .enrolment(
                principal,
                by,
                Some(crate::service::access::INVITE_DEFAULT_MS / 1000),
            )
            .map_err(crate::identity::status_of)?;
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event = "account.employee_invited",
            principal = %by,
            employee = %principal,
            account = %account,
            invite = %made.invite_id,
            "an employee's first-login link was made with the enrolment"
        );
        Ok((
            made.invite_id,
            made.secret,
            made.expires_at.saturating_mul(1000),
        ))
    }

    /// **The account's room, and what it stores**: its `pack_bytes` cap (the
    /// signed fact monetize pushed — the METERED appliance) and what gunnar
    /// measured it storing. `None`: nothing caps the account here — the
    /// community appliance, or no fact (Free). A cap with no measurement to
    /// set against it is refused rather than guessed: a meter with no reading
    /// is not zero.
    fn room_of(&self, account: &str) -> Result<Option<(u64, u64)>, Status> {
        account_room(&self.ctx, account)
    }
}

/// **An account's room and what it stores** (`pack_bytes` cap, measured
/// usage), or `None` when nothing caps it on this server — the question
/// `Accounts.EnrollEmployee` and an SSO first login (or a SCIM/CSV
/// provisioning) ask before a NEW employee takes room.
pub(crate) fn account_room(
    ctx: &crate::service::ServiceContext,
    account: &str,
) -> Result<Option<(u64, u64)>, Status> {
    account_room_at(
        ctx,
        account,
        ctx.sells_room.load(std::sync::atomic::Ordering::Relaxed),
    )
}

/// [`account_room`], told whether this estate SELLS room (the feed's `sells_room`): there a
/// PERSON's own account with NO fact holds no room — `Some((0, stored))` — until its fact
/// lands ([`crate::selling::no_room_yet`]); everywhere else no fact is uncapped, as before.
pub(crate) fn account_room_at(
    ctx: &crate::service::ServiceContext,
    account: &str,
    sells_room: bool,
) -> Result<Option<(u64, u64)>, Status> {
    #[cfg(feature = "metered")]
    {
        use crate::seam::ControlError;
        let (has_fact, cap) = match ctx.control.entitlement_get(account) {
            Ok(Some(fact)) => (
                true,
                fact.caps.get("pack_bytes").copied().filter(|cap| *cap > 0),
            ),
            Ok(None) | Err(ControlError::FailedPrecondition(_)) => (false, None),
            Err(err) => return Err(err.into()),
        };
        let cap = match cap {
            Some(cap) => cap,
            None if sells_room && !has_fact => {
                let person = matches!(ctx.control.principal(account), Ok(Some(p)) if p.kind == gunnar_proto::PrincipalKind::User as i32);
                // An unreadable mark says seller: the account stays as it was, never closed by a guess.
                let seller = ctx.control.holds_server_grant(account).unwrap_or(true);
                if !crate::selling::no_room_yet(sells_room, has_fact, person, seller) {
                    return Ok(None);
                }
                0
            }
            None => return Ok(None),
        };
        let used = ctx
            .control
            .usage(Some(account))
            .map_err(|err| {
                Status::failed_precondition(format!(
                    "{account}'s room could not be counted: what it stores was not measured \
                     ({err})"
                ))
            })?
            .into_iter()
            .find(|row| row.namespace == account)
            .map_or(0, |row| row.pack_bytes);
        Ok(Some((cap, used)))
    }
    #[cfg(not(feature = "metered"))]
    {
        let _ = (ctx, account, sells_room);
        Ok(None)
    }
}

#[tonic::async_trait]
impl Accounts for AccountsService {
    /// **`Accounts.Gate`: lock a user out.** Admin at server scope; see the
    /// module docs.
    async fn gate(
        &self,
        request: Request<GateAccountRequest>,
    ) -> Result<Response<GateAccountResponse>, Status> {
        let caller =
            self.ctx
                .authorize(&request, Access::Server, GrantLevel::Admin, "Accounts.Gate")?;
        let request = request.into_inner();
        let account = account_of(&request.account, "account")?;
        let note = reason_of(&request.note, "note")?;
        let (gate, was_already_gated) =
            self.ctx
                .control
                .gate_account(&account, &caller.principal_id, &note)?;
        // The account's own log says it was locked, and by whom: the one
        // operator act an account admin must always be able to see.
        self.ctx.account_event(
            self.ctx
                .account_of_principal(&account)
                .or_else(|| self.ctx.account_of_namespace(&account)),
            crate::enterprise::AccountAuditEvent::by(&caller, "account.gated", account.clone())
                .detail(note.clone()),
        );
        Ok(Response::new(GateAccountResponse {
            gate: Some(gate_out(gate)),
            was_already_gated,
        }))
    }

    /// **`Accounts.Ungate`: let them back in.**
    ///
    /// An account that was not gated is a SUCCESS with `lifted` unset, not a
    /// `NOT_FOUND`. The caller asked for the account to be open; it is open.
    /// Answering not-found would make the honest retry after a partial failure
    /// look like a mistake.
    async fn ungate(
        &self,
        request: Request<UngateAccountRequest>,
    ) -> Result<Response<UngateAccountResponse>, Status> {
        let caller = self.ctx.authorize(
            &request,
            Access::Server,
            GrantLevel::Admin,
            "Accounts.Ungate",
        )?;
        let account = account_of(&request.into_inner().account, "account")?;
        let lifted = self.ctx.control.ungate_account(&account)?;
        if lifted.is_some() {
            self.ctx.account_event(
                self.ctx
                    .account_of_principal(&account)
                    .or_else(|| self.ctx.account_of_namespace(&account)),
                crate::enterprise::AccountAuditEvent::by(
                    &caller,
                    "account.ungated",
                    account.clone(),
                ),
            );
        }
        Ok(Response::new(UngateAccountResponse {
            lifted: lifted.map(gate_out),
        }))
    }

    /// **`Accounts.ListAccountGates`: who is locked out.**
    ///
    /// Admin at server scope like the rest, and for `Repos.ListDeleted`'s
    /// reason as well as this service's: the answer spans tenants.
    async fn list_account_gates(
        &self,
        request: Request<ListAccountGatesRequest>,
    ) -> Result<Response<ListAccountGatesResponse>, Status> {
        self.ctx.authorize(
            &request,
            Access::Server,
            GrantLevel::Admin,
            "Accounts.ListAccountGates",
        )?;
        let gates = self.ctx.control.account_gates()?;
        Ok(Response::new(ListAccountGatesResponse {
            gates: gates.into_iter().map(gate_out).collect(),
        }))
    }

    /// **`Accounts.Purge`: gate now, walk later.**
    ///
    /// The response carries both halves on purpose. `gate` is a fact by the
    /// time this returns — the user is already locked out — and `job` is a
    /// promise. A caller that treated the job as the whole answer and started
    /// its own teardown would be racing a purge that has not begun; a caller
    /// that treated the gate as the whole answer would think an account with
    /// bytes still on it was finished.
    async fn purge(
        &self,
        request: Request<PurgeAccountRequest>,
    ) -> Result<Response<PurgeAccountResponse>, Status> {
        let caller = self.ctx.authorize(
            &request,
            Access::Server,
            GrantLevel::Admin,
            "Accounts.Purge",
        )?;
        let request = request.into_inner();
        let account = account_of(&request.account, "account")?;
        let reason = reason_of(&request.reason, "reason")?;
        self.ctx.account_event(
            self.ctx
                .account_of_principal(&account)
                .or_else(|| self.ctx.account_of_namespace(&account)),
            crate::enterprise::AccountAuditEvent::by(&caller, "account.purged", account.clone())
                .detail(reason.clone()),
        );
        let (job, gate) = self.ctx.control.purge_account(
            &account,
            PurgeAccountOptions {
                principal_id: caller.principal_id.clone(),
                reason,
                remove_principal: request.remove_principal,
                expect_empty: request.expect_empty,
            },
        )?;
        // Loud, at info, on the audit target. A hard purge is the one control
        // call whose consequence cannot be read back off the box afterwards —
        // the rows it removed are the rows that would have said what happened.
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event = "control.account_purge",
            method = "Accounts.Purge",
            principal = %caller.principal_id,
            account = %account,
            job = %job.job_id,
            remove_principal = request.remove_principal,
            "an account hard purge was accepted and the account is now locked"
        );
        Ok(Response::new(PurgeAccountResponse {
            job: Some(job_out(job)),
            gate: Some(gate_out(gate)),
        }))
    }

    /// **`Accounts.PurgeStatus`: where it got to, and whether the lockout is
    /// off.**
    ///
    /// Both, in one message, because neither answers the operator's actual
    /// question on its own: a DONE job beside a gate that is still in force
    /// means the account is empty and still shut, which is either an
    /// administrative suspension the purge correctly refused to lift or a
    /// lease that has not been released — and those need different actions.
    async fn purge_status(
        &self,
        request: Request<PurgeAccountStatusRequest>,
    ) -> Result<Response<PurgeAccountStatusResponse>, Status> {
        self.ctx.authorize(
            &request,
            Access::Server,
            GrantLevel::Admin,
            "Accounts.PurgeStatus",
        )?;
        let account = account_of(&request.into_inner().account, "account")?;
        let (job, gate) = self.ctx.control.purge_account_status(&account)?;
        Ok(Response::new(PurgeAccountStatusResponse {
            job: job.map(job_out),
            gate: gate.map(gate_out),
        }))
    }

    /// **`Accounts.EnrollEmployee`: a NEW user, created by and bound to the
    /// account** — what makes an account an enterprise account (owner,
    /// 2026-10-03). The account admin's: `Admin` over the account's namespace,
    /// the rule `Repos.SetVisibility` keeps. An existing principal is refused by
    /// the plane, by id and by name; so is an id that names a namespace.
    ///
    /// **"Add users" is this call, with room** (owner, 2026-10-04): the rules a
    /// person types against are kept HERE, once, for every door (console,
    /// CLI, monetize): [`users_asked`]. The room comes out of what the account
    /// holds and has neither stored nor promised ([`Self::room_of`]), and
    /// the plane checks it in the same write as the rows it adds.
    async fn enroll_employee(
        &self,
        request: Request<EnrollEmployeeRequest>,
    ) -> Result<Response<EnrollEmployeeResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let request = request.into_inner();
        let account = account_of(&request.account, "account")?;
        if let Err(status) = self.ctx.require(
            &caller,
            Access::namespace(&account),
            GrantLevel::Admin,
            "Accounts.EnrollEmployee",
        ) {
            // **The SELLER's paid grant** (a caller at server scope naming the
            // order the customer paid for): a server grant does not reach
            // inside an account (2026-10-05), but delivering what the
            // account's admin bought is not looking inside it — the new user
            // is the one the order names, and it is in the account's log.
            let paid = status.code() == tonic::Code::PermissionDenied
                && !request.order.trim().is_empty()
                && self
                    .ctx
                    .check(&caller, Access::Server, GrantLevel::Admin)?
                    .is_none();
            if !paid {
                return Err(crate::service::repos::account_admin_only_for(
                    status,
                    &account,
                    "the employees of account",
                ));
            }
        }
        let mut ask = users_asked(&request)?;
        // A console's name is never an employee's (Stage L, 2026-10-04).
        // Refused BEFORE anything is made: the console used to refuse it only
        // at the invite, after this call had left the principal behind.
        for text in [ask.id.as_str(), ask.name.as_str()] {
            if !text.is_empty() && crate::service::access::is_reserved_principal_name(text) {
                return Err(crate::service::access::reserved_name_refusal(
                    text,
                    "an employee",
                ));
            }
        }
        // The name filter (2026-10-05): an employee is named here, inside the
        // account — a held name waits for this account's admin.
        if !request.dry_run {
            for text in [ask.id.as_str(), ask.name.as_str()] {
                if !text.is_empty() {
                    self.ctx
                        .filter_name(&caller, "member", text, Some(account.clone()), false)?;
                }
            }
        }
        // **A grant the SELLER makes under a paid order is not refused for
        // room.** monetize asked this same question (a dry run) before the
        // money moved and sold exactly the shortfall; if the account stored a
        // little more while the customer paid, the users they paid for are
        // still theirs. Only a caller at server scope (monetize's principal,
        // an operator) is the seller — an account admin naming an `order` is
        // asked like any other.
        let seller = !ask.order.is_empty()
            && self
                .ctx
                .require(
                    &caller,
                    Access::Server,
                    GrantLevel::Admin,
                    "Accounts.EnrollEmployee",
                )
                .is_ok();
        if !ask.from_seat && !seller {
            if let Some((cap, stored)) = self.room_of(&account)? {
                ask.cap = Some(cap);
                ask.stored = stored;
            }
        }
        let answer = self.ctx.control.enroll_employee(&account, &ask)?;
        if answer.shortfall_bytes > 0 && !request.dry_run {
            return Err(Status::failed_precondition(room_short(
                &account, &ask, &answer,
            )));
        }
        let key = request.public_key.trim();
        if let (Some(principal), false, false) =
            (&answer.principal, key.is_empty(), answer.unchanged)
        {
            // A NEW principal holds no key, so no attestation is asked for. A
            // key that does not register undoes the enrolment: an employee the
            // admin cannot hand a key to is not the one that was asked for.
            if let Err(err) = self.ctx.control.add_key(
                &principal.id,
                key,
                "enrolled",
                None,
                &caller.principal_id,
                None,
            ) {
                let _ = self.ctx.control.remove_principal(&principal.id);
                return Err(err.into());
            }
        }
        if !answer.unchanged {
            // **A paid "Add users" is the account admin's act** (2026-10-07):
            // the admin ordered it, monetize took the money, and the SELLER
            // (at server scope) delivers it here. The account's log names the
            // account's admin and says who delivered it under which order —
            // never an operator reaching inside the account, which is what
            // the seller's own name read as ("alice (operator)").
            let delivered = seller.then(|| caller.principal_id.clone());
            let mut event = crate::enterprise::AccountAuditEvent::by(
                &caller,
                if ask.seats > 0 {
                    "member.seats_added"
                } else {
                    "member.added"
                },
                answer
                    .principal
                    .as_ref()
                    .map(|p| p.id.clone())
                    .unwrap_or_default(),
            )
            .detail(if ask.seats > 0 {
                format!(
                    "{} seats of {} bytes{}",
                    ask.seats,
                    answer.room_bytes,
                    if ask.order.is_empty() {
                        String::new()
                    } else {
                        format!(", order {}", ask.order)
                    }
                )
            } else {
                format!(
                    "a NEW user, {} bytes of room{}{}",
                    answer.room_bytes,
                    if ask.from_seat { ", into a seat" } else { "" },
                    if delivered.is_some() && !ask.order.is_empty() {
                        format!(", order {}", ask.order)
                    } else {
                        String::new()
                    }
                )
            });
            if let Some(by) = delivered.filter(|by| *by != account) {
                event.actor = account.clone();
                event.actor_machine = false;
                // No money word in an open build's sentences (open_build_speaks_no_money).
                event.detail = format!("{}, delivered by {by}", event.detail);
            }
            self.ctx.account_event(Some(account.clone()), event);
            tracing::info!(
                target: crate::auth::AUDIT_TARGET,
                event = if ask.seats > 0 { "account.seats_added" } else { "account.employee_enrolled" },
                principal = %caller.principal_id,
                account = %account,
                employee = %answer.principal.as_ref().map(|p| p.id.as_str()).unwrap_or(""),
                seats = ask.seats,
                from_seat = ask.from_seat,
                room_bytes = answer.room_bytes,
                order = %ask.order,
                "an account admin added users"
            );
        }
        // **The new named user's invite, in the same call** (the seat claim's
        // and the paid grant's): the identity core's link — seven days, no
        // server-wide role, made by the caller. Best effort: a refused
        // invite is said and never undoes the enrolment (the admin's invite
        // path can make another).
        let (invite_id, invite_secret, invite_expires_at_unix_ms) =
            match (&answer.principal, request.invite, answer.unchanged) {
                (Some(principal), true, false) => {
                    match self.invite_for(&caller.principal_id, &principal.id, &account) {
                        Ok(made) => made,
                        Err(err) => {
                            tracing::warn!(
                                target: crate::auth::AUDIT_TARGET,
                                event = "account.employee_not_invited",
                                principal = %caller.principal_id,
                                employee = %principal.id,
                                reason = %err.message(),
                                "the employee was enrolled and its invite was refused"
                            );
                            (String::new(), String::new(), 0)
                        }
                    }
                }
                _ => (String::new(), String::new(), 0),
            };
        Ok(Response::new(EnrollEmployeeResponse {
            principal: answer.principal,
            room_bytes: answer.room_bytes,
            seats: answer.seats,
            seats_open: answer.seats_open,
            need_bytes: answer.need_bytes,
            unused_bytes: answer.unused_bytes,
            shortfall_bytes: answer.shortfall_bytes,
            unchanged: answer.unchanged,
            invite_id,
            invite_secret,
            invite_expires_at_unix_ms,
        }))
    }

    /// **`Accounts.ListEmployees`**: the account admin's, like enrolling.
    async fn list_employees(
        &self,
        request: Request<ListEmployeesRequest>,
    ) -> Result<Response<ListEmployeesResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let account = account_of(&request.into_inner().account, "account")?;
        self.ctx
            .require(
                &caller,
                Access::namespace(&account),
                GrantLevel::Admin,
                "Accounts.ListEmployees",
            )
            .map_err(|status| {
                crate::service::repos::account_admin_only_for(
                    status,
                    &account,
                    "the employees of account",
                )
            })?;
        let (seats, seats_open) = self.ctx.control.seats(&account)?;
        Ok(Response::new(ListEmployeesResponse {
            employees: self.ctx.control.employees(&account)?,
            seats,
            seats_open,
        }))
    }

    /// **`Accounts.ClaimOperator`: the estate's operator, the account the first
    /// admin passkey is** (2026-10-04). Admin at SERVER scope, like every other
    /// verb here that makes or binds an account: the console asks it with its
    /// own key when the break-glass token registers the first passkey, and the
    /// primary asks it of its own plane when the installer feeds the name. The
    /// NAME is gunnar's to decide — the fed one wins over the request's — so
    /// what comes back is the principal to bind to, whatever was asked.
    async fn claim_operator(
        &self,
        request: Request<ClaimOperatorRequest>,
    ) -> Result<Response<ClaimOperatorResponse>, Status> {
        let caller = self.ctx.authorize(
            &request,
            Access::Server,
            GrantLevel::Admin,
            "Accounts.ClaimOperator",
        )?;
        let request = request.into_inner();
        // Checked before anything is claimed: a token sent in place of its hash
        // is refused by name and never reaches a table.
        let bootstrap = match request.bootstrap_invite_sha256.trim() {
            "" => None,
            raw => Some(crate::seam::bootstrap_hash(raw)?),
        };
        let claim = self.ctx.control.claim_operator(&request.name)?;
        let invite = match &bootstrap {
            Some(hash) => Some(self.ctx.control.operator_bootstrap_invite(
                &claim.principal.id,
                hash,
                crate::bus::now_unix_ms(),
                crate::seam::OPERATOR_INVITE_TTL_MS,
            )?),
            None => None,
        };
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event = "account.operator_claimed",
            principal = %caller.principal_id,
            operator = %claim.principal.id,
            created = claim.created,
            adopted_namespace = claim.adopted_namespace,
            named_by = claim.named_by,
            bootstrap_invite = ?invite,
            "the estate's operator was claimed: the first admin's passkey binds to this account"
        );
        Ok(Response::new(ClaimOperatorResponse {
            principal: Some(claim.principal),
            created: claim.created,
            adopted_namespace: claim.adopted_namespace,
            named_by: claim.named_by.to_owned(),
            bootstrap_invite: matches!(
                invite,
                Some(
                    crate::seam::BootstrapInvite::Recorded | crate::seam::BootstrapInvite::Renewed
                )
            ),
        }))
    }
}