Skip to main content

gunnar_grpc/
seam.rs

1//! **The storage seam: temporary, and marked so.**
2//!
3//! # Why this trait exists at all
4//!
5//! `gunnar-store` (the real `ObjectStore` / `RefStore` of the storage seam) and `gunnar-core`
6//! (the repo model, identity table and ACL) are being written in
7//! parallel with this crate. Blocking on them would have meant writing no
8//! services at all, so the service implementations here are written against
9//! [`ControlPlane`], a small local trait describing exactly what the seven
10//! gRPC services need and nothing more.
11//!
12//! # SEAM: what happens when `gunnar-core` / `gunnar-store` land
13//!
14//! This trait is **not** the intended long-term shape and must not calcify:
15//!
16//! - The repo/namespace/principal/key/grant model belongs to `gunnar-core`.
17//!   When it exists, [`ControlPlane`] becomes a thin *adapter* over it (one
18//!   impl in this crate) and stops being an independent definition of the
19//!   model. It must not become a second source of truth for what a `Repo` is
20//!   (one implementation, never a twin).
21//! - The ref/object methods ([`ControlPlane::list_refs`],
22//!   [`ControlPlane::log`], [`ControlPlane::read_object`],
23//!   [`ControlPlane::diff`]) are exactly the `RefStore`/`ObjectStore` surface
24//!   of the storage seam seen from the control plane. They will delegate, not reimplement.
25//! - [`MemoryControlPlane`] is a **test double**, not a backend. It is not the
26//!   `mem` backend of the storage seam and must not be mistaken for one.
27//!
28//! # Why it is synchronous
29//!
30//! `ObjectStore` and `RefStore` in the storage seam are synchronous traits, and this one is
31//! shaped to match so the adapter is a straight delegation. That also keeps it
32//! object-safe without pulling in `async-trait`. A backend whose work is long
33//! enough to matter belongs behind `spawn_blocking` at the call site, which is
34//! a decision the real store can make once it knows its own costs.
35
36use std::collections::BTreeMap;
37use std::sync::Mutex;
38
39use crate::auth::Access;
40
41use gunnar_proto::{
42    AddWebhookRequest, BranchProtection, Commit, FileChange, Grant, GrantLevel, HashKind,
43    MirrorConfig, MirrorCredentialKind, MirrorState,
44    Namespace, Principal, PrincipalKind, PublicKey, PullRequest, ReadLfsObjectResponse,
45    ReadObjectResponse, RefNamePolicy,
46    RefTarget, ReplicationStatusResponse, ReplicationTarget, Repo, RepoPath,
47    IssueState, RepoStorage, ResyncReplicationResponse, ReviewState, SignedForgeEvent,
48    SignedKeyAttestation, SignedMembershipEvent, StorageInfoResponse, WebhookListResponse,
49    Visibility,
50};
51
52/// **The two arguments a credential write must agree about**, checked once for
53/// every implementation of the seam.
54///
55/// A `kind` with no `secret`, or a `secret` with no `kind`, are the same
56/// request read two ways — and one of those readings SILENTLY FORGETS a stored
57/// credential. That is the failure this refuses: an operator who meant to set a
58/// token and left the kind unset would otherwise clear the key they already
59/// had, and find out at the next sync.
60///
61/// Free function rather than a default body, because `CoreControlPlane` and the
62/// memory double must apply the same rule and a default body is the one thing
63/// an implementation can silently not call.
64pub fn credential_arguments(
65    kind: MirrorCredentialKind,
66    secret: &str,
67) -> ControlResult<()> {
68    match (kind, secret.is_empty()) {
69        (MirrorCredentialKind::Unspecified, true) => Ok(()),
70        (MirrorCredentialKind::Unspecified, false) => Err(ControlError::InvalidArgument(
71            "a credential value was given with no kind. `kind = UNSPECIFIED` is how a stored \
72             credential is FORGOTTEN, and it must carry no value — otherwise \"set this token\" \
73             and \"forget my key\" are the same request".to_owned(),
74        )),
75        (_, true) => Err(ControlError::InvalidArgument(format!(
76            "a credential of kind {kind:?} was asked for with an empty value. To forget the \
77             stored credential, send `kind = UNSPECIFIED`"
78        ))),
79        (_, false) => Ok(()),
80    }
81}
82
83/// Everything that can go wrong behind the seam.
84///
85/// Deliberately small: it maps one-to-one onto the gRPC status codes the
86/// services return, in [`crate::status`]: one converter, not a `match` copied
87/// into seven service impls (one implementation).
88#[derive(Debug, thiserror::Error)]
89pub enum ControlError {
90    #[error("{0} not found")]
91    NotFound(String),
92    #[error("{0} already exists")]
93    AlreadyExists(String),
94    #[error("invalid argument: {0}")]
95    InvalidArgument(String),
96    #[error("failed precondition: {0}")]
97    FailedPrecondition(String),
98    /// The request is well formed and would be answered, but answering it would
99    /// cost the server more than it is willing to spend.
100    ///
101    /// Distinct from [`ControlError::InvalidArgument`] on purpose: the caller
102    /// did nothing wrong, and a retry with a narrower request is the right
103    /// response, which is what `RESOURCE_EXHAUSTED` tells a generated client and
104    /// `INVALID_ARGUMENT` does not.
105    #[error("resource exhausted: {0}")]
106    ResourceExhausted(String),
107    /// The seam is not wired to a real store yet. This is the honest answer for
108    /// every ref/object operation until `gunnar-store` lands, and it is the
109    /// value that must NOT quietly turn into an empty successful response. An
110    /// empty ref list is indistinguishable from a working server with no refs.
111    #[error("not implemented: {0}; the storage seam is not wired to gunnar-store yet")]
112    Unimplemented(&'static str),
113    #[error("internal error: {0}")]
114    Internal(String),
115    /// **The state could not be read.** A storage fault, not an answer.
116    ///
117    /// Distinct from [`ControlError::Internal`] because a caller must be able
118    /// to tell "I could not find out" from "I found out and here it is". It
119    /// exists for `Bootstrap.Status` above all: an unreadable identity table
120    /// reported as `window_open: false` would be the most dangerous lie this
121    /// server can tell, and one reported as `window_open: true` would open the
122    /// door on a disk fault.
123    #[error("unavailable: {0}")]
124    Unavailable(String),
125    /// No verified caller. Distinct from [`ControlError::PermissionDenied`] on
126    /// purpose: "I do not know who you are" and "I know who you are and the
127    /// answer is no" are different facts, and collapsing them makes an
128    /// operator's first question unanswerable.
129    #[error("unauthenticated: {0}")]
130    Unauthenticated(String),
131    #[error("permission denied: {0}")]
132    PermissionDenied(String),
133}
134
135pub type ControlResult<T> = Result<T, ControlError>;
136
137/// **A `Mirror.LogFetch` range, ready to stream** ([`ControlPlane::log_fetch_stream`]): run on a
138/// blocking thread, it hands the range to the sink as `LogChunk` pieces and returns once the
139/// range is out (twin-streaming, 2026-10-06).
140pub type LogFetchProducer =
141    Box<dyn FnOnce(&mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<()> + Send>;
142
143/// The most blob bytes one `Mirror.LogFetch` piece carries: under every client's default 4 MiB
144/// message ceiling with room for the meta.
145pub const LOG_PIECE_BYTES: usize = 1 << 20;
146
147/// One whole entry as `LogChunk` pieces into `sink`: the blob in [`LOG_PIECE_BYTES`] slabs,
148/// `meta` on the first piece, `last` on the last; an empty blob is one piece.
149pub fn log_entry_pieces(
150    e: gunnar_proto::LogEntry,
151    sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>,
152) -> ControlResult<()> {
153    log_entry_pieces_from(e, 0, sink)
154}
155
156/// [`log_entry_pieces`] from byte `offset` of the blob on (restore-large-entry, 2026-10-07):
157/// the pieces below it are not sent, every piece says its `offset`, and an offset at the blob's
158/// end is one empty last piece. An offset PAST the end is not this entry as the asker knew it
159/// (an entry erased since): it is sent whole from 0, and the asker sees a length that differs.
160/// No `entry_crc`: a plane holding entries in memory has no stored crc to vouch with.
161pub fn log_entry_pieces_from(
162    e: gunnar_proto::LogEntry,
163    offset: u64,
164    sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>,
165) -> ControlResult<()> {
166    let blob_len = e.blob.len() as u64;
167    let start = if offset <= blob_len { offset as usize } else { 0 };
168    let rest = &e.blob[start..];
169    let n = rest.len().div_ceil(LOG_PIECE_BYTES).max(1);
170    let mut meta = Some(e.meta);
171    for i in 0..n {
172        let piece = &rest[(i * LOG_PIECE_BYTES).min(rest.len())..((i + 1) * LOG_PIECE_BYTES).min(rest.len())];
173        sink(gunnar_proto::LogChunk {
174            seq: e.seq,
175            kind: e.kind,
176            meta: meta.take().unwrap_or_default(),
177            blob_len,
178            blob: piece.to_vec(),
179            last: i + 1 == n,
180            restore_head: 0,
181            restore_bytes: 0,
182            log_id: String::new(),
183            offset: Some((start + (i * LOG_PIECE_BYTES).min(rest.len())) as u64),
184            entry_crc: None,
185            snapshot_item: None,
186            snapshot_id: String::new(),
187        })?;
188    }
189    Ok(())
190}
191
192/// **One `Bootstrap.Restore` stream, taken a chunk at a time** (restore-large-entry,
193/// 2026-10-07) — what the handler feeds every chunk of the stream into, so where an entry's
194/// bytes live while it arrives is the plane's business and not the wire's.
195///
196/// The server's intake stages an entry larger than one piece on the box's disk as it arrives
197/// and keeps what it holds when the stream ends inside it, so the next stream resumes the entry
198/// at the byte it reached (`Bootstrap.Status.restore_partial_*`, `LogFetchRequest.from_offset`);
199/// the default one ([`ControlPlane::restore_intake`]) gathers each entry in memory, as every
200/// restore did before.
201pub trait RestoreIntake: Send {
202    /// One chunk: a piece of an entry (`seq` > 0) or of a snapshot (`seq` 0). An entry is
203    /// applied when its last piece arrives whole.
204    fn take(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()>;
205    /// The stream ended cleanly. `Err` when it ended inside an entry — naming what this box
206    /// holds of it and where the next stream resumes.
207    fn end_of_stream(&mut self) -> ControlResult<()>;
208    /// `(applied, lost)` by this stream: entries applied, and replayed CAS claims that lost.
209    fn counts(&self) -> (u64, u64);
210}
211
212/// The intake every plane had before: each entry gathered whole in memory, then
213/// [`ControlPlane::restore_apply`]. An entry the stream ends inside is lost with the stream.
214struct GatherIntake<'a, P: ?Sized> {
215    plane: &'a P,
216    next: u64,
217    open: Option<gunnar_proto::LogEntry>,
218    want_len: u64,
219    applied: u64,
220    lost: u64,
221}
222
223impl<P: ControlPlane + ?Sized> RestoreIntake for GatherIntake<'_, P> {
224    fn take(&mut self, chunk: &gunnar_proto::LogChunk) -> ControlResult<()> {
225        // A snapshot piece (seq 0): staged by the plane, its terminator installs it.
226        if chunk.seq == 0 {
227            if let Some(e) = &self.open {
228                return Err(ControlError::InvalidArgument(format!("a snapshot piece arrived with seq {} unfinished", e.seq)));
229            }
230            if let Some(at) = self.plane.restore_snapshot_chunk(chunk)? {
231                self.next = at + 1;
232            }
233            return Ok(());
234        }
235        let entry = match self.open.as_mut() {
236            Some(e) if e.seq == chunk.seq => e,
237            Some(e) => return Err(ControlError::InvalidArgument(format!("seq {} arrived with seq {} unfinished", chunk.seq, e.seq))),
238            None => {
239                self.want_len = chunk.blob_len;
240                self.open.insert(gunnar_proto::LogEntry { seq: chunk.seq, kind: chunk.kind, meta: chunk.meta.clone(), blob: Vec::with_capacity(gunnar_proto::reserve_for(chunk.blob_len)) })
241            }
242        };
243        entry.blob.extend_from_slice(&chunk.blob);
244        if !chunk.last {
245            return Ok(());
246        }
247        let done = self.open.take().expect("open entry");
248        if done.blob.len() as u64 != self.want_len {
249            return Err(ControlError::InvalidArgument(format!("seq {}: {} of {} blob bytes arrived", done.seq, done.blob.len(), self.want_len)));
250        }
251        if done.seq < self.next {
252            return Ok(());
253        }
254        if done.seq > self.next {
255            return Err(ControlError::FailedPrecondition(format!("seq {} arrived where {} was due; resend from {}", done.seq, self.next, self.next)));
256        }
257        if self.plane.restore_apply(done)? {
258            self.lost += 1;
259        }
260        self.applied += 1;
261        self.next += 1;
262        Ok(())
263    }
264
265    fn end_of_stream(&mut self) -> ControlResult<()> {
266        if self.open.is_some() {
267            return Err(ControlError::InvalidArgument("the stream ended inside an entry; resend from the box's head + 1".into()));
268        }
269        Ok(())
270    }
271
272    fn counts(&self) -> (u64, u64) {
273        (self.applied, self.lost)
274    }
275}
276
277// ───────────────────────────────────────────────────────────────────────────
278// The admission gate, reached through a seam (the gRPC half of the shared gate)
279// ───────────────────────────────────────────────────────────────────────────
280//
281// The git front doors — SSH and smart HTTP — put every transfer through ONE
282// gate object, `gunnar_ssh::Admission`: max-min-fair per-principal share, a
283// server-wide concurrency ceiling, and a memory ledger. `Repos.ArchiveOut`
284// streams a whole repository's stored bytes down the control-plane channel; it
285// is a transfer in everything but name and must take the SAME permit a
286// `git-upload-pack` does, or the third front door is the one with no gate.
287//
288// `gunnar_ssh::Admission` cannot be named here: `gunnar-grpc` is on the
289// client/control graph (`gunnar-cli --features control` pulls this crate WITHOUT
290// `gunnar-server`), and a `gunnar-ssh` dependency here would drag the whole SSH
291// server — russh and all — into every client. So the gate is reached through a
292// seam, exactly as the storage backend is reached through `ControlPlane`:
293// `gunnar-server` implements [`TransferGate`] over the real `Arc<Admission>` the
294// SSH and HTTP fronts already hold. This is a seam TO the one gate, not a second
295// gate — no admission logic lives here.
296
297/// A held admission permit for a control-plane transfer.
298///
299/// It is opaque on purpose: the real permit is a `gunnar_ssh::AdmissionGuard`,
300/// which this crate may not name. Dropping it releases the server-wide slot, the
301/// per-principal count and the memory reservation — so the caller holds it for
302/// the whole stream and lets it fall at the end, the same lifetime the SSH and
303/// HTTP paths give their guard.
304pub struct TransferPermit(pub Box<dyn TransferHold>);
305
306/// What a held [`TransferPermit`] can still be told.
307///
308/// One thing: that the transfer's real work has begun, which is the sample the
309/// governor's CoDel controller reads. The SSH and HTTP paths call the guard's
310/// own `entered_service` at the first line of the spawned transfer; this is that
311/// call, across the seam.
312pub trait TransferHold: Send {
313    /// File the sojourn sample: everything before this was the transfer waiting.
314    fn entered_service(&self);
315}
316
317/// Why the gate refused a control-plane transfer, carrying the same fields the
318/// SSH and HTTP refusals log.
319///
320/// The counters are read from the gate at the moment of refusal so the audit
321/// line names the state that refused — `in_flight`, `peak_in_flight`, the
322/// governed `limit`, `reserved_bytes` and the running `refused_total` — rather
323/// than a bare "busy". `message` is the gate's own `client_message`, so the
324/// sentence the operator reads is the one the log recorded.
325#[derive(Clone, Debug)]
326pub struct TransferRefused {
327    /// The gate's own client-facing sentence.
328    pub message: String,
329    /// Transfers in flight across all fronts when this was refused.
330    pub in_flight: u64,
331    /// The high-water mark of `in_flight`.
332    pub peak_in_flight: u64,
333    /// The governed concurrency limit in force at the refusal.
334    pub limit: usize,
335    /// Bytes the memory ledger had reserved to in-flight transfers.
336    pub reserved_bytes: u64,
337    /// How many transfers this gate has refused since start-up, this one
338    /// included.
339    pub refused_total: u64,
340}
341
342/// The seam to the ONE admission gate (`gunnar_ssh::Admission`).
343///
344/// `gunnar-server` implements it over the real `Arc<Admission>` the SSH and HTTP
345/// fronts hold; a plaintext test router that wires no gate leaves it `None` and
346/// the transfer-shaped RPCs run ungated, which is exactly the ungated state
347/// the red half of the test drives.
348pub trait TransferGate: Send + Sync {
349    /// Admit `principal` for one control-plane transfer of `namespace`'s data,
350    /// or refuse it.
351    ///
352    /// **Never blocks** — it is `gunnar_ssh::Admission::try_admit` across the
353    /// seam. On success the returned [`TransferPermit`] is held for the whole
354    /// stream; on refusal the [`TransferRefused`] carries the audit fields and
355    /// the client sentence.
356    ///
357    /// `namespace` is carried for the flow ledger on the other side, which
358    /// counts a refusal against the tenant that met it. It was not here before
359    /// the ledger was, and the call site always had it: the refusal was logged
360    /// with `repo = %path` in the same breath as it was counted against nobody.
361    fn admit_transfer(
362        &self,
363        principal: &str,
364        namespace: &str,
365    ) -> Result<TransferPermit, TransferRefused>;
366}
367
368// ───────────────────────────────────────────────────────────────────────────
369// Identity: the ACL model, asked through the seam, decided in gunnar-core
370// ───────────────────────────────────────────────────────────────────────────
371
372/// A key as the gate needs it: whose it is, and the OpenSSH line to verify a
373/// signature against.
374///
375/// The line is carried rather than a parsed key because `gunnar-ssh` and
376/// `gunnar-core` link semver-incompatible `ssh-key` versions and no key
377/// *type* can cross that line. Text can.
378/// **A web credential (a WebAuthn passkey) as the plane stores it** (Stage L,
379/// 2026-10-04): the wire row, and the public key the wire never carries —
380/// P-256, SEC1 uncompressed (`0x04 || X || Y`), which is what gunnar verifies a
381/// login's assertion with.
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct StoredCredential {
384    pub credential: gunnar_proto::WebCredential,
385    pub public_key: Vec<u8>,
386}
387
388/// **An invite to be stored** (Stage L): a pending passkey enrolment on an
389/// existing principal. The plane is handed the secret's HASH only — the secret
390/// itself is shown once to whoever created the invite and is never stored.
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub struct NewInvite {
393    pub id: String,
394    pub principal_id: String,
395    pub created_by: String,
396    pub created_at_unix_ms: i64,
397    pub expires_at_unix_ms: i64,
398    pub note: String,
399    /// The server-wide grant a redemption applies; `Unspecified` for none.
400    pub server_level: GrantLevel,
401    /// Lowercase hex SHA-256 of the secret.
402    pub secret_hash: String,
403}
404
405/// **When a principal last signed in, and last changed something**
406/// (2026-10-21) — what `Access.ListPrincipals` answers beside each principal.
407/// `None`: never, as far as this box knows.
408#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
409pub struct PrincipalSeen {
410    /// Unix ms of the last console session issued to it.
411    pub last_sign_in_unix_ms: Option<i64>,
412    /// Unix ms of its last authenticated call that changed something.
413    pub last_operation_unix_ms: Option<i64>,
414}
415
416/// **What an invite secret is now** (2026-10-19, `Access.CheckInvite`):
417/// read by the plane, nothing spent.
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum InviteCheck {
420    /// Redeemable now: the invite's row.
421    Valid(gunnar_proto::Invite),
422    /// A passkey was registered with it.
423    Used,
424    /// Past its expiry, never redeemed.
425    Expired,
426    /// No invite has this secret.
427    Unknown,
428}
429
430/// **What a verified web login commits** (Stage L), in one update: the
431/// passkey's new counter and last use, and the console's session key on the
432/// passkey's principal, expiring at `expires_at_unix_ms`. No attestation is
433/// owed for the session key — the verified passkey assertion IS the person's
434/// consent.
435#[derive(Debug, Clone, PartialEq, Eq)]
436pub struct WebLoginCommit {
437    pub credential_id: Vec<u8>,
438    /// The counter the verified assertion presented.
439    pub sign_count: u32,
440    pub now_unix_ms: i64,
441    /// One OpenSSH public key line.
442    pub session_openssh: String,
443    pub comment: String,
444    pub expires_at_unix_ms: i64,
445}
446
447/// What a committed web login answers.
448#[derive(Debug, Clone, PartialEq, Eq)]
449pub struct WebLoginOutcome {
450    pub principal: Principal,
451    pub credential: gunnar_proto::WebCredential,
452    /// The session key's fingerprint.
453    pub fingerprint: String,
454}
455
456/// **A passkey registered from a creation ceremony** (Stage L), in one
457/// update: the invite (if any) redeemed and removed, the credential stored,
458/// the invite's server grant applied.
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct WebRegistration {
461    /// Lowercase hex SHA-256 of the invite's secret; `None` when the relay
462    /// names the principal itself.
463    pub invite_secret_hash: Option<String>,
464    /// Required without an invite; with one, empty or the invite's own.
465    pub principal_id: Option<String>,
466    pub credential_id: Vec<u8>,
467    pub public_key: Vec<u8>,
468    pub label: String,
469    pub sign_count: u32,
470    pub now_unix_ms: i64,
471}
472
473/// **An approved machine enrolment** (`Access.ApproveMachineEnrolment`,
474/// 2026-10-05), committed in ONE update: the MACHINE principal made
475/// when it is not there (a principal of that id that is not a machine is
476/// `FailedPrecondition`); its server-wide grant set; the credential stored
477/// (an id already registered is `AlreadyExists`); and the approver's step-up
478/// passkey's counter and last use, when a person approved.
479#[derive(Debug, Clone, PartialEq, Eq)]
480pub struct MachineApproval {
481    /// The machine principal: id = name (`kit-*`).
482    pub machine: String,
483    /// `Admin` or `Write`.
484    pub server_level: GrantLevel,
485    pub credential_id: Vec<u8>,
486    pub public_key: Vec<u8>,
487    pub label: String,
488    pub sign_count: u32,
489    pub now_unix_ms: i64,
490    /// The approver's step-up passkey and the counter it presented.
491    pub step_up: Option<(Vec<u8>, u32)>,
492}
493
494/// What an approval committed.
495#[derive(Debug, Clone, PartialEq, Eq)]
496pub struct MachineApproved {
497    pub principal: Principal,
498    pub credential: gunnar_proto::WebCredential,
499    /// The machine principal was made by this approval.
500    pub created: bool,
501}
502
503/// What a registration answers.
504#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct WebRegistered {
506    pub credential: gunnar_proto::WebCredential,
507    pub principal: Principal,
508    /// The server-wide grant the redeemed invite applied; `Unspecified` for
509    /// none.
510    pub server_level: GrantLevel,
511}
512
513#[derive(Debug, Clone, PartialEq, Eq)]
514pub struct RegisteredKey {
515    /// The ACL principal the key belongs to.
516    pub principal_id: String,
517    /// `<algo> <base64> [comment]`, exactly as `authorized_keys` writes it.
518    pub openssh: String,
519    /// The principal is a `Machine` (the ACL model): a server's deploy key rather than a
520    /// person's. The one caller a browse replica's control plane still lets
521    /// write (`Repos.Delete`, the browse replica).
522    pub machine: bool,
523}
524
525/// Who a **client certificate** belongs to, once the handshake has already
526/// proven the peer holds its private key.
527///
528/// The difference from [`RegisteredKey`] is the whole reason this is a separate
529/// type: a `RegisteredKey` carries an `openssh` line **because the gate still
530/// has to verify a signature against it**. A certificate has no such step left.
531/// TLS proved possession during the handshake and the issuer proved provenance,
532/// so what remains is one table lookup: are these bytes registered, and to
533/// whom. Handing back an `openssh` field here would invite a second
534/// verification against material that is not the credential, which is how a
535/// check comes to pass against the wrong thing.
536///
537/// The certificate is **not** an identity model of its own (the mTLS and
538/// certificate-identity plans): the fingerprint lands in the same ACL key table
539/// as every SSH key, under `algo: "x509"`, and the same `AuthTable::may` decides
540/// what the principal can do. A certificate the CA signed and nobody registered
541/// reaches the gate and is refused there.
542#[derive(Debug, Clone, PartialEq, Eq)]
543pub struct CertificateIdentity {
544    /// The ACL principal the certificate was registered to.
545    pub principal_id: String,
546    /// The `SHA256:…` fingerprint of the DER, as
547    /// `gunnar_core::auth::fingerprint_of_certificate` computes it. Carried so
548    /// the audit record can name the credential without re-hashing it in a
549    /// crate that must not own that decision.
550    pub fingerprint: String,
551}
552
553/// **The two ACL questions the gRPC gate asks, and neither of them is answered
554/// in this crate.**
555///
556/// Split out of [`ControlPlane`] rather than folded into it so that the
557/// distinction is visible at the type level: a control plane that has an
558/// identity table implements this, and one that does not cannot be mistaken
559/// for one that does.
560///
561/// # Why there is no default implementation that allows
562///
563/// [`Identity::may`] could be *derived* from [`ControlPlane::list_grants`]:
564/// filter by principal, take the maximum level. That derivation would be a
565/// **second grant decision**, living beside `gunnar_core::AuthTable::may`, free
566/// to disagree with it. The one-sandbox rule records what two security checks that disagree
567/// cost the last time: the safe one gives false confidence about the permissive
568/// one. So this is a delegation, and the only implementation with an opinion is
569/// `AuthTable`'s.
570pub trait Identity: Send + Sync {
571    /// The registered key for a `SHA256:…` fingerprint, or `None` if there is
572    /// no such key, the key has expired, or its principal is gone.
573    ///
574    /// `Result<Option<_>>`, not `Option<_>`: the table lives in storage and a
575    /// read can fail, and a bare `Option` has to invent an answer for that. The
576    /// safe-looking invention (`None`) turns a storage fault into a silent
577    /// authentication failure (the ACL model, the same reason `may` returns a `Result`).
578    fn registered_key(&self, fingerprint: &str) -> ControlResult<Option<RegisteredKey>>;
579
580    /// **A key this box was FED to enrol and has not enrolled yet**: its
581    /// OpenSSH line, for a fingerprint [`Identity::registered_key`] does not
582    /// know (cov-E r3 2026-10-06, run 3). A primary's fed principals — the
583    /// front's certify key, the console's, monetize's — are enrolled by its
584    /// pairing loop after the installer's feed lands: a moment later on a
585    /// healthy box, MINUTES later on a starved one (measured: 4 m 47 s, the
586    /// certify key sixth in line). A caller holding one of those keys is not a
587    /// stranger, and the gate answers it `Unavailable` (ask again), never
588    /// `Unauthenticated`, once its signature verifies against the fed key
589    /// ([`crate::auth::AuthFailure::Enrolling`]).
590    ///
591    /// Defaulted to `None` — every unknown fingerprint unknown, the answer
592    /// before — because `None` IS the refusal: an adapter that never says a
593    /// key is awaited answers exactly as it always did, and nothing is let in
594    /// by this answer either way.
595    fn awaiting_enrolment(&self, _fingerprint: &str) -> Option<String> {
596        None
597    }
598
599    /// Who these **client-certificate** DER bytes were registered to, or `None`
600    /// if nobody registered them, the registration expired, or its principal is
601    /// gone.
602    ///
603    /// # Why the DER and not a fingerprint
604    ///
605    /// Hashing the credential is the one step that decides *what the credential
606    /// is*, and `gunnar_core::auth::fingerprint_of_certificate` already owns
607    /// that decision for both the SSH and the X.509 side of the ACL table. This
608    /// crate deliberately does not link `gunnar-core`
609    /// ([`crate::peer_certificate_der`] says why), so a fingerprint computed on
610    /// this side of the seam would be a *second* opinion about what bytes a
611    /// certificate is — and the two would agree right up until one of them
612    /// changed. The seam therefore carries the bytes and the adapter hashes.
613    ///
614    /// # Why this is not a second identity model
615    ///
616    /// It returns an ACL principal id, from the same table
617    /// [`Identity::registered_key`] reads, and every authorisation question is
618    /// still answered by [`Identity::may`]. The certificate is a *credential*,
619    /// like an SSH key is a credential. What the certificate's subject claims
620    /// is never consulted: two certificates can claim the same subject, and the
621    /// bytes are what was registered (the mTLS plan, and the tension with the
622    /// certificate-identity plan's cert-CN shape is resolved this way on purpose).
623    ///
624    /// **Required, not defaulted.** A default returning `Ok(None)` would be a
625    /// silent "no certificate identity here" for any adapter that forgot to
626    /// implement it, and an adapter that forgot is exactly the one whose
627    /// operators would then be told their registered certificate is unknown.
628    /// The two implementations in this workspace both answer.
629    fn registered_certificate(&self, der: &[u8]) -> ControlResult<Option<CertificateIdentity>>;
630
631    /// **May this principal do this, at this level?** `gunnar_core::AuthTable::may`
632    /// (the ACL model) seen through the seam.
633    fn may(&self, principal_id: &str, access: &Access, level: GrantLevel) -> ControlResult<bool>;
634
635    /// **[`ControlPlane::may`] at `level` for every repository of a listing**
636    /// (round 5.3.1): one answer per row, in order. Defaulted to one `may` per
637    /// row; the core plane answers the whole listing under one read of the
638    /// catalog and one of the identity table, so concurrent listings no longer
639    /// queue on each other's per-row store reads (test5 2026-10-08).
640    fn may_each_repo(&self, principal_id: &str, paths: &[RepoPath], level: GrantLevel) -> ControlResult<Vec<bool>> {
641        paths.iter().map(|p| self.may(principal_id, &Access::repo(p), level)).collect()
642    }
643
644    /// **The methods a NARROWED principal may call** (f-principal-reach), or
645    /// `None` for a principal judged by its grants. A narrowed principal holds
646    /// no grant: [`crate::service::ServiceContext::require`] lets it through
647    /// exactly the methods listed and refuses every other by name. Defaulted
648    /// to `None` because it only ever RESTRICTS a principal nobody narrowed —
649    /// an adapter that does not know narrowing judges every caller as before.
650    fn narrowed_methods(&self, _principal_id: &str) -> ControlResult<Option<Vec<String>>> {
651        Ok(None)
652    }
653}
654
655// ───────────────────────────────────────────────────────────────────────────
656// First boot: the one unauthenticated door, seen through the seam
657// ───────────────────────────────────────────────────────────────────────────
658
659/// What a server can say about its own enrolment, without a credential.
660///
661/// Every field is a fact the server holds, never a derivation this crate makes:
662/// [`BootstrapState::window_open`] is `gunnar_core::AuthTable::bootstrap_window_open`,
663/// the one predicate that also gates the write. A second opinion computed here
664/// out of `principals == 0` would be a status that could disagree with the
665/// gate, and the permissive one would win silently (the one-sandbox rule).
666#[derive(Debug, Clone, PartialEq, Eq, Default)]
667pub struct BootstrapState {
668    /// `Bootstrap.Enroll` will be served without a credential.
669    pub window_open: bool,
670    /// The window has been used. Not the negation of `window_open`.
671    pub bootstrapped: bool,
672    pub bootstrapped_at_unix_ms: i64,
673    pub bootstrapped_principal: String,
674    pub bootstrapped_fingerprint: String,
675    pub principals: u64,
676    pub keys: u64,
677    /// **The git listener is serving**, i.e. the boot window is closed.
678    ///
679    /// `false` while the server's start-up disk test is still saturating the
680    /// disk a repository's first post-crash open has to read; git transfers are
681    /// refused with `EX_TEMPFAIL` and a sentence containing `starting` for that
682    /// whole window. It rides on `Bootstrap.Status` and not on `Admin.Health`
683    /// because this is the one call an unenrolled server answers, and a boot
684    /// script is exactly the caller with no credential.
685    ///
686    /// `Default` is `false`: an implementation that forgets it says "not yet",
687    /// and "wait" is the safe thing to say wrongly.
688    pub serving_git: bool,
689    /// One sentence about [`BootstrapState::serving_git`].
690    pub readiness: String,
691    /// **The build stamp** — `gunnar_common::BUILD_REF`, the same string an
692    /// SSH banner's comments field carries. Empty when the plane does not
693    /// know it (a test double); the server fills it.
694    pub build: String,
695    /// `primary` or `twin`: the `--role` this box was started with, or a box
696    /// that has since become a twin. What a door proves a twin medium by, since
697    /// a twin serves no git and so has no banner.
698    pub role: String,
699    /// **The twin a RECHARGED main awaits** (`Bootstrap.Status.recharge_from`): set while the
700    /// box is a re-admitted main that is fresh or restoring; empty otherwise.
701    pub recharge_from: String,
702    /// A restore is under way (`txlog-restoring`).
703    pub restoring: bool,
704    /// The box's transaction-log head.
705    pub log_head_seq: u64,
706    /// **The entry a restoring box holds part of**, staged on its disk (restore-large-entry,
707    /// 2026-10-07): `(seq, bytes held durably)`; `None` when nothing is staged.
708    pub restore_partial: Option<(u64, u64)>,
709    /// **This box stages a twin's snapshot across streams** (restore-after-snapshot, 2026-10-07).
710    pub restore_snapshot_resumable: bool,
711    /// What a restoring box holds of a twin's snapshot: `(id, items whole, bytes of the next,
712    /// whole)`; `None` when nothing is staged.
713    pub restore_snapshot: Option<(String, u64, u64, bool)>,
714}
715
716/// What [`FirstBoot::enroll_first_identity`] created.
717#[derive(Debug, Clone, PartialEq)]
718pub struct Enrolled {
719    pub principal: Principal,
720    pub key: PublicKey,
721    /// The namespace this principal is intrinsically admin of.
722    pub account: String,
723    pub bootstrapped_at_unix_ms: i64,
724    /// A client certificate minted for the principal by the server's issuer,
725    /// with its private key and the issuer certificate to pin. All three empty
726    /// when the server has no issuer — enrolment does not fail on their
727    /// account, because the window is already shut when minting runs.
728    pub client_cert_pem: String,
729    pub client_key_pem: String,
730    pub issuer_cert_pem: String,
731}
732
733/// **The trust-on-first-use door.**
734///
735/// A supertrait of [`ControlPlane`] for the same reason [`Identity`] is one: a
736/// control plane that cannot answer *has anybody been enrolled here* cannot be
737/// served safely, and a compile error is cheaper than a runtime hole. There is
738/// no default implementation, and in particular no default that answers
739/// `window_open: true`.
740pub trait FirstBoot: Send + Sync {
741    /// The enrolment state, as the server holds it.
742    ///
743    /// `Result`, so a table that cannot be read is an error and never a
744    /// cheerful `window_open: false` **or** `true`. Both inventions are wrong
745    /// and one of them opens the door.
746    fn bootstrap_state(&self) -> ControlResult<BootstrapState>;
747
748    /// Create the first identity and shut the window, atomically.
749    ///
750    /// [`ControlError::FailedPrecondition`] once it has succeeded once,
751    /// including after every identity it created has been deleted.
752    fn enroll_first_identity(
753        &self,
754        name: &str,
755        openssh_public_key: &str,
756    ) -> ControlResult<Enrolled>;
757}
758
759/// Counts for `Admin.Stats`. Event counters come from the bus, not from here.
760#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
761pub struct ControlCounts {
762    pub namespaces: u64,
763    pub repos: u64,
764    pub principals: u64,
765    pub keys: u64,
766    pub grants: u64,
767}
768
769/// One repository's index work in flight, for `Admin.Stats` (`index`) and the
770/// dashboard's `index` row. Information, never a gate.
771#[derive(Debug, Clone, Default, PartialEq, Eq)]
772pub struct IndexProgressRow {
773    pub namespace: String,
774    pub name: String,
775    pub pack_id: u64,
776    pub objects_done: u64,
777    pub objects_total: u64,
778    pub bytes_done: u64,
779    pub bytes_total: u64,
780    pub phase: String,
781    pub started_at_unix_ms: i64,
782    pub resumed: bool,
783    pub reinflations: u64,
784    pub pauses: u64,
785}
786
787impl IndexProgressRow {
788    pub fn percent(&self) -> u32 {
789        (self.objects_done * 100)
790            .checked_div(self.objects_total)
791            .map_or(0, |percent| percent.min(100) as u32)
792    }
793}
794
795/// What a merge moved: the fact the `RefUpdated` event needs, handed back so
796/// the SERVICE publishes it — the seam stays free of the event bus.
797#[derive(Debug, Clone, PartialEq, Eq)]
798pub struct MergedRef {
799    /// Full target ref name.
800    pub ref_name: String,
801    /// Where it was.
802    pub old_oid: String,
803    /// Where it is now: the source tip.
804    pub new_oid: String,
805}
806
807/// What a new repository is created with. Server defaults are applied by the
808/// implementation, not by the caller.
809#[derive(Debug, Clone)]
810pub struct NewRepo {
811    pub path: RepoPath,
812    pub hash_kind: HashKind,
813    /// Full ref name. Empty means the server default.
814    pub default_branch: String,
815    pub visibility: Visibility,
816    pub description: String,
817    /// Take over a store already on disk at this name; see
818    /// `CreateRepoRequest.adopt`.
819    pub adopt: bool,
820}
821
822/// What a `Repos.Delete` was asked to do.
823#[derive(Debug, Clone, PartialEq, Eq, Default)]
824pub struct DeleteOptions {
825    /// Reclaim the bytes NOW instead of tombstoning them.
826    pub purge: bool,
827    /// How long a tombstone keeps them. `None` = the server's default.
828    /// `Some(0)` is "at the next sweep" and is a decision, not an absence.
829    pub retention_days: Option<u32>,
830    /// Who asked. Recorded in the tombstone.
831    pub principal_id: String,
832}
833
834/// What a `Repos.Delete` did. **Not a unit**: "the bytes went" and "the bytes
835/// are being kept until a date" are different outcomes and a caller that could
836/// not tell them apart could not report either one honestly.
837#[derive(Debug, Clone, Copy, PartialEq, Eq)]
838pub enum DeleteOutcome {
839    /// The bytes were reclaimed now.
840    Purged {
841        /// What came back.
842        bytes_reclaimed: u64,
843    },
844    /// The bytes are held until the deadline.
845    Tombstoned {
846        /// When a sweep may take them.
847        purge_after_unix_ms: i64,
848        /// What is being held — and still charged for.
849        bytes_kept: u64,
850    },
851}
852
853/// **The two things `Admin.Stats` reports that are not counts of rows**:
854/// bytes held by tombstones, and what replication is doing.
855///
856/// The standby rows are `gunnar_proto::StandbyStatus` and not a local twin:
857/// `storage_info` already returns the proto type straight through, and a
858/// second definition of "what a standby looks like" is exactly what one
859/// implementation forbids. The default is all-zero and `configured: false`, which is what a
860/// control plane with no replication *is* — a fact, not an absence.
861#[derive(Debug, Clone, Default, PartialEq)]
862pub struct StatsExtras {
863    /// Repositories deleted and not yet purged.
864    pub tombstoned_repos: u64,
865    /// Bytes they are holding — still charged to the account.
866    pub tombstoned_bytes: u64,
867    /// Is any standby configured?
868    pub replication_configured: bool,
869    /// replication counters.
870    pub replication_enqueued: u64,
871    /// Attempts a standby accepted.
872    pub replication_synced: u64,
873    /// Attempts that failed. Never resets.
874    pub replication_failed: u64,
875    /// Times the event subscription fell behind.
876    pub replication_bus_gaps: u64,
877    /// Every ref on this server, and how many of them no standby carries.
878    /// Replication copies `refs/heads/*` and `refs/tags/*` only.
879    pub replication_refs_total: u64,
880    pub replication_refs_unmirrored: u64,
881    /// One row per standby.
882    pub standbys: Vec<gunnar_proto::StandbyStatus>,
883    /// The tenants row: namespaces by ENFORCED plan state. Zero on a build
884    /// that holds no facts.
885    pub tenants_paid: u64,
886    pub tenants_grace: u64,
887    pub tenants_suspended: u64,
888}
889
890/// One soft-deleted repository, as `Repos.ListDeleted` reports it.
891#[derive(Debug, Clone, PartialEq, Eq)]
892pub struct DeletedRepoRow {
893    /// Where it was.
894    pub path: RepoPath,
895    /// When the delete was asked for.
896    pub deleted_at_unix_ms: i64,
897    /// When a sweep may reclaim it; 0 = no deadline.
898    pub purge_after_unix_ms: i64,
899    /// Who asked.
900    pub principal_id: String,
901    /// Bytes still on disk, and still on the account.
902    pub bytes: u64,
903}
904
905/// **One store directory with no catalog row and no soft-delete tombstone**,
906/// as `Repos.ListOrphans` reports it.
907///
908/// The invisible class. Every reclaim path the server had keyed off a record —
909/// `Repos.Delete` off the catalog, `Purge`/`Undelete`/`ListDeleted` and the
910/// expiry sweep off a tombstone — so a directory with neither was reachable by
911/// nothing at all, while `Repos.Create` refused its name because it was there.
912#[derive(Debug, Clone, PartialEq, Eq)]
913pub struct OrphanRow {
914    /// The name the directory stands at.
915    pub path: RepoPath,
916    /// Where it is on the server's disk. There is no shell on the appliance,
917    /// so this is reported rather than looked up.
918    pub dir: String,
919    /// What it is holding.
920    pub bytes: u64,
921    /// `sha1`, `sha256`, or **empty for a store written before the
922    /// `OBJECT_FORMAT` marker existed** — which is what a create with `adopt`
923    /// takes the caller's hash at their word about.
924    pub hash_marker: String,
925    /// Newest mtime under the directory.
926    pub modified_unix_ms: i64,
927    /// Non-empty when a tombstone records a reclaim that FAILED here.
928    pub failed_reclaim: String,
929}
930
931/// What a finished [`ControlPlane::archive_out`] cut did — the last message of
932/// a `Repos.ArchiveOut` stream.
933///
934/// It is a `gunnar-grpc` type rather than the plugin's `ArchiveOutReceipt`
935/// because this crate must not link a storage engine (the dependency budget): the seam is where
936/// the engine's answer becomes the control plane's.
937#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
938pub struct ArchiveCut {
939    /// The watermark this cut started above.
940    pub from_generation: u64,
941    /// The watermark to pass as `from_generation` next time.
942    pub to_generation: u64,
943    /// Payload bytes written into the sink.
944    pub bytes_out: u64,
945    /// SHA-256 of every byte written.
946    pub file_sha256: [u8; 32],
947}
948
949/// Where a queued maintenance run has got to.
950///
951/// The seam's own vocabulary rather than the generated `gunnar.v1.GcState`,
952/// for the same reason every other type in this file is: an adapter implements
953/// this trait without linking the protos, and the mapping to the wire lives in
954/// exactly one place (`service/repos.rs`).
955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
956pub enum GcState {
957    /// Waiting its turn behind other repositories' gcs.
958    Queued,
959    /// The one gc the server is running.
960    Running,
961    /// Finished. `bytes_reclaimed` is the answer.
962    Done,
963    /// Finished badly. The repository was NOT maintained.
964    Failed,
965}
966
967/// One queued or completed maintenance run, as a caller sees it.
968#[derive(Debug, Clone)]
969pub struct GcJob {
970    /// Stable for the life of the job, so a console can tell its own run from
971    /// somebody else's that started after it.
972    pub job_id: String,
973    pub path: RepoPath,
974    pub state: GcState,
975    /// How many jobs are ahead of this one. 0 unless [`GcState::Queued`].
976    pub queue_position: u32,
977    /// Bytes that left the filesystem. Only meaningful when [`GcState::Done`].
978    pub bytes_reclaimed: u64,
979    /// Only set when [`GcState::Failed`].
980    pub error: Option<String>,
981    pub enqueued_at_unix_ms: i64,
982    /// 0 until the job leaves [`GcState::Running`].
983    pub finished_at_unix_ms: i64,
984}
985
986// ───────────────────────────────────────────────────────────────────────────
987// Accounts: the gate, and the per-user hard purge
988// ───────────────────────────────────────────────────────────────────────────
989
990/// Why an account is locked out. Mirrors `gunnar.v1.GateReason`.
991#[derive(Debug, Clone, Copy, PartialEq, Eq)]
992pub enum GateReason {
993    /// Somebody decided. No deadline.
994    Administrative,
995    /// A purge is holding it, under a lease it re-stamps as it goes.
996    Purge,
997}
998
999/// **What `Accounts.ClaimOperator` answered** (2026-10-04): the estate's
1000/// operator, as gunnar holds it now. See the proto for the rule; the plane is
1001/// where it is kept.
1002#[derive(Debug, Clone, PartialEq, Eq)]
1003pub struct OperatorClaim {
1004    /// The operator: a USER, id = name, the account it is named after.
1005    pub principal: Principal,
1006    /// This call created the principal (and claimed its namespace).
1007    pub created: bool,
1008    /// The namespace already existed with no principal of that id and was
1009    /// adopted as the operator's account rather than refused.
1010    pub adopted_namespace: bool,
1011    /// `fed` — the installer's `operator_name` — or `asked`, the request's.
1012    pub named_by: &'static str,
1013}
1014
1015/// **The id of the operator's first invite** — the installer's break-glass
1016/// token, recorded by its hash (`Accounts.ClaimOperator`'s
1017/// `bootstrap_invite_sha256`, the feed's `operator_invite_sha256`). One per
1018/// box: recording it again replaces it.
1019pub const OPERATOR_INVITE_ID: &str = "operator-bootstrap";
1020
1021/// How long the operator's first invite stays redeemable from the moment it
1022/// is recorded — the most any invite lives. Every feed (an install, an
1023/// upgrade's re-admission) records it again while the operator holds no
1024/// passkey, so a box that waits longer than this for its owner is renewed by
1025/// its next feed.
1026pub const OPERATOR_INVITE_TTL_MS: i64 = 30 * 24 * 60 * 60 * 1000;
1027
1028/// **What became of the operator's first invite.**
1029#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1030pub enum BootstrapInvite {
1031    /// Recorded now (none was pending, or a different hash was).
1032    Recorded,
1033    /// The same hash was pending; its expiry is renewed.
1034    Renewed,
1035    /// The operator already holds a passkey: the bootstrap is spent, nothing
1036    /// is recorded, and a leftover one is withdrawn.
1037    Spent,
1038}
1039
1040/// **The least a first invite may live**: a minute. A link that dies before
1041/// a person can open it is no way in.
1042pub const OPERATOR_INVITE_MIN_TTL_MS: i64 = 60 * 1000;
1043
1044/// **A first invite's lifetime, checked** (private ownership, 2026-10-07):
1045/// at least [`OPERATOR_INVITE_MIN_TTL_MS`], at most
1046/// [`OPERATOR_INVITE_TTL_MS`] — the most any invite lives. `INVALID_ARGUMENT`
1047/// otherwise, by name; nothing is recorded.
1048pub fn bootstrap_ttl(ttl_ms: i64) -> ControlResult<i64> {
1049    if (OPERATOR_INVITE_MIN_TTL_MS..=OPERATOR_INVITE_TTL_MS).contains(&ttl_ms) {
1050        Ok(ttl_ms)
1051    } else {
1052        Err(ControlError::InvalidArgument(format!(
1053            "the first invite's lifetime is {ttl_ms} ms: it lives at least a minute and at most {} days",
1054            OPERATOR_INVITE_TTL_MS / 86_400_000
1055        )))
1056    }
1057}
1058
1059/// A bootstrap hash as the wire carries it: 64 lowercase hex digits (what
1060/// `webauthn::sha256_hex` writes and an invite row keeps). `INVALID_ARGUMENT`
1061/// otherwise — a token sent in place of its hash is refused, never stored.
1062pub fn bootstrap_hash(raw: &str) -> ControlResult<String> {
1063    let hash = raw.trim();
1064    if hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) {
1065        Ok(hash.to_owned())
1066    } else {
1067        Err(ControlError::InvalidArgument(
1068            "the bootstrap invite is the lowercase hex SHA-256 of the break-glass token (64 \
1069             characters), never the token itself"
1070                .into(),
1071        ))
1072    }
1073}
1074
1075/// **Which name is the operator's**, the one rule both planes keep: the fed
1076/// name wins; the asked one only when nothing was fed; neither is
1077/// `FAILED_PRECONDITION`. Blank is absent.
1078pub fn operator_name<'a>(
1079    fed: Option<&'a str>,
1080    asked: &'a str,
1081) -> ControlResult<(&'a str, &'static str)> {
1082    match (fed.map(str::trim).filter(|n| !n.is_empty()), asked.trim()) {
1083        (Some(fed), _) => Ok((fed, "fed")),
1084        (None, asked) if !asked.is_empty() => Ok((asked, "asked")),
1085        _ => Err(ControlError::FailedPrecondition(
1086            "no operator is named for this box: the installer fed no `operator_name` and the \
1087             request named nobody"
1088                .into(),
1089        )),
1090    }
1091}
1092
1093/// **One account, locked**, as `Accounts` reports it.
1094#[derive(Debug, Clone, PartialEq, Eq)]
1095pub struct AccountGate {
1096    /// A principal id, which is also the namespace that principal owns.
1097    pub account: String,
1098    pub reason: GateReason,
1099    /// A principal id for a person, a job id for a purge. Only a matching
1100    /// holder may lift its own gate.
1101    pub holder: String,
1102    /// The principal that asked, for the audit trail.
1103    pub actor: String,
1104    /// Shown to the locked-out user in the refusal they get.
1105    pub note: String,
1106    pub at_unix_ms: i64,
1107    /// **0 = no deadline.** Non-zero is a purge's lease, which is what stops a
1108    /// dead purge from bricking an account.
1109    pub expires_unix_ms: i64,
1110}
1111
1112/// Where an account purge has got to. Mirrors `gunnar.v1.PurgeState`.
1113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1114pub enum PurgeState {
1115    /// Waiting its turn behind other accounts' purges.
1116    Queued,
1117    /// The one purge the server is running.
1118    Running,
1119    /// Finished, and everything it set out to reclaim is gone.
1120    Done,
1121    /// Finished badly. The account is PARTIAL and `failures` says where.
1122    Failed,
1123}
1124
1125/// One queued or completed account purge, as a caller sees it.
1126#[derive(Debug, Clone)]
1127pub struct PurgeJob {
1128    /// Stable for the life of the job, and also the gate's `holder` while it
1129    /// runs.
1130    pub job_id: String,
1131    pub account: String,
1132    pub state: PurgeState,
1133    /// How many jobs are ahead of this one. 0 unless [`PurgeState::Queued`].
1134    pub queue_position: u32,
1135    /// Stores the walk found. **0 until the job starts** — counted when its
1136    /// turn comes, because an account can grow while a job waits.
1137    pub stores_found: u32,
1138    /// How many of those are gone.
1139    pub stores_purged: u32,
1140    pub bytes_reclaimed: u64,
1141    pub principal_removed: bool,
1142    /// **Whether the lockout this job took has been released.** The property
1143    /// that decides whether the account is usable again, on the wire rather
1144    /// than only in a log. False on a job that found an administrative gate
1145    /// already in place: it did not take that one and must not lift it.
1146    pub gate_released: bool,
1147    /// Only set when [`PurgeState::Failed`].
1148    pub error: Option<String>,
1149    /// One line per store that could not be reclaimed. The recovery list.
1150    pub failures: Vec<String>,
1151    pub enqueued_at_unix_ms: i64,
1152    /// 0 until the job leaves [`PurgeState::Queued`].
1153    pub started_at_unix_ms: i64,
1154    /// 0 until the job leaves [`PurgeState::Running`].
1155    pub finished_at_unix_ms: i64,
1156}
1157
1158/// **What an enrolment asks** (`Accounts.EnrollEmployee`; owner, 2026-10-04,
1159/// "Add users"): ONE named employee holding `room_bytes`, `seats` unnamed
1160/// seats each holding it, or a seat claim (`from_seat`). The service has
1161/// already applied the rules a person types against (count range, names,
1162/// the room's default and floor); the plane keeps the rules about what is
1163/// STORED: a new principal only, an open seat for a claim, and the room.
1164#[derive(Debug, Clone, Default, PartialEq, Eq)]
1165pub struct EmployeeAsk {
1166    /// The new principal's id and display name; both empty for seats.
1167    pub id: String,
1168    pub name: String,
1169    /// Bytes per employee or seat. Ignored for a claim: the seat decides.
1170    pub room_bytes: u64,
1171    /// Unnamed seats to grant; 0: one named employee (or a claim).
1172    pub seats: u32,
1173    /// Fill an open seat with the named employee.
1174    pub from_seat: bool,
1175    /// The account's room — its `pack_bytes` cap. `None`: nothing caps it on
1176    /// this server.
1177    pub cap: Option<u64>,
1178    /// What the account stores now (counted against `cap`).
1179    pub stored: u64,
1180    /// The order this belongs to, for its retry; empty: none.
1181    pub order: String,
1182    /// Ask, write nothing.
1183    pub dry_run: bool,
1184}
1185
1186/// **The plane's answer** to an [`EmployeeAsk`]. A short account is an
1187/// answer, not an error, at this layer: `shortfall_bytes > 0` with
1188/// `unchanged`, nothing written — the SERVICE turns it into the refusal, in
1189/// one sentence for every plane.
1190#[derive(Debug, Clone, Default, PartialEq)]
1191pub struct EmployeeAnswer {
1192    pub principal: Option<Principal>,
1193    pub room_bytes: u64,
1194    pub seats: u32,
1195    pub seats_open: u32,
1196    pub need_bytes: u64,
1197    pub unused_bytes: Option<u64>,
1198    pub shortfall_bytes: u64,
1199    pub unchanged: bool,
1200}
1201
1202/// What `Accounts.Purge` was asked for, carried whole rather than as three
1203/// positional booleans nobody can read at a call site.
1204#[derive(Debug, Clone, Default)]
1205pub struct PurgeAccountOptions {
1206    /// The principal that asked. Recorded on the gate.
1207    pub principal_id: String,
1208    /// Why. Required at the edge; shown to the locked-out user.
1209    pub reason: String,
1210    /// Also remove the principal, its keys, grants, tokens and memberships.
1211    pub remove_principal: bool,
1212    /// Refuse unless the account holds no live repositories.
1213    pub expect_empty: bool,
1214}
1215
1216/// The control plane's view of storage. See the module docs: this is a seam.
1217///
1218/// [`Identity`] is a supertrait, not an optional extra: a control plane that
1219/// cannot say who a key belongs to and whether they may act cannot be served
1220/// safely, and making that a compile error is cheaper than making it a runtime
1221/// hole.
1222pub trait ControlPlane: Identity + FirstBoot + Send + Sync + 'static {
1223    // ── namespaces (multi-tenancy) ─────────────────────────────────────────
1224    fn create_namespace(&self, name: &str, description: &str) -> ControlResult<Namespace>;
1225    fn list_namespaces(&self) -> ControlResult<Vec<Namespace>>;
1226    /// **One namespace, if it exists** — what the account settings and the
1227    /// outsider's view read (`Namespace.public`, `Namespace.events_public`).
1228    ///
1229    /// The default walks [`ControlPlane::list_namespaces`], which is right for
1230    /// the test double; the real adapter answers it with one catalog probe.
1231    fn namespace(&self, name: &str) -> ControlResult<Option<Namespace>> {
1232        Ok(self
1233            .list_namespaces()?
1234            .into_iter()
1235            .find(|namespace| namespace.name == name))
1236    }
1237    /// **`name` as an outsider may be shown it** — the namespace whose
1238    /// `public` / `events_public` its account admin opened, read for a caller
1239    /// who is NOT inside it. `None` when it does not exist, and also while the
1240    /// account or the caller is gated: the gate outranks the admin's setting
1241    /// exactly as it outranks `Visibility::Public` in `AuthTable::may`, so a
1242    /// public account that is being emptied is not listed and its events are
1243    /// not shown, and a locked-out principal is shown no public account at all.
1244    ///
1245    /// The default is [`ControlPlane::namespace`], which is right only for a
1246    /// double that cannot gate anybody (its `gate_account` is
1247    /// `Unimplemented`). An adapter that gates answers it with the gate read.
1248    fn namespace_for_outsider(&self, principal_id: &str, name: &str) -> ControlResult<Option<Namespace>> {
1249        let _ = principal_id;
1250        self.namespace(name)
1251    }
1252    /// **The account admin's two settings** (owner, 2026-10-03: namespaces and
1253    /// emitted events are not public by default, "such things has to be
1254    /// set"): is the namespace public, are its events public. `None` leaves a
1255    /// setting as it is. Who may (the account admin) is the service's question.
1256    fn set_namespace_settings(
1257        &self,
1258        name: &str,
1259        public: Option<bool>,
1260        events_public: Option<bool>,
1261    ) -> ControlResult<Namespace>;
1262    /// Returns how many repos went with it. Refuses a non-empty namespace
1263    /// unless `delete_repos`.
1264    fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32>;
1265
1266    // ── repos ──────────────────────────────────────────────────────────────
1267    fn create_repo(&self, spec: NewRepo) -> ControlResult<Repo>;
1268
1269    /// **Has `namespace` room for a NEW repository, by its plan?** — the
1270    /// questions a push-to-create meets at the front door, asked for
1271    /// `Repos.Create` (owner 2026-10-06: the console's New repository is the
1272    /// account admin's, by the push's rules): the plan's verdict on a write,
1273    /// then whether the namespace is already at its cap. `machine`: the caller
1274    /// is a machine principal, which a plan does not bind unless it says so
1275    /// (as on the SSH door). The refusal is the plan's own line.
1276    ///
1277    /// Defaulted to `Ok(())`: a plane with no plan (the memory double, a
1278    /// community build) has room, which is what it answered before.
1279    fn create_room(&self, _namespace: &str, _machine: bool) -> ControlResult<()> {
1280        Ok(())
1281    }
1282    fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo>;
1283    fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>>;
1284    /// **Delete a repository: a dated tombstone by default, a reclaim only
1285    /// when [`DeleteOptions::purge`] says so.**
1286    ///
1287    /// The signature carries the whole decision rather than a bare path
1288    /// because there is no safe default for the caller to omit: `purge` is the
1289    /// difference between "the name is free again in thirty days" and "181 GiB
1290    /// went in two seconds", and the principal is what the tombstone records
1291    /// so an undelete can say who deleted it.
1292    fn delete_repo(&self, path: &RepoPath, options: DeleteOptions) -> ControlResult<DeleteOutcome>;
1293
1294    /// **Put a soft-deleted repository back**, inside its window. Returns the
1295    /// restored catalog row, with the recorded hash kind and default branch.
1296    fn undelete_repo(&self, path: &RepoPath) -> ControlResult<Repo> {
1297        let _ = path;
1298        Err(ControlError::Unimplemented("Repos.Undelete"))
1299    }
1300
1301    /// **Reclaim a soft-deleted repository's bytes now.** Returns the bytes.
1302    fn purge_repo(&self, path: &RepoPath) -> ControlResult<u64> {
1303        let _ = path;
1304        Err(ControlError::Unimplemented("Repos.Purge"))
1305    }
1306
1307    /// **Ask for a LIVE repository to be maintained** (`Repos.Gc`): seal the
1308    /// live logs, compact the archive, unlink the exploded read cache.
1309    ///
1310    /// **Returns when the job is ACCEPTED, not when the gc has run.** A gc is
1311    /// hundreds of thousands of unlinks and the box has one disk, so the plane
1312    /// runs one at a time and queues the rest; an RPC that blocked for the
1313    /// duration is what makes a console button feel broken. The returned
1314    /// [`GcJob`] is the one now responsible for the repository — a fresh one,
1315    /// or the one that was already queued or running, because a button gets
1316    /// pressed twice and twice is one gc.
1317    ///
1318    /// Named apart from [`ControlPlane::purge_repo`] because they are opposite
1319    /// in consequence and identical only in shape. A purge ends a retention
1320    /// window and there is no undo; this one reclaims what the server can
1321    /// re-derive, so the worst it costs is a slower next read.
1322    fn gc_repo(&self, path: &RepoPath) -> ControlResult<GcJob> {
1323        let _ = path;
1324        Err(ControlError::Unimplemented("Repos.Gc"))
1325    }
1326
1327    /// **How a repository's gc is going** (`Repos.GcStatus`).
1328    ///
1329    /// `Ok(None)` is a real answer and not a stub: the plane is holding no job
1330    /// for this repository, because none was ever asked for or the last one
1331    /// finished long enough ago to have been dropped. A plane that cannot see
1332    /// the queue at all still answers `Unimplemented` — "nothing is running" and
1333    /// "I cannot tell" must not look alike.
1334    fn gc_status(&self, path: &RepoPath) -> ControlResult<Option<GcJob>> {
1335        let _ = path;
1336        Err(ControlError::Unimplemented("Repos.GcStatus"))
1337    }
1338
1339    /// What is deleted and not yet purged. Empty is a legitimate answer here —
1340    /// unlike a ref list, "nothing is deleted" is a fact a caller can act on —
1341    /// but the default is still `Unimplemented`, because a control plane that
1342    /// cannot see tombstones must not report that there are none.
1343    fn list_deleted(&self, namespace: Option<&str>) -> ControlResult<Vec<DeletedRepoRow>> {
1344        let _ = namespace;
1345        Err(ControlError::Unimplemented("Repos.ListDeleted"))
1346    }
1347    /// **Every store directory with no catalog row and no soft-delete
1348    /// tombstone** (`Repos.ListOrphans`).
1349    ///
1350    /// Empty is a legitimate answer — "nothing is stranded" is a fact an
1351    /// operator can act on — but the default is still `Unimplemented`, for the
1352    /// same reason [`ControlPlane::list_deleted`]'s is: a control plane that
1353    /// cannot walk the disk must not report that the disk is clean.
1354    fn list_orphans(&self, namespace: Option<&str>) -> ControlResult<Vec<OrphanRow>> {
1355        let _ = namespace;
1356        Err(ControlError::Unimplemented("Repos.ListOrphans"))
1357    }
1358
1359    /// **Reclaim an orphan's bytes** (`Repos.ReclaimOrphan`). Returns them.
1360    ///
1361    /// Refuses a catalogued or soft-deleted repository by name: each of those
1362    /// has its own verb, and a second door onto destroying a live repository
1363    /// is not a recovery tool.
1364    fn reclaim_orphan(&self, path: &RepoPath) -> ControlResult<u64> {
1365        let _ = path;
1366        Err(ControlError::Unimplemented("Repos.ReclaimOrphan"))
1367    }
1368
1369    fn set_default_branch(&self, path: &RepoPath, default_branch: &str) -> ControlResult<Repo>;
1370    fn set_visibility(&self, path: &RepoPath, visibility: Visibility) -> ControlResult<Repo>;
1371
1372    /// Turn a repository's "encrypted" flag on or off (`Repos.SetEncrypted`).
1373    /// The caller has already checked that the account admin asks.
1374    fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo> {
1375        let _ = (path, encrypted);
1376        Err(ControlError::Unimplemented("Repos.SetEncrypted"))
1377    }
1378
1379    /// A principal's SSH public keys as stored OpenSSH lines
1380    /// (`Repos.RecipientKeys`). The caller has already checked access.
1381    fn recipient_keys(&self, principal_id: &str) -> ControlResult<Vec<gunnar_proto::RecipientKey>> {
1382        let _ = principal_id;
1383        Err(ControlError::Unimplemented("Repos.RecipientKeys"))
1384    }
1385
1386    /// **A person's encryption key and its unlock methods** (`Access.GetEncryptionKey`,
1387    /// CRYPT-YUBI). `exists: false` when they have none. The caller is the person.
1388    fn encryption_key(&self, principal_id: &str) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
1389        let _ = principal_id;
1390        Err(ControlError::Unimplemented("Access.GetEncryptionKey"))
1391    }
1392    /// The first time (`Access.CreateEncryptionKey`); `AlreadyExists` when one is held.
1393    fn create_encryption_key(
1394        &self,
1395        principal_id: &str,
1396        public_key: &str,
1397        first: gunnar_proto::UnlockMethod,
1398    ) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
1399        let _ = (principal_id, public_key, first);
1400        Err(ControlError::Unimplemented("Access.CreateEncryptionKey"))
1401    }
1402    /// One more unlock method (`Access.AddUnlockMethod`).
1403    fn add_unlock_method(&self, principal_id: &str, method: gunnar_proto::UnlockMethod) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
1404        let _ = (principal_id, method);
1405        Err(ControlError::Unimplemented("Access.AddUnlockMethod"))
1406    }
1407    /// Delete one method's wrap (`Access.RemoveUnlockMethod`); the last is `FailedPrecondition`.
1408    fn remove_unlock_method(&self, principal_id: &str, id: &str) -> ControlResult<gunnar_proto::GetEncryptionKeyResponse> {
1409        let _ = (principal_id, id);
1410        Err(ControlError::Unimplemented("Access.RemoveUnlockMethod"))
1411    }
1412
1413    // ── accounts: the gate, and the per-user hard purge ────────────────────
1414
1415    /// **Lock `account` out, completely** (`Accounts.Gate`).
1416    ///
1417    /// Returns the gate as stored, and whether the account was **already**
1418    /// gated when the request arrived. Idempotent; the newest writer owns the
1419    /// row, because an operator must be able to suspend an account a purge is
1420    /// already holding.
1421    ///
1422    /// The default is [`ControlError::Unimplemented`] and not a silent success:
1423    /// an adapter that cannot lock anybody out must not answer "locked", or
1424    /// [`ControlPlane::purge_account`] would walk an account that is still
1425    /// live and a caller would be told the user was shut out when they were
1426    /// not.
1427    fn gate_account(
1428        &self,
1429        account: &str,
1430        actor: &str,
1431        note: &str,
1432    ) -> ControlResult<(AccountGate, bool)> {
1433        let _ = (account, actor, note);
1434        Err(ControlError::Unimplemented("Accounts.Gate"))
1435    }
1436
1437    /// **Let `account` back in** (`Accounts.Ungate`). Returns what was lifted,
1438    /// or `None` if it was not gated.
1439    ///
1440    /// Unconditional — it does not consult the holder — because this is the
1441    /// operator's override, and an override that could be blocked by a lease
1442    /// belonging to a process that no longer exists is not an override.
1443    fn ungate_account(&self, account: &str) -> ControlResult<Option<AccountGate>> {
1444        let _ = account;
1445        Err(ControlError::Unimplemented("Accounts.Ungate"))
1446    }
1447
1448    /// **Every account locked out right now** (`Accounts.ListAccountGates`).
1449    ///
1450    /// Empty is a legitimate answer; the default is still `Unimplemented`, for
1451    /// the reason [`ControlPlane::list_deleted`]'s is. A plane that cannot read
1452    /// the gate table must not report that nobody is locked out.
1453    fn account_gates(&self) -> ControlResult<Vec<AccountGate>> {
1454        Err(ControlError::Unimplemented("Accounts.ListAccountGates"))
1455    }
1456
1457    /// **Empty an account: gate it NOW, enqueue the walk** (`Accounts.Purge`).
1458    ///
1459    /// Returns when the account is locked out and the job is accepted, **not**
1460    /// when the account is empty. The two halves are deliberately different in
1461    /// timing: the gate is synchronous so there is no window in which a purge
1462    /// has been accepted and the user can still push, and the walk is queued
1463    /// because it is minutes of unlink I/O and an RPC that held a tonic worker
1464    /// for it would make a console button look broken.
1465    ///
1466    /// The returned [`AccountGate`] is the one now in force — which may be an
1467    /// administrative gate that was already there rather than this job's lease.
1468    fn purge_account(
1469        &self,
1470        account: &str,
1471        options: PurgeAccountOptions,
1472    ) -> ControlResult<(PurgeJob, AccountGate)> {
1473        let _ = (account, options);
1474        Err(ControlError::Unimplemented("Accounts.Purge"))
1475    }
1476
1477    /// **How an account's purge is going** (`Accounts.PurgeStatus`), and the
1478    /// lockout as it stands.
1479    ///
1480    /// `Ok((None, None))` is a real answer: no job is being held for this
1481    /// account and it is not gated. A plane that cannot see the queue answers
1482    /// `Unimplemented` instead — "nothing is running" and "I cannot tell" must
1483    /// not look alike.
1484    fn purge_account_status(
1485        &self,
1486        account: &str,
1487    ) -> ControlResult<(Option<PurgeJob>, Option<AccountGate>)> {
1488        let _ = account;
1489        Err(ControlError::Unimplemented("Accounts.PurgeStatus"))
1490    }
1491
1492    /// **Write one repository's archive bytes above `from_generation` into
1493    /// `sink`** (`Repos.ArchiveOut`).
1494    ///
1495    /// The bytes are exactly what `gunnar repo archive-out --to FILE` writes,
1496    /// so what comes off the wire restores with `archive-in` and nothing new
1497    /// has to understand a second format.
1498    ///
1499    /// **Blocking, on purpose.** It reads files and writes a sink; the service
1500    /// runs it on the blocking pool rather than pretending it is async, which
1501    /// is the same shape `replicate_once` already uses for send-pack.
1502    ///
1503    /// The default is [`ControlError::Unimplemented`] and not an empty
1504    /// success: an archive stream that ends immediately looks exactly like a
1505    /// repository with no bytes, and a backup that silently archived nothing is
1506    /// the failure this whole surface exists to prevent.
1507    fn archive_out(
1508        &self,
1509        path: &RepoPath,
1510        from_generation: u64,
1511        sink: &mut dyn std::io::Write,
1512    ) -> ControlResult<ArchiveCut> {
1513        let _ = (path, from_generation, sink);
1514        Err(ControlError::Unimplemented("Repos.ArchiveOut"))
1515    }
1516
1517    // ── refs and objects (the storage seam: RefStore / ObjectStore) ────────
1518    fn list_refs(&self, path: &RepoPath, prefix: &str, peel: bool)
1519        -> ControlResult<Vec<RefTarget>>;
1520    fn get_ref(&self, path: &RepoPath, ref_name: &str, peel: bool) -> ControlResult<RefTarget>;
1521    fn log(
1522        &self,
1523        path: &RepoPath,
1524        start: &str,
1525        limit: u32,
1526        touching: &str,
1527    ) -> ControlResult<Vec<Commit>>;
1528    /// [`ControlPlane::log`] walked from every branch and tag tip (and `HEAD`)
1529    /// at once, newest committer time first, at most `limit` commits.
1530    fn log_all_refs(
1531        &self,
1532        path: &RepoPath,
1533        limit: u32,
1534        touching: &str,
1535    ) -> ControlResult<Vec<Commit>> {
1536        let _ = (path, limit, touching);
1537        Err(ControlError::Unimplemented("Refs.Log all_refs"))
1538    }
1539
1540    // ── pull requests ──────────────────────────────────────────────────────
1541    /// Open a pull request. `author` is the authenticated principal.
1542    fn create_pull(
1543        &self,
1544        path: &RepoPath,
1545        author: &str,
1546        title: &str,
1547        description: &str,
1548        source_ref: &str,
1549        target_ref: &str,
1550    ) -> ControlResult<PullRequest>;
1551    /// Every pull in one repository; `state` zero means all.
1552    fn list_pulls(&self, path: &RepoPath, state: i32) -> ControlResult<Vec<PullRequest>>;
1553    fn get_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>;
1554    fn close_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>;
1555    /// FAST-FORWARD ONLY. Moves the target ref through the store's CAS and
1556    /// returns what moved, so the service can publish the same `RefUpdated`
1557    /// a push would.
1558    ///
1559    /// # Why the caller's identity crosses this seam
1560    ///
1561    /// It did not, until GUN-004. `merge_pull(path, number)` moved a
1562    /// **protected** ref and the principal-dependent half of branch protection
1563    /// — `allowed_principals` — was impossible to evaluate on the far side,
1564    /// because the far side did not know who was asking. So `PullRequests.Merge`
1565    /// read the same policy document the push path reads, took
1566    /// `forbid_self_approval` and `required_approvals` out of it, and wrote the
1567    /// ref with `update_ref`: an explicitly configured per-ref protection,
1568    /// bypassed by a side door, while the push path refused correctly and the
1569    /// protection *looked* like it worked.
1570    ///
1571    /// `principal_id` is `Caller::principal_id`, the same string
1572    /// `ServiceContext::require` just checked a grant for.
1573    fn merge_pull(
1574        &self,
1575        path: &RepoPath,
1576        number: u64,
1577        principal_id: &str,
1578    ) -> ControlResult<(PullRequest, MergedRef)>;
1579    /// **A new repository's first commit, written by the server**
1580    /// (`CreateRepoRequest.readme`): `README.md` holding `readme` on the default
1581    /// branch. Answers the commit id (hex). A control plane that keeps no git
1582    /// store refuses by name.
1583    fn write_first_commit(&self, path: &RepoPath, readme: &str) -> ControlResult<String> {
1584        let _ = readme;
1585        Err(ControlError::FailedPrecondition(format!("this control plane writes no commit into {path}: it keeps no git store")))
1586    }
1587    fn read_object(
1588        &self,
1589        path: &RepoPath,
1590        oid: &str,
1591        max_bytes: u64,
1592    ) -> ControlResult<ReadObjectResponse>;
1593
1594    /// **One LFS object, by the sha256 its pointer names** (`Refs.ReadLfsObject`).
1595    ///
1596    /// A file kept in LFS is a POINTER in the tree, so a caller that reads the
1597    /// tree gets 131 bytes of text where the file should be. This is the way
1598    /// to the bytes for a caller that already holds Read on the repository and
1599    /// speaks gRPC rather than one of `git lfs`'s transfer adapters.
1600    ///
1601    /// Whole object or nothing: a prefix of a git blob is a readable prefix of
1602    /// a file, and a prefix of an LFS object is a broken PDF. Past the ceiling
1603    /// it is refused, and the caller that wants it anyway wants `git lfs`.
1604    ///
1605    /// `range` is `Some((offset, length))` for a ranged read (the request's
1606    /// `offset`/`length`, not both 0): the bytes from `offset`, at most
1607    /// `length` (0 = to the ceiling), the ceiling applied to the range, an
1608    /// offset past the end refused. `None` is the whole-or-refused read.
1609    ///
1610    /// Defaulted to `Unimplemented` so a control plane that has no LFS store —
1611    /// the seams in this crate's tests — needs no stub.
1612    fn read_lfs_object(
1613        &self,
1614        path: &RepoPath,
1615        oid: &str,
1616        max_bytes: u64,
1617        range: Option<(u64, u64)>,
1618    ) -> ControlResult<ReadLfsObjectResponse> {
1619        let _ = (path, oid, max_bytes, range);
1620        Err(ControlError::Unimplemented("Refs.ReadLfsObject"))
1621    }
1622    fn diff(
1623        &self,
1624        path: &RepoPath,
1625        from: &str,
1626        to: &str,
1627        path_prefixes: &[String],
1628    ) -> ControlResult<Vec<FileChange>>;
1629
1630    /// **Blame a file at a revision** (`Refs.Blame`).
1631    ///
1632    /// The walk itself lives in `gunnar-server`'s `blame` module — the ONE
1633    /// implementation the store-local CLI verb already uses (one implementation) — and it
1634    /// runs here against the store the running server already holds, so a
1635    /// no-shell appliance can be blamed over the control plane while the
1636    /// exclusive lock that verb takes makes it impossible to run down.
1637    ///
1638    /// The default is `Unimplemented`, not an empty blame: a control plane that
1639    /// cannot walk must refuse, because zero lines reads as "an empty file",
1640    /// which is a wrong answer a console reader would believe.
1641    fn blame(
1642        &self,
1643        path: &RepoPath,
1644        rev: &str,
1645        file: &str,
1646    ) -> ControlResult<gunnar_proto::BlameResponse> {
1647        let _ = (path, rev, file);
1648        Err(ControlError::Unimplemented("Refs.Blame"))
1649    }
1650
1651    /// **Search one revision's files for a literal string** (`Refs.Search`).
1652    ///
1653    /// The same store-shared, walk-once-live implementation as [`blame`]: the
1654    /// `search` module in `gunnar-server`, reached against the running server's
1655    /// open store. `max_file_bytes` and `max_matches` of zero mean the module's
1656    /// defaults. Empty is a legitimate answer (a string that is not there), so
1657    /// the refusal below is the walk that could not START, never a miss.
1658    ///
1659    /// [`blame`]: ControlPlane::blame
1660    fn search(
1661        &self,
1662        path: &RepoPath,
1663        rev: &str,
1664        pattern: &str,
1665        max_file_bytes: u64,
1666        max_matches: u32,
1667    ) -> ControlResult<gunnar_proto::SearchResponse> {
1668        let _ = (path, rev, pattern, max_file_bytes, max_matches);
1669        Err(ControlError::Unimplemented("Refs.Search"))
1670    }
1671
1672    // ── identity and access (the ACL model) ────────────────────────────────
1673    /// Create a principal with `id` as its identity (and account) and `name`
1674    /// as its display name. The service resolves an empty wire `id` to the
1675    /// name before calling, so this never sees an empty id.
1676    fn add_principal(&self, id: &str, name: &str, kind: PrincipalKind)
1677        -> ControlResult<Principal>;
1678    /// **Enroll an employee: a NEW user created by and bound to `account`**
1679    /// (`Accounts.EnrollEmployee`). `AlreadyExists` for an id or name that is
1680    /// taken — an existing principal never becomes an employee — and for an id
1681    /// that names an existing namespace (its owner-by-name would be the
1682    /// employee). `NotFound` when `account` is not a namespace. The employee
1683    /// gets no namespace of its own and `Read` on the account's.
1684    ///
1685    /// With room ("Add users"): see [`EmployeeAsk`]. `FailedPrecondition`
1686    /// for a claim with no open seat; a short account is answered with
1687    /// `shortfall_bytes` and nothing written.
1688    fn enroll_employee(&self, account: &str, ask: &EmployeeAsk) -> ControlResult<EmployeeAnswer>;
1689    /// The account's employees, by id.
1690    fn employees(&self, account: &str) -> ControlResult<Vec<Principal>>;
1691    /// **Has the account enrolled anyone?** What `Access.WhoAmI` asks on every call; a plane
1692    /// that can answer without listing (CoreControlPlane) says so.
1693    fn has_employees(&self, account: &str) -> ControlResult<bool> {
1694        Ok(!self.employees(account)?.is_empty())
1695    }
1696    /// The account's seats: every seat granted, and the open ones.
1697    fn seats(&self, account: &str) -> ControlResult<(u32, u32)> {
1698        let _ = account;
1699        Ok((0, 0))
1700    }
1701    /// **Clans** (2026-10-05, [`crate::clans::ClanPlane`]), when this plane
1702    /// keeps an identity table that holds them. `None`: every `Clans` verb is
1703    /// UNIMPLEMENTED and no namespace is a clan's.
1704    fn clans(&self) -> Option<&dyn crate::clans::ClanPlane> {
1705        None
1706    }
1707    /// **The estate's operator, created on first claim** (`Accounts.ClaimOperator`,
1708    /// 2026-10-04). `asked` is the caller's name for it, used only when nothing
1709    /// was fed ([`operator_name`]). Idempotent; an ownerless namespace of that
1710    /// name is adopted; a machine under the name is `FAILED_PRECONDITION`. The
1711    /// principal is created keyless and gunnar-admin. The default is the
1712    /// honest answer of a plane that keeps no operator.
1713    fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim> {
1714        let _ = asked;
1715        Err(ControlError::Unimplemented("Accounts.ClaimOperator"))
1716    }
1717    /// **The operator's first invite** (Stage L, 2026-10-04): record
1718    /// `secret_sha256` (checked by [`bootstrap_hash`]) as the invite
1719    /// [`OPERATOR_INVITE_ID`] for `operator`'s first passkey — no server
1720    /// grant, expiring `ttl_ms` after `now` (checked by [`bootstrap_ttl`]:
1721    /// [`OPERATOR_INVITE_TTL_MS`] for the kit's own estates, shorter for a
1722    /// SOLD estate's first-login link) — unless the operator already holds a
1723    /// passkey ([`BootstrapInvite::Spent`]). The console's first-admin
1724    /// registration, or the buyer's first-login link, redeems it through
1725    /// `Access.WebRegister`.
1726    fn operator_bootstrap_invite(
1727        &self,
1728        operator: &str,
1729        secret_sha256: &str,
1730        now_unix_ms: i64,
1731        ttl_ms: i64,
1732    ) -> ControlResult<BootstrapInvite> {
1733        let _ = (operator, secret_sha256, now_unix_ms, ttl_ms);
1734        Err(ControlError::Unimplemented("Accounts.ClaimOperator (bootstrap invite)"))
1735    }
1736    fn list_principals(&self) -> ControlResult<Vec<Principal>>;
1737    /// **When each principal last signed in and last changed something**
1738    /// (2026-10-21), by principal id; a principal with no entry was never seen.
1739    /// The default knows nothing — a plane that keeps no stamps answers
1740    /// "never" for everybody, never an error.
1741    fn principal_activity(&self) -> ControlResult<std::collections::BTreeMap<String, PrincipalSeen>> {
1742        Ok(std::collections::BTreeMap::new())
1743    }
1744    /// One principal by its display name, or `None`.
1745    fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>>;
1746    /// One principal by its ID, or `None` (`Access.WhoAmI`). The default walks
1747    /// [`ControlPlane::list_principals`]; a plane with an id index may answer
1748    /// it directly.
1749    fn principal(&self, id: &str) -> ControlResult<Option<Principal>> {
1750        Ok(self.list_principals()?.into_iter().find(|p| p.id == id))
1751    }
1752    /// **Does `principal_id` hold a SERVER-scope grant** (the seller's mark,
1753    /// `Namespace.owner_server_grant`)? The default: no grant is known here.
1754    fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool> {
1755        let _ = principal_id;
1756        Ok(false)
1757    }
1758    /// Change the display name and nothing else. `AlreadyExists` when another
1759    /// principal carries the name (as name or id); `NotFound` for an unknown id.
1760    fn rename_principal(&self, principal_id: &str, name: &str) -> ControlResult<Principal>;
1761    /// **Register a web credential on a principal** (`Access.AddCredential`,
1762    /// Stage L). `NotFound` for an unknown principal; `FailedPrecondition` for
1763    /// one that is not a USER (no human credential on a machine principal);
1764    /// `AlreadyExists` for a credential id already registered. The public key
1765    /// is validated by the service before it gets here. Removed with the
1766    /// principal. Defaults to `Unimplemented`: a plane that keeps no identity
1767    /// table keeps no passkeys either.
1768    fn add_credential(
1769        &self,
1770        principal_id: &str,
1771        credential_id: &[u8],
1772        public_key: &[u8],
1773        label: &str,
1774        sign_count: u32,
1775    ) -> ControlResult<gunnar_proto::WebCredential> {
1776        let _ = (principal_id, credential_id, public_key, label, sign_count);
1777        Err(ControlError::Unimplemented("Access.AddCredential"))
1778    }
1779    /// One principal's web credentials, or every one (`None`).
1780    fn credentials(&self, principal_id: Option<&str>) -> ControlResult<Vec<gunnar_proto::WebCredential>> {
1781        let _ = principal_id;
1782        Err(ControlError::Unimplemented("Access.ListCredentials"))
1783    }
1784    /// One web credential with its public key, or `None`.
1785    fn credential(&self, credential_id: &[u8]) -> ControlResult<Option<StoredCredential>> {
1786        let _ = credential_id;
1787        Err(ControlError::Unimplemented("Access.RevokeCredential"))
1788    }
1789    /// Change a web credential's label; the updated row, or `None` if there
1790    /// is no such credential.
1791    fn rename_credential(
1792        &self,
1793        credential_id: &[u8],
1794        label: &str,
1795    ) -> ControlResult<Option<gunnar_proto::WebCredential>> {
1796        let _ = (credential_id, label);
1797        Err(ControlError::Unimplemented("Access.RenameCredential"))
1798    }
1799    /// **Store an invite** (`Access.CreateInvite`, Stage L). `NotFound` for an
1800    /// unknown principal, `FailedPrecondition` for one that is not a USER,
1801    /// `AlreadyExists` for an id taken. Removed with the principal.
1802    fn create_invite(&self, invite: NewInvite) -> ControlResult<gunnar_proto::Invite> {
1803        let _ = invite;
1804        Err(ControlError::Unimplemented("Access.CreateInvite"))
1805    }
1806    /// Every stored invite, expired ones included — the service decides what
1807    /// is pending. Never a secret: the plane holds only hashes.
1808    fn invites(&self) -> ControlResult<Vec<gunnar_proto::Invite>> {
1809        Err(ControlError::Unimplemented("Access.ListInvites"))
1810    }
1811    /// Withdraw an invite; the row it was, or `None`.
1812    fn revoke_invite(&self, id: &str) -> ControlResult<Option<gunnar_proto::Invite>> {
1813        let _ = id;
1814        Err(ControlError::Unimplemented("Access.RevokeInvite"))
1815    }
1816    /// **What the invite with this secret hash is now** (`Access.CheckInvite`,
1817    /// 2026-10-19): valid, used, expired or unknown. Read-only: never spends it.
1818    fn check_invite(&self, secret_hash: &str, now_unix_ms: i64) -> ControlResult<InviteCheck> {
1819        let _ = (secret_hash, now_unix_ms);
1820        Err(ControlError::Unimplemented("Access.CheckInvite"))
1821    }
1822    /// **Commit a verified web login** (`Access.WebLogin`, Stage L): see
1823    /// [`WebLoginCommit`]. `NotFound` for a credential that is gone;
1824    /// `PermissionDenied` when its counter did not increase (re-checked in
1825    /// the update); `AlreadyExists` for a session key already registered.
1826    fn web_login(&self, commit: WebLoginCommit) -> ControlResult<WebLoginOutcome> {
1827        let _ = commit;
1828        Err(ControlError::Unimplemented("Access.WebLogin"))
1829    }
1830    /// **Register a passkey from a creation ceremony** (`Access.WebRegister`,
1831    /// Stage L): see [`WebRegistration`]. An invite that is unknown, used or
1832    /// expired — or names another principal — is `PermissionDenied`, in the
1833    /// same words for all of them.
1834    fn web_register(&self, registration: WebRegistration) -> ControlResult<WebRegistered> {
1835        let _ = registration;
1836        Err(ControlError::Unimplemented("Access.WebRegister"))
1837    }
1838    /// **Commit an approved machine enrolment** (2026-10-05): see
1839    /// [`MachineApproval`]. The ONE way a web credential lands on a MACHINE
1840    /// principal — `add_credential` and `web_register` still refuse one. A
1841    /// step-up counter that did not increase is `PermissionDenied`. The
1842    /// operator's first invite is never touched: it is the owner's.
1843    fn approve_machine_enrolment(&self, approval: MachineApproval) -> ControlResult<MachineApproved> {
1844        let _ = approval;
1845        Err(ControlError::Unimplemented("Access.ApproveMachineEnrolment"))
1846    }
1847    /// The account that enrolled `principal_id` as an employee, if any.
1848    fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>> {
1849        let _ = principal_id;
1850        Err(ControlError::Unimplemented("Accounts.EnrollEmployee"))
1851    }
1852    /// **Company SSO's storage** ([`crate::sso::plane::SsoPlane`]), when this
1853    /// plane has an identity table that holds it. `None`: every `Sso` verb
1854    /// answers UNIMPLEMENTED, and `Access.WebLogin` knows of no `require_sso`.
1855    fn sso(&self) -> Option<&dyn crate::sso::plane::SsoPlane> {
1856        None
1857    }
1858    /// Remove a web credential; the row it was, or `None` if there was none.
1859    fn revoke_credential(&self, credential_id: &[u8]) -> ControlResult<Option<gunnar_proto::WebCredential>> {
1860        let _ = credential_id;
1861        Err(ControlError::Unimplemented("Access.RevokeCredential"))
1862    }
1863
1864    /// Returns (keys revoked, grants revoked).
1865    fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)>;
1866    /// Register a key on `principal_id`.
1867    ///
1868    /// `caller` is the principal on the connection (empty under allow-all), and
1869    /// `attestation` the target's signed word that this key may act as them.
1870    /// The rule for when the attestation is required is
1871    /// `gunnar_core::key_attestation::Requirement::of`, one writer both planes
1872    /// ask; the plane derives the new key's fingerprint itself and checks the
1873    /// attestation against it before anything is written.
1874    #[allow(clippy::too_many_arguments)]
1875    fn add_key(
1876        &self,
1877        principal_id: &str,
1878        openssh_public_key: &str,
1879        comment: &str,
1880        expires_at_unix_ms: Option<i64>,
1881        caller: &str,
1882        attestation: Option<SignedKeyAttestation>,
1883    ) -> ControlResult<PublicKey>;
1884    fn revoke_key(&self, fingerprint: &str) -> ControlResult<()>;
1885    fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>>;
1886    fn grant(
1887        &self,
1888        principal_id: &str,
1889        namespace: &str,
1890        repo: &str,
1891        level: GrantLevel,
1892    ) -> ControlResult<Grant>;
1893    fn revoke(&self, principal_id: &str, namespace: &str, repo: &str) -> ControlResult<()>;
1894    fn list_grants(
1895        &self,
1896        principal_id: Option<&str>,
1897        namespace: Option<&str>,
1898        repo: Option<&str>,
1899    ) -> ControlResult<Vec<Grant>>;
1900
1901    // ── groups (punkt 9) ───────────────────────────────────────────────────
1902    //
1903    // A group is a principal (`add_principal` with `PrincipalKind::Group`);
1904    // these manage its membership. A member inherits the group's grants
1905    // through `may`, so the RPC gates each of these on Admin exactly as it gates
1906    // `grant`. Default bodies return `Unimplemented` so a control plane that has
1907    // no membership store (the in-memory test double) compiles unchanged.
1908    //
1909    // Add and Remove take the acting admin's `signed` event and, like
1910    // `forge_append`, verify it against the ACL key table before applying and
1911    // append it to the signed membership log afterwards — the verification
1912    // lives behind this seam so there is one implementation of "is this
1913    // evidence" and not one per transport (one implementation). They return the log index
1914    // the event landed at, the index an inclusion proof is taken at.
1915    /// Verify the acting admin's signed event, make `member_id` a member of
1916    /// `group_id`, and append the event to the membership log.
1917    fn add_group_member(
1918        &self,
1919        group_id: &str,
1920        member_id: &str,
1921        signed: SignedMembershipEvent,
1922    ) -> ControlResult<u64> {
1923        let _ = (group_id, member_id, signed);
1924        Err(ControlError::Unimplemented("Access.AddGroupMember"))
1925    }
1926    /// Verify the signed event, remove `member_id` from `group_id`, and append
1927    /// it. Returns whether it was a member and the log index.
1928    fn remove_group_member(
1929        &self,
1930        group_id: &str,
1931        member_id: &str,
1932        signed: SignedMembershipEvent,
1933    ) -> ControlResult<(bool, u64)> {
1934        let _ = (group_id, member_id, signed);
1935        Err(ControlError::Unimplemented("Access.RemoveGroupMember"))
1936    }
1937    /// The direct members of a group.
1938    fn list_group_members(&self, group_id: &str) -> ControlResult<Vec<Principal>> {
1939        let _ = group_id;
1940        Err(ControlError::Unimplemented("Access.ListGroupMembers"))
1941    }
1942    /// The signed membership log, optionally filtered to one group, in append
1943    /// order — the evidence behind the live table.
1944    fn membership_events(
1945        &self,
1946        group_id: Option<&str>,
1947    ) -> ControlResult<Vec<SignedMembershipEvent>> {
1948        let _ = group_id;
1949        Err(ControlError::Unimplemented("Access.ListGroupMembershipEvents"))
1950    }
1951
1952    // ── policy (the pre-receive gate) ──────────────────────────────────────
1953    fn get_protection(
1954        &self,
1955        path: &RepoPath,
1956        ref_pattern: Option<&str>,
1957    ) -> ControlResult<Vec<BranchProtection>>;
1958    fn set_protection(
1959        &self,
1960        path: &RepoPath,
1961        protection: BranchProtection,
1962    ) -> ControlResult<BranchProtection>;
1963    fn delete_protection(&self, path: &RepoPath, ref_pattern: &str) -> ControlResult<()>;
1964    fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy>;
1965    fn set_ref_name_policy(
1966        &self,
1967        path: &RepoPath,
1968        policy: RefNamePolicy,
1969    ) -> ControlResult<RefNamePolicy>;
1970
1971    // ── forge: the review half (roadmap phase 1) ───────────────────────────
1972    //
1973    // The server never signs. `forge_append` takes an event the caller signed,
1974    // verifies it against the ACL key table, and appends it; the verification
1975    // lives behind this seam rather than in the service so that there is one
1976    // implementation of "is this evidence" and not one per transport (one implementation).
1977    //
1978    // There is no `next_pull` here: the number belongs to the record, and
1979    // `create_pull` assigns it. A second numbering authority is exactly the
1980    // twin this split exists to avoid.
1981    /// Verify a signed review event against the registered keys and append it.
1982    ///
1983    /// Returns the index it landed at — the index an inclusion proof is taken
1984    /// at. `InvalidArgument` for an event that does not verify, because a
1985    /// signature that does not check out is a malformed request and not a
1986    /// server fault.
1987    fn forge_append(&self, event: SignedForgeEvent) -> ControlResult<u64>;
1988    /// One pull request's review, folded, with every approval re-checked now.
1989    fn forge_review(
1990        &self,
1991        repo: &str,
1992        pull: u64,
1993    ) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)>;
1994    /// The review on every pull request in `repo` that has one.
1995    fn forge_reviews(&self, repo: &str) -> ControlResult<Vec<ReviewState>>;
1996
1997    // ── forge: the issue half ──────────────────────────────────────────────
1998    //
1999    // The SAME log and the SAME `forge_append`: an issue event is signed,
2000    // verified against the ACL key table and appended exactly as a review event
2001    // is. What differs is only the fold — an issue owns its open/closed state
2002    // where a review defers it to the pull request record — so these read the
2003    // one log through a different fold rather than opening a second store.
2004    /// One issue, folded from its events, or `None` if nothing opened that
2005    /// number. The whole thread is returned alongside — the opening, every
2006    /// comment, and the close if there is one.
2007    fn forge_issue(
2008        &self,
2009        repo: &str,
2010        number: u64,
2011    ) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>>;
2012    /// Every issue in `repo`, folded, in the order they were opened.
2013    fn forge_issues(&self, repo: &str) -> ControlResult<Vec<IssueState>>;
2014
2015    // ── admin ──────────────────────────────────────────────────────────────
2016    fn counts(&self) -> ControlResult<ControlCounts>;
2017
2018    /// **Tombstones and replication, for `Admin.Stats`.**
2019    ///
2020    /// The default is the honest empty one and NOT `Unimplemented`: unlike a
2021    /// ref list, "no tombstones and no standbys" is a real state a caller can
2022    /// act on, and a `Stats` that failed because a test double has no
2023    /// replicator would make the whole call unusable.
2024    fn stats_extras(&self) -> ControlResult<StatsExtras> {
2025        Ok(StatsExtras::default())
2026    }
2027    fn storage_info(&self, path: Option<&RepoPath>) -> ControlResult<StorageInfoResponse>;
2028
2029    /// **"Which CA does this box mint with?"** (`Admin.IssuerInfo`).
2030    ///
2031    /// A read of the PKI control document: the live issuer's certificate and
2032    /// fingerprint, and the trusted-CA list. `gunnar-server` answers it
2033    /// through [`ServerStores::pki_readonly`], whose secret store refuses
2034    /// every call — so this seam cannot mint, cannot rotate, and cannot reach
2035    /// the private key even if someone later edits the body of the one
2036    /// implementation.
2037    ///
2038    /// The default is `Unimplemented` and NOT `has_issuer: false`, for the
2039    /// reason [`Self::capacity`] gives above: a control plane with no PKI
2040    /// behind it has nothing to read, and "this box has no issuer" is a
2041    /// SPECIFIC and actionable fact — it is what a never-served data root
2042    /// looks like. A caller told that when the truth is "I cannot say" would
2043    /// go and mint one over the top of a CA their whole fleet pins.
2044    fn issuer_info(&self) -> ControlResult<gunnar_proto::IssuerInfoResponse> {
2045        Err(ControlError::Unimplemented("Admin.IssuerInfo"))
2046    }
2047
2048    /// **"Is the record of what happened here still complete?"**
2049    /// (`Admin.AuditTrail`, lane T11, 2026-09-21).
2050    ///
2051    /// The shape of the control plane's tamper-evident audit chain — entry
2052    /// count, tip hash, whether it verifies, whether it is replicated into the
2053    /// transaction log, and optionally the hash at one sequence number. Never
2054    /// an entry body: this answers completeness, not contents.
2055    ///
2056    /// The default is `Unimplemented` and NOT `present: false`, for the reason
2057    /// [`Self::issuer_info`] gives: "this box keeps no trail" is a specific and
2058    /// alarming fact, and a caller told it when the truth is "I cannot say"
2059    /// would report a box as unaudited that is not.
2060    fn audit_trail(&self, _at_seq: u64) -> ControlResult<gunnar_proto::AuditTrailResponse> {
2061        Err(ControlError::Unimplemented("Admin.AuditTrail"))
2062    }
2063
2064    /// **A generic server setting** (`Admin.GetSetting`, owner decision 2026-10-26): the bytes
2065    /// stored under `name`, `None` where nothing is. The server knows no setting name: the
2066    /// client names them (gunnar-core `settings`).
2067    fn setting(&self, _name: &str) -> ControlResult<Option<Vec<u8>>> {
2068        Err(ControlError::Unimplemented("Admin.GetSetting"))
2069    }
2070
2071    /// **Store a generic server setting** (`Admin.SetSetting`): `value` under `name`; the value
2072    /// it replaced, `None` for a new name. Bounded (gunnar-core `settings`), and a refusal names
2073    /// the bound.
2074    fn set_setting(&self, _name: &str, _value: &[u8]) -> ControlResult<Option<Vec<u8>>> {
2075        Err(ControlError::Unimplemented("Admin.SetSetting"))
2076    }
2077
2078    /// **"If a tenant's cap rose to N bytes, could you serve it?"**
2079    /// (`Admin.Capacity`) — the question a seller must ask BEFORE the money
2080    /// moves, and OPEN SOURCE, ALWAYS ON: a self-hoster wants to know whether
2081    /// their disk can grow as much as a service provider does, so this is not
2082    /// behind `metered`.
2083    ///
2084    /// Not "how much free space is there". The answer carries a verdict AND,
2085    /// independently, whether the data set can ever hold more than it does —
2086    /// because on the appliance the second answer is NO and a caller that read
2087    /// a single small number would buy a disk that is billed and invisible.
2088    /// `gunnar-server`'s `capacity` module is the ONE implementation (one implementation);
2089    /// it measures with the same `statvfs` the push door refuses under.
2090    ///
2091    /// The default is `Unimplemented`, not a plausible answer: a control plane
2092    /// with no filesystem behind it has nothing to measure, and "0 bytes free"
2093    /// from it would read as a full disk rather than as an unanswered
2094    /// question — which is exactly the blind guard to avoid.
2095    fn capacity(
2096        &self,
2097        _request: &gunnar_proto::CapacityRequest,
2098    ) -> ControlResult<gunnar_proto::CapacityResponse> {
2099        Err(ControlError::Unimplemented("Admin.Capacity"))
2100    }
2101
2102    // ── The runtime data-set growth (`Admin.Grow*`, `Bootstrap.GrowStatus`) ──
2103    //
2104    // `DATA-SET-GROWTH-FLOW.md` §3, ruling 1. `gunnar-server`'s `grow` module
2105    // is the ONE implementation; a control plane without it answers an honest
2106    // `idle` with `engine_present: false` on status — never `Unimplemented`,
2107    // because the credential-free status is what an outside actor reads to
2108    // decide about the volumes, and "I cannot say" must not look like "ask
2109    // again later" — and refuses the two mutators by name.
2110
2111    /// Begin a runtime growth: drain, unmount, wait, grow, mount, reopen.
2112    fn grow_start(
2113        &self,
2114        _request: &gunnar_proto::GrowStartRequest,
2115    ) -> ControlResult<gunnar_proto::GrowStatusResponse> {
2116        Err(ControlError::Unimplemented(
2117            "Admin.GrowStart: this control plane has no runtime grow engine (gunnar-server built without `grow-runtime`)",
2118        ))
2119    }
2120
2121    /// The growth's phase, members and journal. Answered with and without a
2122    /// credential (`Admin.GrowStatus`, `Bootstrap.GrowStatus`).
2123    fn grow_status(&self) -> ControlResult<gunnar_proto::GrowStatusResponse> {
2124        Ok(gunnar_proto::GrowStatusResponse {
2125            phase: "idle".to_owned(),
2126            engine_present: false,
2127            serving: true,
2128            server_unix_ms: crate::bus::now_unix_ms(),
2129            detail: "this control plane has no runtime grow engine; the data set grows only at a restart, if at all".to_owned(),
2130            ..Default::default()
2131        })
2132    }
2133
2134    /// The outside actor says "look again" (or the operator gives up).
2135    fn grow_resume(&self, _give_up: bool) -> ControlResult<gunnar_proto::GrowStatusResponse> {
2136        Err(ControlError::Unimplemented(
2137            "Admin.GrowResume: this control plane has no runtime grow engine (gunnar-server built without `grow-runtime`)",
2138        ))
2139    }
2140
2141    /// **Per-namespace meters** (`Usage.Read` / `Usage.ReadAll`), the numbers a
2142    /// bill is made of — and OPEN SOURCE, ALWAYS ON: a self-hoster wants them
2143    /// as much as a service provider does, so this is not behind `metered`.
2144    ///
2145    /// `Some(ns)` answers one namespace and is `NotFound` for one the catalog
2146    /// does not hold; `None` answers every namespace, including ones with no
2147    /// repositories (`repos = 0`, everything else 0) — a tenant with nothing
2148    /// is a real row, not an absent one. What each meter counts, file by
2149    /// file, is written in `proto/gunnar/v1/usage.proto`; the ONE
2150    /// implementation is `gunnar-server`'s (one implementation), which measures the same
2151    /// directories `Admin.StorageInfo` walks, attributed per file.
2152    ///
2153    /// The default is `Unimplemented`, not an empty answer: a control plane
2154    /// with no store behind it has no bytes to count, and "0 bytes" from it
2155    /// would read as a free tenant rather than as an unanswered question.
2156    fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<gunnar_proto::NamespaceUsage>> {
2157        let _ = namespace;
2158        Err(ControlError::Unimplemented("Usage"))
2159    }
2160
2161    /// **One namespace's repositories, each with what it stores**
2162    /// (`Usage.Read`'s `repos`, 2026-10-04) — measured by the same walk that
2163    /// sums [`ControlPlane::usage`], so the rows' `stored_bytes` add up to the
2164    /// namespace's `pack_bytes`. Sorted by repository name. `NotFound` for a
2165    /// namespace the catalog does not hold; the default is `Unimplemented`, for
2166    /// [`ControlPlane::usage`]'s reason.
2167    fn repo_usage(&self, namespace: &str) -> ControlResult<Vec<gunnar_proto::RepoStored>> {
2168        let _ = namespace;
2169        Err(ControlError::Unimplemented("Usage"))
2170    }
2171
2172    /// **One page of every namespace's meters** (`UsageBatch.ReadMany`,
2173    /// 2026-10-06), in namespace order, at most `page_size` rows, resuming
2174    /// after `page_token` ([`usage_page_token`]); with a `changed_since` cursor
2175    /// it may answer only the namespaces whose row changed.
2176    ///
2177    /// The default is CORRECT and not cheap: it pages over
2178    /// [`ControlPlane::usage`]`(None)` and never honours `changed_since`
2179    /// (`changed_only = false`, no cursor) — every plane that can read usage can
2180    /// page it. `gunnar-server` overrides it with its usage ledger, which is the
2181    /// whole point of the call.
2182    fn usage_page(
2183        &self,
2184        page_token: &str,
2185        page_size: usize,
2186        changed_since: &str,
2187    ) -> ControlResult<gunnar_proto::ReadManyUsageResponse> {
2188        let _ = changed_since;
2189        let (_, after) = usage_page_token::decode(page_token);
2190        let mut rows = self.usage(None)?;
2191        rows.sort_by(|a, b| a.namespace.cmp(&b.namespace));
2192        Ok(usage_page_token::page(rows, &after, page_size, String::new(), false))
2193    }
2194
2195    /// **The FLOW series** (`Usage.Activity`): one row per UTC day this
2196    /// namespace moved anything, oldest first — and OPEN SOURCE, ALWAYS ON,
2197    /// for [`ControlPlane::usage`]'s reason.
2198    ///
2199    /// The window is inclusive at both ends and resolved to whole UTC days;
2200    /// `0` for `since_unix_ms` means the whole kept window, `0` for
2201    /// `until_unix_ms` means now. A day with no traffic is ABSENT rather than
2202    /// returned as a row of zeroes: the caller knows which days it asked for,
2203    /// and a day that was never written and a day on which nothing happened
2204    /// are the same fact.
2205    ///
2206    /// The second half of the answer is how many days this server keeps, which
2207    /// is what lets a caller tell an empty series that means *nothing
2208    /// happened* from one that means *you asked about a time this box no
2209    /// longer remembers*.
2210    ///
2211    /// The default is `Unimplemented`, not an empty series — for exactly the
2212    /// reason [`ControlPlane::usage`]'s is. A control plane with no flow ledger
2213    /// behind it counted nothing, and "no days" from it would read as a silent
2214    /// tenant rather than as an unanswered question. That distinction is the
2215    /// whole point of this call.
2216    fn activity(
2217        &self,
2218        namespace: &str,
2219        since_unix_ms: i64,
2220        until_unix_ms: i64,
2221    ) -> ControlResult<(Vec<gunnar_proto::DayActivity>, i64)> {
2222        let _ = (namespace, since_unix_ms, until_unix_ms);
2223        Err(ControlError::Unimplemented("Usage.Activity"))
2224    }
2225
2226    /// **The admission plan for a namespace** (`Usage.AdmitPlan`): its usage
2227    /// over the rolling window, the budget, and per kind of work the class
2228    /// the gate would give the next transfer, the learnt estimate and the
2229    /// plan's sentence. Open source and always on — a build without
2230    /// `admit-share` answers `pass-through`, which is the truth about it.
2231    ///
2232    /// The default is `Unimplemented`, for [`ControlPlane::usage`]'s reason: a
2233    /// control plane with no admission gate behind it has no plan to report.
2234    fn admit_plan(&self, namespace: &str) -> ControlResult<gunnar_proto::AdmitPlanResponse> {
2235        let _ = namespace;
2236        Err(ControlError::Unimplemented("Usage.AdmitPlan"))
2237    }
2238
2239    // ── the METERED appliance: `Entitlement` ────────────────────────────────
2240    //
2241    // Compiled only with the `metered` feature, and then the defaults are a
2242    // refusal BY NAME — the in-memory test double has no monetize key to
2243    // verify a fact against and no control document to keep it in. Off, these
2244    // methods do not exist: a `ControlPlane` with no `entitlement_set` is the
2245    // whole point (the metered build split), not a stub that returns
2246    // `Unimplemented` from a binary that still carries the code.
2247
2248    /// **The fact being ENFORCED for one namespace**, or `None` = Free.
2249    ///
2250    /// # The contract, and the one implementation that does not keep it
2251    ///
2252    /// "Enforced", not "stored". Two things read a namespace's plan on this
2253    /// box and they read different places:
2254    ///
2255    /// * `gunnar_ssh::PlanGate` — the push door, `Entitlement.Set`'s whole
2256    ///   purpose — asks the CACHE (`EntitlementCache::allows`), which holds
2257    ///   only facts that verified under the configured monetize key;
2258    /// * this method, on `gunnar-server`, read the STORE until 2026-09-21
2259    ///   (`EntitlementSlot::get` → `EntitlementStore::get`), which holds
2260    ///   whatever was written, unverified on the way out. **It now reads the
2261    ///   cache** — the fix described below is made, and
2262    ///   `gunnar-server/tests/entitlement.rs`
2263    ///   `a_box_restored_under_the_wrong_key_reports_no_plan_it_does_not_enforce`
2264    ///   pins it. The rest of this note is the reason, kept.
2265    ///
2266    /// They agreed on every path except one, and it is a path that runs on
2267    /// every restart. `EntitlementSlot::open` loads each stored fact and pushes
2268    /// it into the cache **through `monetize_embed::signing::verify_fact`**; a
2269    /// fact that no longer verifies — the monetize key was rotated, the control
2270    /// document was edited — is logged (`entitlement.boot.refused`) and left
2271    /// out, and that namespace is Free for everything the cache answers.
2272    ///
2273    /// The store still has it. So `Usage.Read` reports its caps as the
2274    /// namespace's `Allowance`, `Entitlement.Get` returns it, and
2275    /// `Entitlement.List` lists it, while NOTHING on the box enforces any of
2276    /// it. That is precisely the defect `Allowance.enforcement` was added to
2277    /// close, coming back through a different door and worse: there the tenant
2278    /// was shown a measured number labelled as a limit, here they are shown a
2279    /// `pack_bytes` cap labelled `ENFORCED` — by
2280    /// [`crate::service::meters::METERS`], correctly, since that table
2281    /// describes the CODE — with no fact in force behind it.
2282    ///
2283    /// **A rejected fact must not be reported as an allowance.** The fix is in
2284    /// the implementation and not here: `EntitlementSlot::get` must answer out
2285    /// of the same snapshot `PlanGate` reads, so that a fact refused at boot is
2286    /// `None` to both. Reporting a plan the box will not act on is a promise
2287    /// nobody keeps, and it is silent — the only trace today is one
2288    /// `tracing::error!` at startup.
2289    ///
2290    /// Refusing to serve `Usage.Read` at all in that state would be the wrong
2291    /// repair: the METERS are still true, and a tenant whose signature broke
2292    /// still deserves to see what they hold.
2293    #[cfg(feature = "metered")]
2294    fn entitlement_get(
2295        &self,
2296        namespace: &str,
2297    ) -> ControlResult<Option<gunnar_proto::EntitlementFact>> {
2298        let _ = namespace;
2299        Err(ControlError::Unimplemented("Entitlement.Get"))
2300    }
2301
2302    /// Every fact, one per namespace.
2303    #[cfg(feature = "metered")]
2304    fn entitlement_list(&self) -> ControlResult<Vec<gunnar_proto::EntitlementFact>> {
2305        Err(ControlError::Unimplemented("Entitlement.List"))
2306    }
2307
2308    /// The stored facts this box refused when it booted (`Entitlement.List`'s
2309    /// `refused_at_boot`). Default `None`: nothing refused, or nothing held.
2310    #[cfg(feature = "metered")]
2311    fn entitlement_refused_at_boot(&self) -> ControlResult<Option<gunnar_proto::RefusedAtBoot>> {
2312        Ok(None)
2313    }
2314
2315    /// **Verify, log, store, swap.** Returns the state now enforced and the
2316    /// sequence number (line index) of the attestation line the accepted
2317    /// fact was appended as. A fact that does not verify is
2318    /// [`ControlError::PermissionDenied`] naming why, and NOTHING moves — not
2319    /// the snapshot, not the document, not the log.
2320    #[cfg(feature = "metered")]
2321    fn entitlement_set(
2322        &self,
2323        fact: gunnar_proto::EntitlementFact,
2324    ) -> ControlResult<(gunnar_proto::EntitlementState, u64)> {
2325        let _ = fact;
2326        Err(ControlError::Unimplemented("Entitlement.Set"))
2327    }
2328
2329    // ── the METERED appliance: `Billing` ────────────────────────────────────
2330    //
2331    // Two methods and a deliberate shape: this side of the seam holds the
2332    // KEY, and the other side holds the ticket FORMAT.
2333    //
2334    // `crate::service::billing` builds the ticket, asks here for a signature
2335    // over bytes, and pastes the result in. It is never handed key material.
2336    // The alternative — a seam method that returns the private key — would put
2337    // a signing key into a crate whose job is to parse requests, and would add
2338    // a second place on this box that can produce the appliance's signature
2339    // over anything at all. The alternative in the other direction — a seam
2340    // method that mints the whole ticket — would move the format into
2341    // `gunnar-server` and leave the grant check and the thing it guards in two
2342    // different crates.
2343
2344    /// **The public half of the actor-ticket key, 64 lowercase hex characters**
2345    /// (`Billing.ApplianceIdentity`).
2346    ///
2347    /// The default is a refusal BY NAME rather than a fabricated key: a control
2348    /// plane that cannot reach a secret store has no key, and answering with
2349    /// something that looks like one would be advertising a key that verifies
2350    /// nothing this box will ever sign.
2351    #[cfg(feature = "metered")]
2352    fn actor_ticket_public_key_hex(&self) -> ControlResult<String> {
2353        Err(ControlError::Unimplemented("Billing.ApplianceIdentity"))
2354    }
2355
2356    /// **Sign `message` with the actor-ticket key** — raw Ed25519, 64 bytes, no
2357    /// envelope.
2358    ///
2359    /// `message` is `monetize_embed::ticket::ticket_message` of an unsigned
2360    /// ticket, and this method has no opinion about that: it takes bytes,
2361    /// because the half that holds the key is not the half that knows the
2362    /// format, and an opinion here would be a second place the format is
2363    /// written down.
2364    ///
2365    /// **Every call to this is a ticket that exists.** The caller must have
2366    /// passed its grant check before it gets here — `service::billing` does,
2367    /// and `tests/billing.rs` counts the calls to prove a refused request makes
2368    /// none.
2369    #[cfg(feature = "metered")]
2370    fn actor_ticket_sign(&self, message: &[u8]) -> ControlResult<[u8; 64]> {
2371        let _ = message;
2372        Err(ControlError::Unimplemented("Billing.MintActorTicket"))
2373    }
2374
2375    /// **The named sealed slot `name`** (2026-10-09): its owner's principal id
2376    /// and its value, or `None` when there is no such slot. The ACL is the
2377    /// service's (`service::sealed`); the plane stores and reads.
2378    #[cfg(feature = "metered")]
2379    fn sealed_get(&self, name: &str) -> ControlResult<Option<(String, Vec<u8>)>> {
2380        let _ = name;
2381        Err(ControlError::Unimplemented("Sealed.Get"))
2382    }
2383
2384    /// **Store `value` in the slot `name`, owned by `owner`** (a principal id):
2385    /// sealed under the KEK, one `Secret` frame. Answers whether it was created.
2386    #[cfg(feature = "metered")]
2387    fn sealed_put(&self, name: &str, owner: &str, value: &[u8]) -> ControlResult<bool> {
2388        let _ = (name, owner, value);
2389        Err(ControlError::Unimplemented("Sealed.Put"))
2390    }
2391
2392    /// **Walk the provenance chain** (`Admin.VerifyChain`, roadmap phase 2).
2393    ///
2394    /// The walk itself lives in `gunnar-server`'s `verify` module — the one
2395    /// implementation the CLI verb already uses (one implementation) — so the default here
2396    /// is `Unimplemented`, not an empty chain: a control plane that cannot
2397    /// walk the chain must refuse, because an empty chain with no BROKEN link
2398    /// reads as merely incomplete, and "the backend cannot verify anything"
2399    /// must never be dressed up as "nothing was disproven".
2400    fn verify_chain(
2401        &self,
2402        request: &gunnar_proto::VerifyChainRequest,
2403    ) -> ControlResult<gunnar_proto::VerifyChainResponse> {
2404        let _ = request;
2405        Err(ControlError::Unimplemented("Admin.VerifyChain"))
2406    }
2407
2408    /// Index work in flight. Default: none — an engine without a background
2409    /// indexer has nothing to report, and that is not an error.
2410    fn index_progress(&self) -> ControlResult<Vec<IndexProgressRow>> {
2411        Ok(Vec::new())
2412    }
2413
2414    // ── mirror (mirroring) ─────────────────────────────────────────────────
2415    fn mirror_add(
2416        &self,
2417        path: &RepoPath,
2418        remote_url: &str,
2419        prune: bool,
2420        create_repo: bool,
2421    ) -> ControlResult<MirrorConfig>;
2422    /// **Enqueue a sync, and return without syncing.**
2423    ///
2424    /// Returns the registration with [`MirrorConfig::job`] set to the job now
2425    /// responsible for the repository — a new one, or the one that was already
2426    /// queued or running for it. It does NOT return what moved: a fetch is
2427    /// minutes of somebody else's network and a unary RPC that waited for one
2428    /// would time out. Poll `mirror_status` until the job is DONE.
2429    ///
2430    /// It used to return `(MirrorConfig, Vec<RefUpdate>)` and to be
2431    /// `Unimplemented` in every implementation, so nothing ever read that pair.
2432    fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig>;
2433    /// **Store, replace or forget the credential this mirror dials with.**
2434    ///
2435    /// `secret` is the value and is **write-only**: nothing returns it, and the
2436    /// `MirrorConfig` that comes back says only which kind is set. A `kind` of
2437    /// `Unspecified` forgets it, and is the only spelling of that; see
2438    /// `mirror.proto` for why this is a verb of its own rather than two more
2439    /// fields on `mirror_add`.
2440    fn mirror_set_credential(
2441        &self,
2442        path: &RepoPath,
2443        kind: MirrorCredentialKind,
2444        secret: &str,
2445        username: &str,
2446    ) -> ControlResult<MirrorConfig>;
2447    fn mirror_status(
2448        &self,
2449        namespace: Option<&str>,
2450        path: Option<&RepoPath>,
2451    ) -> ControlResult<Vec<MirrorConfig>>;
2452    fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()>;
2453
2454    // ── replication: the OPERATOR half ─────────────────────────────────────
2455    //
2456    // Default bodies, and the default is a REFUSAL BY NAME. Two callers cannot
2457    // do this: a build without `gunnar-server`'s `replication` feature, and the
2458    // in-memory test double, which has no replicator to reconfigure. Both must
2459    // say so out loud — an empty `Ok` here would be a control plane that
2460    // accepts a standby and silently keeps none, which is the exact shape of
2461    // the defect replication's whole file is written against.
2462    //
2463    // They exist at all because the appliance has NO SHELL. `replication.json`
2464    // is the truth, and until these three verbs there was no way to write it on
2465    // an OS-less image that runs as PID 1.
2466
2467    /// Add or replace one standby by name, persist it, and start replicating.
2468    fn replication_set(
2469        &self,
2470        _target: ReplicationTarget,
2471    ) -> ControlResult<ReplicationStatusResponse> {
2472        Err(ControlError::Unimplemented(
2473            "this server was built without replication (`--features replication`), so it              cannot be given a standby",
2474        ))
2475    }
2476    /// Drop one standby by name, persist that, and stop replicating to it.
2477    fn replication_remove(&self, _name: &str) -> ControlResult<ReplicationStatusResponse> {
2478        Err(ControlError::Unimplemented(
2479            "this server was built without replication (`--features replication`), so it              has no standby to remove",
2480        ))
2481    }
2482    /// Drop EVERY standby at once — the failover demote. Persist the empty
2483    /// roster and stop replicating anywhere.
2484    fn replication_demote(&self) -> ControlResult<ReplicationStatusResponse> {
2485        Err(ControlError::Unimplemented(
2486            "this server was built without replication (`--features replication`), so it              has no standby to demote",
2487        ))
2488    }
2489    /// **Set this box's role**: `STANDBY` freezes client writes (a read-only
2490    /// standby); `PRIMARY` is a writable primary — the takeover flip; `BROWSE`
2491    /// is a frozen standby that serves anonymous reads and can never be
2492    /// promoted (the browse replica) — an implementation refuses
2493    /// `PRIMARY` on a browse box with `FailedPrecondition`. Persist it and
2494    /// flip the shared write-gate the front doors read.
2495    fn replication_set_role(
2496        &self,
2497        _role: gunnar_proto::ReplicationRole,
2498    ) -> ControlResult<ReplicationStatusResponse> {
2499        Err(ControlError::Unimplemented(
2500            "this server was built without replication (`--features replication`), so it              has no role to set",
2501        ))
2502    }
2503    /// **Abdicate**: freeze client writes, flush pending to the standbys, then
2504    /// demote — the master steps down cleanly, in that order.
2505    fn replication_abdicate(&self) -> ControlResult<ReplicationStatusResponse> {
2506        Err(ControlError::Unimplemented(
2507            "this server was built without replication (`--features replication`), so it              cannot abdicate",
2508        ))
2509    }
2510    /// What is configured, and what each running standby has done.
2511    ///
2512    /// `include_refs` asks for the expensive half — the count of refs a mirror
2513    /// does not carry, which opens every repository's reference database. False
2514    /// (a plain `replication list`) answers the cheap operational signal and
2515    /// opens no store; true (`--refs`) takes the walk.
2516    fn replication_status(
2517        &self,
2518        _include_refs: bool,
2519    ) -> ControlResult<ReplicationStatusResponse> {
2520        Err(ControlError::Unimplemented(
2521            "replication: this server was built without the `replication` feature",
2522        ))
2523    }
2524    /// **Backfill**: queue every repository one standby covers, or one named
2525    /// repository, so a standby added AFTER the repositories were written gets
2526    /// them. Adding a standby enqueues nothing.
2527    fn replication_resync(
2528        &self,
2529        _name: &str,
2530        _repo: Option<&RepoPath>,
2531        _except: &[RepoPath],
2532    ) -> ControlResult<ResyncReplicationResponse> {
2533        Err(ControlError::Unimplemented(
2534            "replication: this server was built without the `replication` feature, so it              has no standby to backfill",
2535        ))
2536    }
2537
2538    /// **The replication watermark** (`Mirror.FlushReplication`,
2539    /// `DATA-SET-GROWTH-FLOW.md` G2/T11): push everything pending to every
2540    /// standby, wait — bounded — and report the tuple that proves (or does
2541    /// not) that the twin is caught up. Changes no role and no roster.
2542    fn replication_flush(
2543        &self,
2544        _request: &gunnar_proto::FlushReplicationRequest,
2545    ) -> ControlResult<gunnar_proto::FlushReplicationResponse> {
2546        Err(ControlError::Unimplemented(
2547            "replication: this server was built without the `replication` feature, so it has no standby to flush to",
2548        ))
2549    }
2550
2551    // ── The transaction log: what the twin fetches ──────────────────────────
2552
2553    /// The log's head — `head_seq`, `first_seq`, bytes, and on a twin what it
2554    /// has applied. Default `Unimplemented` for a plane with no log.
2555    fn log_head(&self) -> ControlResult<gunnar_proto::LogHeadResponse> {
2556        Err(ControlError::Unimplemented(
2557            "transaction log: this control plane keeps none",
2558        ))
2559    }
2560
2561    /// Entries `from..=to`, whole, at most `max_bytes` of blob in total but
2562    /// never fewer than one entry. `from` below the oldest retained sequence
2563    /// is `FailedPrecondition` naming it.
2564    fn log_fetch(
2565        &self,
2566        _from: u64,
2567        _to: u64,
2568        _max_bytes: u64,
2569    ) -> ControlResult<Vec<gunnar_proto::LogEntry>> {
2570        Err(ControlError::Unimplemented(
2571            "transaction log: this control plane keeps none",
2572        ))
2573    }
2574
2575    /// **[`ControlPlane::log_fetch`], streamed** (twin-streaming, 2026-10-06): the same
2576    /// refusals, answered NOW — before the call's first piece, so `Mirror.LogFetch` keeps its
2577    /// status codes (`FailedPrecondition` for a range retention dropped, and the rest) — then a
2578    /// producer that hands the range to its sink as `LogChunk` pieces of at most
2579    /// [`LOG_PIECE_BYTES`], `meta` on an entry's first, `last` on its last. The service runs it
2580    /// on a blocking thread behind a few pieces of window, so the sink's backpressure paces it.
2581    ///
2582    /// The default gathers through `log_fetch` and cuts each entry into pieces, which is right
2583    /// for a plane that holds its entries in memory anyway. A plane that keeps a real log
2584    /// overrides it and reads each entry off the disk a piece at a time: a 512 MiB pack must not
2585    /// cost the primary 512 MiB (it cost twice that before: the entry, then its pieces).
2586    fn log_fetch_stream(&self, from: u64, to: u64, max_bytes: u64) -> ControlResult<LogFetchProducer> {
2587        let entries = self.log_fetch(from, to, max_bytes)?;
2588        Ok(Box::new(move |sink| {
2589            for e in entries {
2590                log_entry_pieces(e, sink)?;
2591            }
2592            Ok(())
2593        }))
2594    }
2595
2596    /// **[`ControlPlane::log_fetch_stream`] with the FIRST entry started at byte `from_offset`
2597    /// of its blob** (`LogFetchRequest.from_offset`, restore-large-entry 2026-10-07): a restore
2598    /// resuming an entry the box being filled holds part of. Every piece says its `offset`.
2599    /// 0 is [`ControlPlane::log_fetch_stream`] exactly. The default gathers through
2600    /// `log_fetch` and cuts the first entry from the offset; a plane with a real log overrides it,
2601    /// reads the blob from the offset only and stamps the stored crc on the last piece for the
2602    /// receiver to check.
2603    fn log_fetch_stream_at(&self, from: u64, from_offset: u64, to: u64, max_bytes: u64) -> ControlResult<LogFetchProducer> {
2604        if from_offset == 0 {
2605            return self.log_fetch_stream(from, to, max_bytes);
2606        }
2607        let entries = self.log_fetch(from, to, max_bytes)?;
2608        Ok(Box::new(move |sink| {
2609            for e in entries {
2610                let at = if e.seq == from { from_offset } else { 0 };
2611                log_entry_pieces_from(e, at, sink)?;
2612            }
2613            Ok(())
2614        }))
2615    }
2616
2617    /// **On a twin: its primary's heartbeat** (when the primary last answered),
2618    /// with the silence bound `silent_after_ms`. Default `None`: not a twin.
2619    fn primary_heartbeat(&self, _silent_after_ms: u64) -> Option<gunnar_proto::PrimaryHeartbeat> {
2620        None
2621    }
2622
2623    /// **What `Admin.Topology` needs from the plane** (2026-10-04): the
2624    /// pairing feed's peers, which principals run on the front, the plugin
2625    /// registration, this box's own address and data volume. Default: a box
2626    /// that knows only itself — which is the truthful answer for a plane with
2627    /// no pairing (a solo gunnar, a test).
2628    fn topology_facts(&self) -> crate::topology::Facts {
2629        crate::topology::Facts::default()
2630    }
2631
2632    /// **Is this box a twin right now?** Default `false`. Read by the browse refusal: a twin is
2633    /// frozen like a browse replica, yet it must take its OWN data-set growth exactly as main
2634    /// does (owner ruling 2026-09-22, T21), so the grow verbs pass on a twin and on no browse box.
2635    fn is_twin(&self) -> bool {
2636        false
2637    }
2638
2639    /// **Does this box's transaction log FOLLOW another's** (a twin, or a main
2640    /// being restored)? It originates no transaction until it opens as the
2641    /// primary. Default `false` (wiring tracer B 2026-10-08: `Entitlement.Set`
2642    /// answers such a box's "try again" before it asks whether a namespace
2643    /// exists — a namespace made after the twin's snapshot is not replayed yet).
2644    fn log_follows(&self) -> bool {
2645        false
2646    }
2647
2648    /// **States this box is in that an operator must act on and no console reaches**
2649    /// (`Admin.Health.alarms`). Default: none.
2650    fn health_alarms(&self) -> Vec<gunnar_proto::HealthAlarm> {
2651        Vec::new()
2652    }
2653
2654    /// **Why this box reports `maintenance`** (`Admin.Health.status`), one reason each; empty is
2655    /// `serving`. It still serves either way. Default: none.
2656    fn maintenance_reasons(&self) -> Vec<String> {
2657        Vec::new()
2658    }
2659
2660    /// Is `principal_id` a member of `group`? The twin's log gate asks it
2661    /// (`Mirror.LogHead` / `LogFetch` for a member of group `twin`). Default
2662    /// `false`: a plane with no groups has no twin members.
2663    fn is_group_member(&self, _principal_id: &str, _group: &str) -> ControlResult<bool> {
2664        Ok(false)
2665    }
2666
2667    /// `Bootstrap.BecomeTwin`: a fresh box becomes a twin of `primary`;
2668    /// answers the twin's public key line.
2669    fn become_twin(&self, _primary: &str) -> ControlResult<String> {
2670        Err(ControlError::Unimplemented("twin: this control plane cannot become a twin"))
2671    }
2672
2673    /// `Bootstrap.AssignTunnelAddress` (2026-09-21, T17): `address` empty =
2674    /// answer only; otherwise record it as this box's WireGuard tunnel address
2675    /// and raise `wg0`. Default: a plane with no tunnel has no address to take.
2676    fn assign_tunnel_address(&self, _address: &str) -> ControlResult<gunnar_proto::AssignTunnelAddressResponse> {
2677        Err(ControlError::Unimplemented("tunnel: this control plane has no WireGuard tunnel (built without wg-client)"))
2678    }
2679
2680    /// `Admin.SetTunnelAddress` (2026-09-21, T17): the SAME answer as
2681    /// [`ControlPlane::assign_tunnel_address`] for a box past its bootstrap
2682    /// window. The caller has already been held to server-wide Admin by the
2683    /// service; the plane does not re-check the window.
2684    fn set_tunnel_address(&self, _address: &str) -> ControlResult<gunnar_proto::AssignTunnelAddressResponse> {
2685        Err(ControlError::Unimplemented("tunnel: this control plane has no WireGuard tunnel (built without wg-client)"))
2686    }
2687
2688    /// `Bootstrap.TwinCsr`: this twin's replication certificate request,
2689    /// minting its TLS key on the box on first call (lane T9).
2690    fn twin_csr(&self) -> ControlResult<String> {
2691        Err(ControlError::Unimplemented("Bootstrap.TwinCsr"))
2692    }
2693
2694    /// `Bootstrap.InstallTwinCertificate`: take the primary-signed certificate
2695    /// and pin its issuer; answers whole days left.
2696    fn install_twin_certificate(&self, _cert_pem: &str, _ca_pem: &str, _replication_port: u16, _replication_host: &str) -> ControlResult<i64> {
2697        Err(ControlError::Unimplemented("Bootstrap.InstallTwinCertificate"))
2698    }
2699
2700    /// `Admin.IssueTwinCertificate`, on the primary: `(cert, issuer, port, host)`
2701    /// — `host` is the address the listener is bound on when that is not the
2702    /// joined one (its tunnel address), empty otherwise.
2703    fn issue_twin_certificate(&self, _principal: &str, _csr_pem: &str) -> ControlResult<(String, String, u16, String)> {
2704        Err(ControlError::Unimplemented("Admin.IssueTwinCertificate"))
2705    }
2706
2707    /// [`ControlPlane::log_head`], **signed over the caller's nonce** — what a
2708    /// twin's poll actually calls (2026-09-21, lane T4). The default ignores the
2709    /// nonce and answers unsigned, which is the honest answer for a plane with
2710    /// no key of its own; a twin that has pinned a key refuses it.
2711    fn log_head_signed(&self, _nonce: &str) -> ControlResult<gunnar_proto::LogHeadResponse> {
2712        self.log_head()
2713    }
2714
2715    /// **A twin's proof that a NEWER generation of this box's log exists**
2716    /// (`LogHeadRequest.generation_proof`, stinky row 18, the main's half).
2717    /// Called before the answer is made. A box that finds the proof genuine — its
2718    /// own log, its own host key, a generation above its own — fences itself and
2719    /// originates nothing from then on; anything else is ignored. The default
2720    /// keeps no log and has nothing to fence.
2721    fn fence_by_generation(&self, _proof: &gunnar_proto::GenerationProof) -> ControlResult<()> {
2722        Ok(())
2723    }
2724
2725    /// **Is this box's git listener still bound where a primary's was?**
2726    ///
2727    /// A separate question from `become_twin`'s answer because it stays true
2728    /// until the box restarts, and can be asked again afterwards. The bind is
2729    /// chosen once at start-up; a box conferred the twin role over the wire is
2730    /// past that point. Nothing is SERVED through that socket — the front door
2731    /// refuses every git service on a twin from the instant the role lands —
2732    /// but it is still bound on an address a twin's socket does not belong on.
2733    ///
2734    /// `false` for a plane that has no git listener to be wrong about.
2735    fn twin_git_listener_needs_restart(&self) -> bool {
2736        false
2737    }
2738
2739    /// `Bootstrap.Restore`, start: the next sequence this box takes. `owner` is
2740    /// who the stream's credential proved the caller to be
2741    /// ([`crate::auth::authenticate_restore`]): a box already restoring resumes
2742    /// only for the owner that started it, and refuses anybody else
2743    /// `PERMISSION_DENIED` (h-v1-6a, 2026-09-28).
2744    fn restore_begin(&self, _owner: &str) -> ControlResult<u64> {
2745        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2746    }
2747
2748    /// **The keys this box takes a `Bootstrap.Restore` from** (h-v1-6a,
2749    /// 2026-09-28), as OpenSSH public key lines. A fresh box has no identity
2750    /// table to check a credential against, so these are the keys it was FED
2751    /// before anybody could enrol: what the installer's door handed it, and
2752    /// what the medium named. Default: none — a plane that was fed nothing
2753    /// refuses every restore by name, which is the fail-closed answer.
2754    fn restore_keys(&self) -> ControlResult<Vec<String>> {
2755        Ok(Vec::new())
2756    }
2757
2758    /// `Bootstrap.Restore`, the room check: has this box's data set space for a stream of
2759    /// `announced_bytes` of log? Asked ONCE, on the first chunk, before anything of the restore
2760    /// is written — the restore-onto-different-hardware case. `0` = the sender did not say, and
2761    /// no guess is made. A default `Ok(())`: a plane with no data set of its own cannot run out.
2762    fn restore_room(&self, _announced_bytes: u64) -> ControlResult<()> {
2763        Ok(())
2764    }
2765
2766    /// **`Bootstrap.Restore`, the stream's intake** (restore-large-entry, 2026-10-07): what every
2767    /// chunk of one stream is fed into. Starting one is [`ControlPlane::restore_begin`] — the
2768    /// owner check, the next sequence — and, on a plane that stages, holding the box's ONE intake
2769    /// until it is dropped. `Ok(None)`: another stream's intake still holds the box (a stream the
2770    /// sender abandoned whose apply has not returned yet); the caller waits a little and asks
2771    /// again rather than racing it over the same staged entry.
2772    ///
2773    /// The default gathers every entry whole in memory and applies it with
2774    /// [`ControlPlane::restore_apply`]: right for a plane with no disk of its own.
2775    fn restore_intake<'a>(&'a self, owner: &str) -> ControlResult<Option<Box<dyn RestoreIntake + 'a>>> {
2776        let next = self.restore_begin(owner)?;
2777        Ok(Some(Box::new(GatherIntake { plane: self, next, open: None, want_len: 0, applied: 0, lost: 0 })))
2778    }
2779
2780    /// `Bootstrap.Restore`, one whole entry: `true` when a replayed CAS lost.
2781    fn restore_apply(&self, _entry: gunnar_proto::LogEntry) -> ControlResult<bool> {
2782        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2783    }
2784
2785    /// `Bootstrap.Restore`, end: the head reached. `announced_head` is the largest
2786    /// `LogChunk.restore_head` the stream carried (0 = none): a box whose head is below it
2787    /// refuses to finish and stays restoring.
2788    fn restore_finish(&self, _announced_head: u64, _log_id_of_source: &str) -> ControlResult<u64> {
2789        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2790    }
2791
2792    /// `Mirror.SnapshotFetch`: a consistent snapshot streamed into `sink` as `LogChunk` pieces —
2793    /// items with `seq` 0, at most 1 MiB of blob a piece, `blob_len` the item's total on its
2794    /// last piece and `u64::MAX` before it, the `SnapshotAt` terminator last. Never gathered:
2795    /// the sink's backpressure paces the producer. Answers the sequence it was taken at.
2796    fn snapshot_stream(&self, _sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<u64> {
2797        Err(ControlError::Unimplemented("snapshot: this control plane keeps no log"))
2798    }
2799
2800    /// **[`ControlPlane::snapshot_stream`] from item `from_item`, byte `from_offset` on**
2801    /// (`SnapshotFetchRequest`, restore-after-snapshot 2026-10-07): a twin's kept base, resumed
2802    /// for a box being restored across capped calls; every piece names its item, offset and base
2803    /// (`LogChunk.snapshot_item`, `offset`, `snapshot_id`). `expect_id`: the base the asker holds
2804    /// part of — another one now is refused `[snapshot-changed]`. The default resumes nothing:
2805    /// asked from its start it is `snapshot_stream`, otherwise refused by name.
2806    fn snapshot_stream_from(&self, from_item: u64, from_offset: u64, expect_id: &str, sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<u64> {
2807        if from_item == 0 && from_offset == 0 && expect_id.trim().is_empty() {
2808            return self.snapshot_stream(sink);
2809        }
2810        Err(ControlError::Unimplemented("snapshot: this control plane cannot resume a snapshot"))
2811    }
2812
2813    /// `Mirror.LogRetain`: drop every whole segment below `from` (0 = head + 1); answers the
2814    /// head as `LogHead` does. Refused on a twin.
2815    fn log_retain(&self, _from: u64) -> ControlResult<gunnar_proto::LogHeadResponse> {
2816        Err(ControlError::Unimplemented("retention: this control plane keeps no log"))
2817    }
2818
2819    /// ★ **The log's segments, for a byte-bounded retention** (cov-13, 2026-10-06):
2820    /// `(first, last, bytes)`, oldest first, the open one last (`last = first - 1` while it
2821    /// holds nothing). Only whole segments are dropped, so this is what a byte budget is
2822    /// decided over. Default `Unimplemented`: a plane with no log.
2823    fn log_layout(&self) -> ControlResult<Vec<(u64, u64, u64)>> {
2824        Err(ControlError::Unimplemented("retention: this control plane keeps no log"))
2825    }
2826
2827    /// ★ **The data volume the log lives on**: `(total, used)` bytes, statvfs — what the
2828    /// retention's byte budget is a share of, and what a twin silent through main's growth
2829    /// is owed (cov-13). `None` when unknown, and while the set is released for a growth
2830    /// (statvfs would read the box's root, not the set).
2831    fn data_volume(&self) -> Option<(u64, u64)> {
2832        None
2833    }
2834
2835    /// ★ `twin-owed.json` (cov-13): what each twin is owed, as last written; empty = never.
2836    /// Default `Unimplemented`: the book then lives in memory only.
2837    fn twin_owed_read(&self) -> ControlResult<Vec<u8>> {
2838        Err(ControlError::Unimplemented("twin-owed: this control plane keeps no data root"))
2839    }
2840
2841    /// ★ Write `twin-owed.json` (replace, crash-safe).
2842    fn twin_owed_write(&self, _bytes: &[u8]) -> ControlResult<()> {
2843        Err(ControlError::Unimplemented("twin-owed: this control plane keeps no data root"))
2844    }
2845
2846    /// `Bootstrap.Restore`, one snapshot piece (`seq` 0), staged on disk: `Some(at)` once the
2847    /// terminator installed the snapshot, so the restore continues from `at + 1`.
2848    fn restore_snapshot_chunk(&self, _chunk: &gunnar_proto::LogChunk) -> ControlResult<Option<u64>> {
2849        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2850    }
2851
2852    // ── Outbound webhooks: delivery to an external URL ─────────────────────
2853    //
2854    // A webhook is a subscriber to the SAME event bus the replicator and the SSE
2855    // fan-out consume (one implementation), which POSTs each matching event to a URL, signed
2856    // with an sshsig the receiver verifies against the server's published key.
2857    // Configuration lives in `<data-root>/webhooks.json` and is set over the
2858    // control plane because the appliance has no shell — exactly the reason the
2859    // replication verbs above exist. Default `Unimplemented`, so a server built
2860    // without the `webhooks` feature says so rather than silently accepting a
2861    // configuration it will never deliver on (measured, not asserted — the silent green).
2862
2863    /// Add or replace one endpoint by name, persist it, and start delivering.
2864    fn webhook_add(&self, _request: AddWebhookRequest) -> ControlResult<WebhookListResponse> {
2865        Err(ControlError::Unimplemented(
2866            "outbound webhooks: this server was built without the `webhooks` feature, so it              cannot be given an outbound endpoint",
2867        ))
2868    }
2869
2870    /// What is configured, plus the signing public key a receiver verifies with.
2871    fn webhook_list(&self) -> ControlResult<WebhookListResponse> {
2872        Err(ControlError::Unimplemented(
2873            "outbound webhooks: this server was built without the `webhooks` feature, so it              has no outbound endpoints",
2874        ))
2875    }
2876
2877    /// Drop one endpoint by name, persist that, and stop delivering to it.
2878    fn webhook_remove(&self, _name: &str) -> ControlResult<WebhookListResponse> {
2879        Err(ControlError::Unimplemented(
2880            "outbound webhooks: this server was built without the `webhooks` feature, so it              has no outbound endpoint to remove",
2881        ))
2882    }
2883
2884    /// **The owner's enterprise list** (2026-10-05): the account audit log,
2885    /// the operator's aggregates and look, the name filter
2886    /// ([`crate::enterprise::EnterprisePlane`]), when this plane has the
2887    /// stores behind it. `None`: nothing is recorded, nothing is shielded,
2888    /// every name is clean, and every `Enterprise` verb answers UNIMPLEMENTED.
2889    fn enterprise(&self) -> Option<&dyn crate::enterprise::EnterprisePlane> {
2890        None
2891    }
2892}
2893
2894// ───────────────────────────────────────────────────────────────────────────
2895// The test double
2896// ───────────────────────────────────────────────────────────────────────────
2897
2898/// An in-memory [`ControlPlane`]. **A test double, not a backend.**
2899///
2900/// It implements the catalogue and identity halves so the services can be
2901/// exercised end to end, and answers [`ControlError::Unimplemented`] for
2902/// everything that genuinely needs `gunnar-store`: refs, objects, diffs and
2903/// mirror sync. Returning an empty success for those would be worse than
2904/// useless: an empty ref list looks exactly like a working server with no refs
2905/// (measured, not asserted — the silent green).
2906#[derive(Debug, Default)]
2907pub struct MemoryControlPlane {
2908    inner: Mutex<Inner>,
2909    /// **A slow synchronous call, on demand** (R5.1-G): milliseconds every
2910    /// `bootstrap_state` sleeps on the calling thread before it answers — the
2911    /// shape of a real control-plane call (a disk walk, an fsync) that holds a
2912    /// worker without yielding. Zero, the default, answers at once.
2913    stall_ms: std::sync::atomic::AtomicU64,
2914}
2915
2916/// One seat lot: `(room per seat, total, filled ids, order)`.
2917type SeatLot = (u64, u32, Vec<String>, String);
2918
2919#[derive(Debug, Default)]
2920struct Inner {
2921    namespaces: BTreeMap<String, Namespace>,
2922    repos: BTreeMap<(String, String), Repo>,
2923    principals: BTreeMap<String, Principal>,
2924    /// Employee id → the account that enrolled it (`enroll_employee`).
2925    employers: BTreeMap<String, String>,
2926    /// Employee id → the room it holds and the order it came with.
2927    rooms: BTreeMap<String, (u64, String)>,
2928    /// Account → its seat lots `(room per seat, total, filled ids, order)`.
2929    seats: BTreeMap<String, Vec<SeatLot>>,
2930    /// Order → `(account, principal or "", seats, room, need)`.
2931    room_orders: BTreeMap<String, (String, String, u32, u64, u64)>,
2932    /// namespace → what a test says gunnar measured for it (`usage`).
2933    /// Empty in every other use, and then `usage` answers `Unimplemented`
2934    /// exactly as before: the double has no store to count.
2935    usage: BTreeMap<String, gunnar_proto::NamespaceUsage>,
2936    /// Web credentials (Stage L): credential id (base64url) → the row and
2937    /// its public key.
2938    credentials: BTreeMap<String, StoredCredential>,
2939    /// Web credential (base64url) → the session keys its logins enrolled.
2940    credential_sessions: BTreeMap<String, Vec<String>>,
2941    /// Invites (Stage L): id → the row and the secret's hash.
2942    invites: BTreeMap<String, (gunnar_proto::Invite, String)>,
2943    /// Redeemed invites: the secret's hash (the plane's USED answer).
2944    redeemed_invites: std::collections::BTreeSet<String>,
2945    keys: BTreeMap<String, PublicKey>,
2946    grants: Vec<Grant>,
2947    protections: BTreeMap<(String, String), BTreeMap<String, BranchProtection>>,
2948    ref_policies: BTreeMap<(String, String), RefNamePolicy>,
2949    mirrors: BTreeMap<(String, String), MirrorConfig>,
2950    /// fingerprint → the `authorized_keys` line, so a credential can be
2951    /// verified. The proto `PublicKey` deliberately does not carry key
2952    /// material; the gate needs it, so the double keeps it beside.
2953    openssh: BTreeMap<String, String>,
2954    /// Client-certificate DER → the identity it was registered under.
2955    ///
2956    /// Keyed by the **bytes**, not by a fingerprint the double computed: the
2957    /// real adapter hashes with `gunnar_core::auth::fingerprint_of_certificate`
2958    /// and a second hash spelled here would be a twin of that decision (one implementation).
2959    /// The double is handed the fingerprint by whoever registers, so the two
2960    /// can never disagree — there is only one of them.
2961    certificates: BTreeMap<Vec<u8>, CertificateIdentity>,
2962    /// `Scope::Server` grants (the ACL model). The proto `Grant` cannot express one, since
2963    /// it requires a namespace, which is a real gap recorded in
2964    /// `gunnar-cli`'s adapter; the double works around it rather than
2965    /// pretending a server-wide grant is a namespace grant on `""`.
2966    server_grants: BTreeMap<String, GrantLevel>,
2967    /// Narrowed principals → the methods each may call (f-principal-reach).
2968    narrowed: BTreeMap<String, Vec<String>>,
2969    /// See [`MemoryControlPlane::force_may`]. `None` in every real use.
2970    forced_may: Option<bool>,
2971    /// See [`MemoryControlPlane::set_twin`]. `false` unless a test says otherwise.
2972    twin: bool,
2973    /// `set_log_follows`: a main being restored.
2974    log_follows: bool,
2975    /// See [`MemoryControlPlane::await_enrolment`]: fingerprint → the fed key's
2976    /// OpenSSH line. Empty unless a test feeds one.
2977    awaiting: BTreeMap<String, String>,
2978    /// See [`MemoryControlPlane::forge_principal_id`]. `None` in every real use.
2979    forged_principal_id: Option<String>,
2980    /// The bootstrap record: `(at, principal, fingerprint)`. Beside the
2981    /// identity maps above and never removed with them, which is the whole
2982    /// property the double has to reproduce.
2983    bootstrap: Option<(i64, String, String)>,
2984    /// namespace → the fact this box holds for it (the METERED appliance).
2985    ///
2986    /// Seeded by a test; empty in every other use. `entitlement_get` answers
2987    /// out of it, which is the truthful thing for a double to do — "no fact"
2988    /// is Free, and that is a real answer rather than a missing one. Writing a
2989    /// fact is still [`ControlError::Unimplemented`]: the double has no
2990    /// monetize key to VERIFY one with, and a `set` that stored whatever it
2991    /// was given would be a double that is more permissive than the thing it
2992    /// stands in for.
2993    #[cfg(feature = "metered")]
2994    entitlements: BTreeMap<String, gunnar_proto::EntitlementFact>,
2995    /// How many times the actor-ticket key has been asked to sign.
2996    ///
2997    /// **An instrument, and the reason it exists is a specific defect.** A
2998    /// handler that signed a ticket and then checked the grant would satisfy a
2999    /// test that only looked at the returned `Status`, while having put a
3000    /// valid, spendable ticket for somebody else's tenant into whatever sits
3001    /// between the handler and the caller. Counting the signatures is how a
3002    /// refusal is held to "nothing was minted" rather than to "nothing was
3003    /// returned".
3004    #[cfg(feature = "metered")]
3005    actor_tickets_signed: u64,
3006    /// The double's actor-ticket key, minted on first use, exactly as the real
3007    /// appliance's is — a real ed25519 key and never a stand-in, because the
3008    /// signature it makes is checked by the OTHER repository's verifier in
3009    /// `tests/billing.rs`.
3010    #[cfg(feature = "metered")]
3011    actor_ticket_key: Option<ed25519_dalek::SigningKey>,
3012}
3013
3014impl Inner {
3015    /// The double's actor-ticket key, minted on first use.
3016    ///
3017    /// Minted from `OsRng` and not from a constant seed. A fixed key would make
3018    /// every run of `tests/billing.rs` produce byte-identical tickets, which
3019    /// reads as reassuring and is the opposite: the contract test would then
3020    /// be checking one recorded signature rather than the agreement between a
3021    /// signer and a verifier.
3022    #[cfg(feature = "metered")]
3023    fn actor_ticket_key(inner: &mut Inner) -> &ed25519_dalek::SigningKey {
3024        inner.actor_ticket_key.get_or_insert_with(|| {
3025            use rand::Rng as _;
3026            let mut seed = [0u8; 32];
3027            rand::rand_core::UnwrapErr(rand::rngs::SysRng).fill_bytes(&mut seed);
3028            ed25519_dalek::SigningKey::from_bytes(&seed)
3029        })
3030    }
3031
3032    /// Who, other than `own_id`, holds `name` — as their name or as their id.
3033    /// The double's spelling of `AuthTable::refuse_taken_name`.
3034    fn name_holder(inner: &Inner, name: &str, own_id: &str) -> Option<String> {
3035        inner
3036            .principals
3037            .values()
3038            .find(|p| p.id != own_id && (p.name == name || p.id == name))
3039            .map(|p| p.id.clone())
3040    }
3041}
3042
3043/// The default branch a repo gets when the request does not name one.
3044pub const DEFAULT_BRANCH: &str = "refs/heads/main";
3045
3046/// sha256 is the default hash kind for new repos.
3047pub const DEFAULT_HASH_KIND: HashKind = HashKind::Sha256;
3048
3049impl MemoryControlPlane {
3050    /// **Say this plane is a twin** (T21): what `CoreControlPlane::is_twin` reads off
3051    /// `twin.json`, for the gate's twin-only grow verbs.
3052    pub fn set_twin(&self, yes: bool) {
3053        self.inner.lock().expect("memory control plane").twin = yes;
3054    }
3055
3056    /// **Say this plane's log follows another's** (`ControlPlane::log_follows`):
3057    /// a main being restored, as `CoreControlPlane` reads it off its txlog.
3058    pub fn set_log_follows(&self, yes: bool) {
3059        self.inner.lock().expect("memory control plane").log_follows = yes;
3060    }
3061
3062    /// **Say this key was fed and is not enrolled yet** (`Identity::awaiting_enrolment`):
3063    /// what a primary's installer feed says of the front's keys until its pairing loop
3064    /// enrolled them.
3065    pub fn await_enrolment(&self, fingerprint: &str, openssh: &str) {
3066        self.inner.lock().expect("memory control plane").awaiting.insert(fingerprint.to_owned(), openssh.to_owned());
3067    }
3068
3069    pub fn new() -> Self {
3070        Self::default()
3071    }
3072
3073    /// Make every `bootstrap_state` (`Bootstrap.Status`) hold its thread for
3074    /// `by` before it answers: a synchronous call that blocks the worker it
3075    /// runs on, which is what the keepalive test needs every handler worker
3076    /// doing at once.
3077    pub fn stall_bootstrap_state(&self, by: std::time::Duration) {
3078        self.stall_ms.store(
3079            by.as_millis().min(u64::MAX as u128) as u64,
3080            std::sync::atomic::Ordering::SeqCst,
3081        );
3082    }
3083
3084    /// Convenience for tests: a namespace and a repo in one call.
3085    pub fn with_repo(namespace: &str, repo: &str) -> Self {
3086        let plane = Self::new();
3087        plane
3088            .create_namespace(namespace, "")
3089            .expect("fresh namespace");
3090        plane
3091            .create_repo(NewRepo {
3092                path: RepoPath::new(namespace, repo),
3093                hash_kind: DEFAULT_HASH_KIND,
3094                default_branch: String::new(),
3095                visibility: Visibility::Private,
3096                description: String::new(),
3097                adopt: false,
3098            })
3099            .expect("fresh repo");
3100        plane
3101    }
3102
3103    fn key(path: &RepoPath) -> (String, String) {
3104        (path.namespace.clone(), path.repo.clone())
3105    }
3106
3107    /// **Narrow a principal to a method list** (f-principal-reach).
3108    pub fn narrow(&self, principal_id: &str, methods: &[&str]) {
3109        self.inner.lock().unwrap().narrowed.insert(principal_id.to_owned(), methods.iter().map(|m| m.to_string()).collect());
3110    }
3111
3112    /// Give a principal a `Scope::Server` grant (the ACL model).
3113    ///
3114    /// Not on [`ControlPlane`]: the proto `Grant` cannot express a server-wide
3115    /// scope (it requires a namespace), which is a gap in `gunnar-proto`
3116    /// recorded in `gunnar-cli`'s adapter. Until that is fixed the double
3117    /// carries server grants beside the namespace/repo ones rather than
3118    /// smuggling one in as a namespace grant on `""`.
3119    pub fn grant_server(&self, principal_id: &str, level: GrantLevel) {
3120        self.inner
3121            .lock()
3122            .unwrap()
3123            .server_grants
3124            .insert(principal_id.to_owned(), level);
3125    }
3126
3127    /// Seed what gunnar measured for one namespace (`Usage.Read`'s row).
3128    pub fn set_usage(&self, usage: gunnar_proto::NamespaceUsage) {
3129        self.inner
3130            .lock()
3131            .unwrap()
3132            .usage
3133            .insert(usage.namespace.clone(), usage);
3134    }
3135
3136    /// Seed the fact this box holds for one namespace (the METERED appliance).
3137    ///
3138    /// The double's stand-in for a fact monetize signed and `Entitlement.Set`
3139    /// verified and stored. It takes the fact as given and checks no signature,
3140    /// which is why it is a test-only setter and not `entitlement_set`.
3141    #[cfg(feature = "metered")]
3142    pub fn set_entitlement_fact(&self, fact: gunnar_proto::EntitlementFact) {
3143        self.inner
3144            .lock()
3145            .unwrap()
3146            .entitlements
3147            .insert(fact.namespace.clone(), fact);
3148    }
3149
3150    /// **How many times the actor-ticket key has been asked to sign.**
3151    ///
3152    /// The instrument that turns "the request was refused" into "nothing was
3153    /// minted". See the field's own comment for the defect it exists to catch.
3154    #[cfg(feature = "metered")]
3155    pub fn actor_tickets_signed(&self) -> u64 {
3156        self.inner.lock().unwrap().actor_tickets_signed
3157    }
3158
3159    /// **A red-first instrument.** Force [`Identity::may`] to a constant answer.
3160    ///
3161    /// `Some(false)` must make every *allow* test in `tests/auth.rs` go red,
3162    /// and `Some(true)` must make every *permission-denied* test go red. Both
3163    /// are asserted there. Without them a handler that had been hard-wired to
3164    /// allow, or a gate that refused everything, would pass a naive suite just
3165    /// as well as a correct one. That is the exact failure this instrument
3166    /// exists to make impossible.
3167    ///
3168    /// `None` restores the real answer.
3169    pub fn force_may(&self, forced: Option<bool>) {
3170        self.inner.lock().unwrap().forced_may = forced;
3171    }
3172
3173    /// **A red-first instrument, for anonymous read.** Make
3174    /// [`Identity::registered_key`] name
3175    /// a principal of the caller's choosing, whatever the key table says.
3176    ///
3177    /// `AuthGate` refuses a credential that resolves to the anonymous principal
3178    /// id. Through `gunnar-core` that state is unreachable (the reserved id
3179    /// cannot be registered and cannot be given a key), so without this the
3180    /// guard could never be *seen* refusing, and a guard nobody has watched
3181    /// fail is worth nothing.
3182    ///
3183    /// Nor is it a contrivance. `AuthGate` takes [`ControlPlane`] as a
3184    /// **trait**, so any adapter, present or future, can return whatever
3185    /// principal id it likes. That is the situation this reproduces.
3186    ///
3187    /// `None` restores the real answer.
3188    pub fn forge_principal_id(&self, id: Option<&str>) {
3189        self.inner.lock().unwrap().forged_principal_id = id.map(str::to_owned);
3190    }
3191}
3192
3193/// `Admin` implies `Write` implies `Read` (the ACL model). One ordering, used by every
3194/// comparison below.
3195fn level_rank(level: GrantLevel) -> u8 {
3196    match level {
3197        GrantLevel::Unspecified => 0,
3198        GrantLevel::Read => 1,
3199        GrantLevel::Write => 2,
3200        GrantLevel::Admin => 3,
3201    }
3202}
3203
3204/// The **test double's** ACL.
3205///
3206/// It is a double of `gunnar_core::AuthTable::may`, not a second production
3207/// decision: the real server routes every `may` through `AuthTable` via the
3208/// adapter, and this exists so the seven services can be exercised without a
3209/// storage backend. Its rules are deliberately the same three: unknown
3210/// principal may nothing, a covering grant must be at least `level`, and a
3211/// namespace grant matches its namespace **exactly** (`team` does not reach
3212/// `team/sub`).
3213impl Identity for MemoryControlPlane {
3214    fn awaiting_enrolment(&self, fingerprint: &str) -> Option<String> {
3215        self.inner.lock().unwrap().awaiting.get(fingerprint).cloned()
3216    }
3217
3218    fn narrowed_methods(&self, principal_id: &str) -> ControlResult<Option<Vec<String>>> {
3219        Ok(self.inner.lock().unwrap().narrowed.get(principal_id).cloned())
3220    }
3221
3222    fn registered_key(&self, fingerprint: &str) -> ControlResult<Option<RegisteredKey>> {
3223        let inner = self.inner.lock().unwrap();
3224        let Some(key) = inner.keys.get(fingerprint) else {
3225            return Ok(None);
3226        };
3227        if let Some(expires) = key.expires_at_unix_ms {
3228            if now_ms() >= expires {
3229                return Ok(None);
3230            }
3231        }
3232        let Some(openssh) = inner.openssh.get(fingerprint) else {
3233            return Ok(None);
3234        };
3235        Ok(Some(RegisteredKey {
3236            principal_id: inner
3237                .forged_principal_id
3238                .clone()
3239                .unwrap_or_else(|| key.principal_id.clone()),
3240            openssh: openssh.clone(),
3241            machine: inner
3242                .principals
3243                .get(&key.principal_id)
3244                .is_some_and(|p| p.kind == PrincipalKind::Machine as i32),
3245        }))
3246    }
3247
3248    fn registered_certificate(&self, der: &[u8]) -> ControlResult<Option<CertificateIdentity>> {
3249        let inner = self.inner.lock().unwrap();
3250        Ok(inner.certificates.get(der).cloned())
3251    }
3252
3253    fn may(&self, principal_id: &str, access: &Access, level: GrantLevel) -> ControlResult<bool> {
3254        if level == GrantLevel::Unspecified {
3255            return Ok(false);
3256        }
3257        let inner = self.inner.lock().unwrap();
3258        if let Some(forced) = inner.forced_may {
3259            return Ok(forced);
3260        }
3261        if !inner.principals.contains_key(principal_id) {
3262            return Ok(false);
3263        }
3264        // A principal is intrinsically admin of the account named after it.
3265        // `gunnar_core::AuthTable::owns_account` is the real decision; this is
3266        // the double keeping the same shape, and `tests/bootstrap.rs` asserts
3267        // both directions of it so the double cannot quietly allow more.
3268        // A MEMBER owns no account by its name (2026-10-07), as in the table.
3269        let member = inner.employers.contains_key(principal_id);
3270        let owns_account = |namespace: &str| !member && !namespace.is_empty() && namespace == principal_id;
3271        match access {
3272            Access::Namespace(name) if owns_account(name) => return Ok(true),
3273            Access::Repo(path) if owns_account(&path.namespace) => return Ok(true),
3274            _ => {}
3275        }
3276        // A PUBLIC repository is readable by every registered principal —
3277        // `AuthTable::may`'s rule for it, kept here so a double cannot quietly
3278        // hide what the real table shows (the outsider's view of an account,
3279        // `tests/account_model.rs`).
3280        if let Access::Repo(path) = access {
3281            if level == GrantLevel::Read
3282                && inner
3283                    .repos
3284                    .get(&(path.namespace.clone(), path.repo.clone()))
3285                    .is_some_and(|repo| repo.visibility == Visibility::Public as i32)
3286            {
3287                return Ok(true);
3288            }
3289        }
3290        let want = level_rank(level);
3291        // **A server grant stops at a customer's account** (2026-10-05):
3292        // `gunnar_core::AuthTable`'s shield, kept here so the double cannot
3293        // quietly show an operator what the real table refuses it.
3294        // The box's own first identity (bootstrap) is the estate, as a
3295        // machine is: `gunnar_core::AuthTable::is_box_identity`.
3296        let box_identity = inner.bootstrap.as_ref().map(|(_, id, _)| id.as_str());
3297        let machine = box_identity == Some(principal_id)
3298            || inner
3299                .principals
3300                .get(principal_id)
3301                .is_some_and(|p| p.kind == PrincipalKind::Machine as i32);
3302        let shielded = |namespace: &str| {
3303            let account = namespace.split('/').next().unwrap_or("");
3304            !machine
3305                && !account.is_empty()
3306                && box_identity != Some(account)
3307                && account != principal_id
3308                && inner
3309                    .principals
3310                    .get(account)
3311                    .is_some_and(|p| p.kind == PrincipalKind::User as i32)
3312                && !inner.employers.contains_key(account)
3313                && inner.employers.get(principal_id).map(String::as_str) != Some(account)
3314        };
3315        let inside_closed_account = match access {
3316            Access::Server => false,
3317            Access::Namespace(name) => shielded(name),
3318            Access::Repo(path) => shielded(&path.namespace),
3319        };
3320        let server = inner
3321            .server_grants
3322            .get(principal_id)
3323            .copied()
3324            .map(level_rank)
3325            .unwrap_or(0);
3326        if server >= want && !inside_closed_account {
3327            return Ok(true);
3328        }
3329        let scoped = inner
3330            .grants
3331            .iter()
3332            .filter(|grant| grant.principal_id == principal_id)
3333            .filter(|grant| match access {
3334                // A server-scoped question is never answered by a namespace or
3335                // repo grant. Widening it there would be silent escalation.
3336                Access::Server => false,
3337                Access::Namespace(name) => grant.repo.is_empty() && &grant.namespace == name,
3338                Access::Repo(path) => {
3339                    grant.namespace == path.namespace
3340                        && (grant.repo.is_empty() || grant.repo == path.repo)
3341                }
3342            })
3343            .filter_map(|grant| GrantLevel::try_from(grant.level).ok())
3344            .map(level_rank)
3345            .max()
3346            .unwrap_or(0);
3347        Ok(scoped >= want)
3348    }
3349}
3350
3351/// The double's first-boot door.
3352///
3353/// It mirrors `gunnar_core::AuthTable::install_first_identity` in the one thing
3354/// that matters: the record is checked and written **under the same lock** as
3355/// the identity it creates, so two concurrent enrolments cannot both win. In
3356/// the real server that lock is a compare-and-swap on `refs/gunnar/auth`; here
3357/// it is the `Mutex` this whole double is behind. `tests/bootstrap.rs` races
3358/// both.
3359impl FirstBoot for MemoryControlPlane {
3360    fn bootstrap_state(&self) -> ControlResult<BootstrapState> {
3361        let stall = self.stall_ms.load(std::sync::atomic::Ordering::SeqCst);
3362        if stall > 0 {
3363            // Blocking on purpose: `std::thread::sleep`, not a timer, so the
3364            // worker is held exactly as a synchronous store call holds it.
3365            std::thread::sleep(std::time::Duration::from_millis(stall));
3366        }
3367        let inner = self.inner.lock().unwrap();
3368        let (at, principal, fingerprint) =
3369            inner
3370                .bootstrap
3371                .clone()
3372                .unwrap_or((0, String::new(), String::new()));
3373        Ok(BootstrapState {
3374            window_open: inner.bootstrap.is_none()
3375                && inner.principals.is_empty()
3376                && inner.keys.is_empty(),
3377            bootstrapped: inner.bootstrap.is_some(),
3378            bootstrapped_at_unix_ms: at,
3379            bootstrapped_principal: principal,
3380            bootstrapped_fingerprint: fingerprint,
3381            principals: inner.principals.len() as u64,
3382            keys: inner.keys.len() as u64,
3383            // An in-memory control plane has no disk to be racing, so it is
3384            // always serving. The boot window is `gunnar-server`'s fact.
3385            serving_git: true,
3386            readiness: "serving git".to_owned(),
3387            build: String::new(),
3388            role: "primary".to_owned(),
3389            ..Default::default()
3390        })
3391    }
3392
3393    fn enroll_first_identity(
3394        &self,
3395        name: &str,
3396        openssh_public_key: &str,
3397    ) -> ControlResult<Enrolled> {
3398        let fingerprint = fingerprint_of_openssh(openssh_public_key).ok_or_else(|| {
3399            ControlError::InvalidArgument(format!(
3400                "{openssh_public_key:?} is not an authorized_keys line"
3401            ))
3402        })?;
3403        let mut inner = self.inner.lock().unwrap();
3404        if let Some((at, principal, _)) = &inner.bootstrap {
3405            return Err(ControlError::FailedPrecondition(format!(
3406                "this server was bootstrapped at {at} by \"{principal}\"; the window is closed \
3407                 permanently and deleting identities does not reopen it"
3408            )));
3409        }
3410        if !inner.principals.is_empty() || !inner.keys.is_empty() {
3411            return Err(ControlError::FailedPrecondition(format!(
3412                "this server already holds {} principals and {} keys, so it is not a fresh server",
3413                inner.principals.len(),
3414                inner.keys.len()
3415            )));
3416        }
3417        if name.is_empty() {
3418            return Err(ControlError::InvalidArgument("`name` is required".into()));
3419        }
3420        let at = now_ms();
3421        let principal = Principal {
3422            id: name.to_owned(),
3423            name: name.to_owned(),
3424            kind: PrincipalKind::User as i32,
3425            created_at_unix_ms: at,
3426        };
3427        let key = PublicKey {
3428            fingerprint: fingerprint.clone(),
3429            algo: String::new(),
3430            principal_id: name.to_owned(),
3431            comment: String::new(),
3432            added_at_unix_ms: at,
3433            last_used_unix_ms: None,
3434            expires_at_unix_ms: None,
3435        };
3436        inner.principals.insert(name.to_owned(), principal.clone());
3437        inner.keys.insert(fingerprint.clone(), key.clone());
3438        inner
3439            .openssh
3440            .insert(fingerprint.clone(), openssh_public_key.trim().to_owned());
3441        // gunnar-admin, the ordinary `Scope::Server` Admin grant.
3442        inner
3443            .server_grants
3444            .insert(name.to_owned(), GrantLevel::Admin);
3445        inner.namespaces.insert(
3446            name.to_owned(),
3447            Namespace {
3448                name: name.to_owned(),
3449                description: String::new(),
3450                created_at_unix_ms: at,
3451                owner_kind: 0,
3452                ..Default::default()
3453            },
3454        );
3455        inner.bootstrap = Some((at, name.to_owned(), fingerprint));
3456        Ok(Enrolled {
3457            principal,
3458            key,
3459            account: name.to_owned(),
3460            bootstrapped_at_unix_ms: at,
3461            // The in-memory control plane has no PKI; the degraded (empty)
3462            // shape is exactly what it should exercise.
3463            client_cert_pem: String::new(),
3464            client_key_pem: String::new(),
3465            issuer_cert_pem: String::new(),
3466        })
3467    }
3468}
3469
3470fn now_ms() -> i64 {
3471    crate::bus::now_unix_ms()
3472}
3473
3474/// The `SHA256:<base64>` fingerprint OpenSSH prints, computed by `ssh-key`
3475/// itself.
3476///
3477/// This used to be a deliberate `PLACEHOLDER-…` string, because computing
3478/// "something like" a fingerprint and letting an integration believe it would
3479/// have been a lie. Since gRPC authentication landed this crate links
3480/// `ssh-key`, so the double can produce the **real** value. It must, because a
3481/// credential carries the fingerprint the client's key actually has and the
3482/// double has to be able to find it.
3483///
3484/// It is not a second definition of gunnar's fingerprint: `ssh-key`'s own
3485/// `PublicKey::fingerprint` and `gunnar_core::fingerprint_of_blob` are both
3486/// sha256 over the same wire blob, and `gunnar-core` only spells it out by hand
3487/// because russh pins an incompatible `ssh-key`. A mismatch between them
3488/// would be caught the first time a real key was used against a real table.
3489fn fingerprint_of_openssh(line: &str) -> Option<String> {
3490    let key = ssh_key::PublicKey::from_openssh(line).ok()?;
3491    Some(key.fingerprint(ssh_key::HashAlg::Sha256).to_string())
3492}
3493
3494/// **The double's spelling of the key-attestation rule**, structurally.
3495///
3496/// The rule and its arithmetic are `gunnar_core::key_attestation` — one
3497/// writer, which the shipped plane asks. This crate does not depend on
3498/// `gunnar-core` (see the note on `fingerprint_of_openssh`'s neighbour), so the
3499/// double reproduces the three arms — nobody to ask, the caller is the target,
3500/// otherwise required — and checks the attestation's SHAPE: present, about
3501/// this principal and this fingerprint, signed by a key the target holds. It
3502/// does **not** verify the signature; the real record is verified on the real
3503/// plane, and `gunnar-cli/tests/add_key_attested.rs` drives that red. The same
3504/// stance the double takes on membership events, which it does not implement
3505/// at all.
3506fn require_attestation(
3507    target: &str,
3508    caller: &str,
3509    holders: &[String],
3510    adding: &str,
3511    attestation: Option<&SignedKeyAttestation>,
3512) -> ControlResult<()> {
3513    if holders.is_empty() || (!caller.is_empty() && caller == target) {
3514        return Ok(());
3515    }
3516    let Some(signed) = attestation else {
3517        return Err(ControlError::FailedPrecondition(format!(
3518            "principal {target} already holds {} key(s), so adding another needs {target}'s own \
3519             attestation over the new key's fingerprint ({adding})",
3520            holders.len()
3521        )));
3522    };
3523    let Some(subject) = signed.attestation.as_ref() else {
3524        return Err(ControlError::InvalidArgument("`attestation.attestation` is required".into()));
3525    };
3526    if subject.principal_id != target || subject.fingerprint != adding {
3527        return Err(ControlError::InvalidArgument(format!(
3528            "the attestation is for key {} on {:?} but the request adds key {adding} to {target:?}",
3529            subject.fingerprint, subject.principal_id
3530        )));
3531    }
3532    if !holders.iter().any(|held| held == &signed.key_fingerprint) {
3533        return Err(ControlError::InvalidArgument(format!(
3534            "the attestation is signed by {}, which is not a key {target:?} holds",
3535            signed.key_fingerprint
3536        )));
3537    }
3538    Ok(())
3539}
3540
3541impl ControlPlane for MemoryControlPlane {
3542    fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<gunnar_proto::NamespaceUsage>> {
3543        let inner = self.inner.lock().unwrap();
3544        if inner.usage.is_empty() {
3545            return Err(ControlError::Unimplemented("Usage"));
3546        }
3547        Ok(inner
3548            .usage
3549            .values()
3550            .filter(|row| namespace.is_none_or(|ns| row.namespace == ns))
3551            .cloned()
3552            .collect())
3553    }
3554
3555    fn is_twin(&self) -> bool {
3556        self.inner.lock().expect("memory control plane").twin
3557    }
3558
3559    fn log_follows(&self) -> bool {
3560        let inner = self.inner.lock().expect("memory control plane");
3561        inner.twin || inner.log_follows
3562    }
3563
3564    /// The fact this box holds, or `None` = Free.
3565    ///
3566    /// The double answers this one rather than refusing, because it now has
3567    /// somewhere to keep a fact ([`MemoryControlPlane::set_entitlement_fact`])
3568    /// and "no fact" is a real answer and not a missing one. `entitlement_set`
3569    /// stays `Unimplemented`: storing is not the half the double is missing,
3570    /// VERIFYING is, and a double that stored an unverified fact would be more
3571    /// permissive than the thing it stands in for.
3572    #[cfg(feature = "metered")]
3573    fn entitlement_get(
3574        &self,
3575        namespace: &str,
3576    ) -> ControlResult<Option<gunnar_proto::EntitlementFact>> {
3577        Ok(self
3578            .inner
3579            .lock()
3580            .unwrap()
3581            .entitlements
3582            .get(namespace)
3583            .cloned())
3584    }
3585
3586    #[cfg(feature = "metered")]
3587    fn entitlement_list(&self) -> ControlResult<Vec<gunnar_proto::EntitlementFact>> {
3588        Ok(self
3589            .inner
3590            .lock()
3591            .unwrap()
3592            .entitlements
3593            .values()
3594            .cloned()
3595            .collect())
3596    }
3597
3598    /// The double's actor-ticket key, minted on first use and stable after —
3599    /// which is what the real store's `get_or_create_with` gives the appliance,
3600    /// reproduced here under this double's one `Mutex`.
3601    #[cfg(feature = "metered")]
3602    fn actor_ticket_public_key_hex(&self) -> ControlResult<String> {
3603        let mut inner = self.inner.lock().unwrap();
3604        let key = Inner::actor_ticket_key(&mut inner);
3605        Ok(key
3606            .verifying_key()
3607            .to_bytes()
3608            .iter()
3609            .map(|byte| format!("{byte:02x}"))
3610            .collect())
3611    }
3612
3613    #[cfg(feature = "metered")]
3614    fn actor_ticket_sign(&self, message: &[u8]) -> ControlResult<[u8; 64]> {
3615        use ed25519_dalek::Signer as _;
3616        let mut inner = self.inner.lock().unwrap();
3617        let signature = Inner::actor_ticket_key(&mut inner).sign(message).to_bytes();
3618        // Counted AFTER the signature exists, so the number means "a ticket
3619        // this box signed" and not "a call that was attempted".
3620        inner.actor_tickets_signed += 1;
3621        Ok(signature)
3622    }
3623
3624    fn create_namespace(&self, name: &str, description: &str) -> ControlResult<Namespace> {
3625        if name.is_empty() {
3626            return Err(ControlError::InvalidArgument("namespace is empty".into()));
3627        }
3628        let mut inner = self.inner.lock().unwrap();
3629        if inner.namespaces.contains_key(name) {
3630            return Err(ControlError::AlreadyExists(format!("namespace {name}")));
3631        }
3632        let namespace = Namespace {
3633            name: name.to_string(),
3634            description: description.to_string(),
3635            created_at_unix_ms: now_ms(),
3636            owner_kind: 0,
3637            ..Default::default()
3638        };
3639        inner.namespaces.insert(name.to_string(), namespace.clone());
3640        Ok(namespace)
3641    }
3642
3643    fn list_namespaces(&self) -> ControlResult<Vec<Namespace>> {
3644        let inner = self.inner.lock().unwrap();
3645        Ok(inner
3646            .namespaces
3647            .values()
3648            .cloned()
3649            .map(|mut ns| {
3650                ns.employees = inner
3651                    .employers
3652                    .values()
3653                    .filter(|account| **account == ns.name)
3654                    .count() as u32;
3655                ns
3656            })
3657            .collect())
3658    }
3659
3660    fn set_namespace_settings(
3661        &self,
3662        name: &str,
3663        public: Option<bool>,
3664        events_public: Option<bool>,
3665    ) -> ControlResult<Namespace> {
3666        {
3667            let mut inner = self.inner.lock().unwrap();
3668            let ns = inner
3669                .namespaces
3670                .get_mut(name)
3671                .ok_or_else(|| ControlError::NotFound(format!("namespace {name}")))?;
3672            if let Some(public) = public {
3673                ns.public = public;
3674            }
3675            if let Some(events_public) = events_public {
3676                ns.events_public = events_public;
3677            }
3678        }
3679        Ok(self
3680            .namespace(name)?
3681            .expect("the namespace was just written"))
3682    }
3683
3684    fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32> {
3685        let mut inner = self.inner.lock().unwrap();
3686        if !inner.namespaces.contains_key(name) {
3687            return Err(ControlError::NotFound(format!("namespace {name}")));
3688        }
3689        let held: Vec<(String, String)> = inner
3690            .repos
3691            .keys()
3692            .filter(|(ns, _)| ns == name)
3693            .cloned()
3694            .collect();
3695        if !held.is_empty() && !delete_repos {
3696            return Err(ControlError::FailedPrecondition(format!(
3697                "namespace {name} holds {} repos; deleting repos is never implicit",
3698                held.len()
3699            )));
3700        }
3701        for key in &held {
3702            inner.repos.remove(key);
3703            inner.mirrors.remove(key);
3704            inner.protections.remove(key);
3705            inner.ref_policies.remove(key);
3706        }
3707        inner.namespaces.remove(name);
3708        inner.grants.retain(|g| g.namespace != name);
3709        Ok(held.len() as u32)
3710    }
3711
3712    fn create_repo(&self, spec: NewRepo) -> ControlResult<Repo> {
3713        spec.path
3714            .validate()
3715            .map_err(|e| ControlError::InvalidArgument(e.to_string()))?;
3716        let mut inner = self.inner.lock().unwrap();
3717        if !inner.namespaces.contains_key(&spec.path.namespace) {
3718            return Err(ControlError::NotFound(format!(
3719                "namespace {}",
3720                spec.path.namespace
3721            )));
3722        }
3723        let key = MemoryControlPlane::key(&spec.path);
3724        if inner.repos.contains_key(&key) {
3725            return Err(ControlError::AlreadyExists(format!(
3726                "repository {}",
3727                spec.path
3728            )));
3729        }
3730        let hash_kind = if spec.hash_kind == HashKind::Unspecified {
3731            DEFAULT_HASH_KIND
3732        } else {
3733            spec.hash_kind
3734        };
3735        let default_branch = if spec.default_branch.is_empty() {
3736            DEFAULT_BRANCH.to_string()
3737        } else {
3738            spec.default_branch.clone()
3739        };
3740        let visibility = if spec.visibility == Visibility::Unspecified {
3741            Visibility::Private
3742        } else {
3743            spec.visibility
3744        };
3745        let repo = Repo {
3746            path: Some(spec.path.clone()),
3747            hash_kind: hash_kind as i32,
3748            default_branch,
3749            visibility: visibility as i32,
3750            created_at_unix_ms: now_ms(),
3751            description: spec.description,
3752            encrypted: false,
3753        };
3754        inner.repos.insert(key, repo.clone());
3755        Ok(repo)
3756    }
3757
3758    fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo> {
3759        self.inner
3760            .lock()
3761            .unwrap()
3762            .repos
3763            .get(&MemoryControlPlane::key(path))
3764            .cloned()
3765            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))
3766    }
3767
3768    fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>> {
3769        Ok(self
3770            .inner
3771            .lock()
3772            .unwrap()
3773            .repos
3774            .iter()
3775            .filter(|((ns, _), _)| namespace.is_none_or(|want| want == ns))
3776            .map(|(_, repo)| repo.clone())
3777            .collect())
3778    }
3779
3780    /// The test double holds no bytes, so both arms of the real verb collapse
3781    /// to removing the row — but the OUTCOME still says which was asked for,
3782    /// because that is the fact the service reports and a double that always
3783    /// said `Purged` would let a soft-delete regression pass.
3784    fn delete_repo(&self, path: &RepoPath, options: DeleteOptions) -> ControlResult<DeleteOutcome> {
3785        let key = MemoryControlPlane::key(path);
3786        let mut inner = self.inner.lock().unwrap();
3787        if inner.repos.remove(&key).is_none() {
3788            return Err(ControlError::NotFound(format!("repository {path}")));
3789        }
3790        inner.mirrors.remove(&key);
3791        inner.protections.remove(&key);
3792        inner.ref_policies.remove(&key);
3793        inner
3794            .grants
3795            .retain(|g| !(g.namespace == path.namespace && g.repo == path.repo));
3796        Ok(if options.purge {
3797            DeleteOutcome::Purged { bytes_reclaimed: 0 }
3798        } else {
3799            DeleteOutcome::Tombstoned {
3800                purge_after_unix_ms: now_ms()
3801                    + i64::from(options.retention_days.unwrap_or(30)) * 86_400_000,
3802                bytes_kept: 0,
3803            }
3804        })
3805    }
3806
3807    fn set_default_branch(&self, path: &RepoPath, default_branch: &str) -> ControlResult<Repo> {
3808        if default_branch.is_empty() {
3809            return Err(ControlError::InvalidArgument(
3810                "default_branch is empty".into(),
3811            ));
3812        }
3813        let mut inner = self.inner.lock().unwrap();
3814        let repo = inner
3815            .repos
3816            .get_mut(&MemoryControlPlane::key(path))
3817            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
3818        repo.default_branch = default_branch.to_string();
3819        Ok(repo.clone())
3820    }
3821
3822    fn set_visibility(&self, path: &RepoPath, visibility: Visibility) -> ControlResult<Repo> {
3823        if visibility == Visibility::Unspecified {
3824            return Err(ControlError::InvalidArgument(
3825                "visibility unspecified".into(),
3826            ));
3827        }
3828        let mut inner = self.inner.lock().unwrap();
3829        let repo = inner
3830            .repos
3831            .get_mut(&MemoryControlPlane::key(path))
3832            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
3833        repo.visibility = visibility as i32;
3834        Ok(repo.clone())
3835    }
3836
3837    fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo> {
3838        let mut inner = self.inner.lock().unwrap();
3839        let repo = inner
3840            .repos
3841            .get_mut(&MemoryControlPlane::key(path))
3842            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
3843        repo.encrypted = encrypted;
3844        Ok(repo.clone())
3845    }
3846
3847    fn list_refs(
3848        &self,
3849        path: &RepoPath,
3850        _prefix: &str,
3851        _peel: bool,
3852    ) -> ControlResult<Vec<RefTarget>> {
3853        self.get_repo(path)?;
3854        Err(ControlError::Unimplemented("Refs.List"))
3855    }
3856
3857    fn get_ref(&self, path: &RepoPath, _ref_name: &str, _peel: bool) -> ControlResult<RefTarget> {
3858        self.get_repo(path)?;
3859        Err(ControlError::Unimplemented("Refs.Get"))
3860    }
3861
3862    fn log(
3863        &self,
3864        path: &RepoPath,
3865        _start: &str,
3866        _limit: u32,
3867        _touching: &str,
3868    ) -> ControlResult<Vec<Commit>> {
3869        self.get_repo(path)?;
3870        Err(ControlError::Unimplemented("Refs.Log"))
3871    }
3872
3873    fn create_pull(
3874        &self,
3875        path: &RepoPath,
3876        _author: &str,
3877        _title: &str,
3878        _description: &str,
3879        _source_ref: &str,
3880        _target_ref: &str,
3881    ) -> ControlResult<PullRequest> {
3882        self.get_repo(path)?;
3883        Err(ControlError::Unimplemented("PullRequests.Create"))
3884    }
3885
3886    fn list_pulls(&self, path: &RepoPath, _state: i32) -> ControlResult<Vec<PullRequest>> {
3887        self.get_repo(path)?;
3888        Err(ControlError::Unimplemented("PullRequests.List"))
3889    }
3890
3891    fn get_pull(&self, path: &RepoPath, _number: u64) -> ControlResult<PullRequest> {
3892        self.get_repo(path)?;
3893        Err(ControlError::Unimplemented("PullRequests.Get"))
3894    }
3895
3896    fn close_pull(&self, path: &RepoPath, _number: u64) -> ControlResult<PullRequest> {
3897        self.get_repo(path)?;
3898        Err(ControlError::Unimplemented("PullRequests.Close"))
3899    }
3900
3901    fn merge_pull(
3902        &self,
3903        path: &RepoPath,
3904        _number: u64,
3905        _principal_id: &str,
3906    ) -> ControlResult<(PullRequest, MergedRef)> {
3907        self.get_repo(path)?;
3908        Err(ControlError::Unimplemented("PullRequests.Merge"))
3909    }
3910
3911    fn read_object(
3912        &self,
3913        path: &RepoPath,
3914        _oid: &str,
3915        _max_bytes: u64,
3916    ) -> ControlResult<ReadObjectResponse> {
3917        self.get_repo(path)?;
3918        Err(ControlError::Unimplemented("Refs.ReadObject"))
3919    }
3920
3921    fn diff(
3922        &self,
3923        path: &RepoPath,
3924        _from: &str,
3925        _to: &str,
3926        _path_prefixes: &[String],
3927    ) -> ControlResult<Vec<FileChange>> {
3928        self.get_repo(path)?;
3929        Err(ControlError::Unimplemented("Refs.Diff"))
3930    }
3931
3932    fn add_principal(&self, id: &str, name: &str, kind: PrincipalKind) -> ControlResult<Principal> {
3933        if name.is_empty() || id.is_empty() {
3934            return Err(ControlError::InvalidArgument(
3935                "principal name and id are required".into(),
3936            ));
3937        }
3938        let mut inner = self.inner.lock().unwrap();
3939        if inner.principals.contains_key(id) {
3940            return Err(ControlError::AlreadyExists(format!("principal {id}")));
3941        }
3942        // The same rule as the real table: a name is taken by another name
3943        // AND by another id.
3944        if let Some(holder) = Inner::name_holder(&inner, name, id) {
3945            return Err(ControlError::AlreadyExists(format!(
3946                "the name \"{name}\" is already taken by principal {holder}"
3947            )));
3948        }
3949        let principal = Principal {
3950            id: id.to_string(),
3951            name: name.to_string(),
3952            kind: kind as i32,
3953            created_at_unix_ms: now_ms(),
3954        };
3955        inner.principals.insert(id.to_string(), principal.clone());
3956        Ok(principal)
3957    }
3958
3959    fn enroll_employee(&self, account: &str, ask: &EmployeeAsk) -> ControlResult<EmployeeAnswer> {
3960        let named = ask.seats == 0;
3961        if named && (ask.name.is_empty() || ask.id.is_empty()) {
3962            return Err(ControlError::InvalidArgument(
3963                "an employee's id and name are required".into(),
3964            ));
3965        }
3966        let mut inner = self.inner.lock().unwrap();
3967        if !inner.namespaces.contains_key(account) {
3968            return Err(ControlError::NotFound(format!("account {account}")));
3969        }
3970        let open = |lots: &[(u64, u32, Vec<String>, String)]| -> u32 {
3971            lots.iter()
3972                .map(|(_, total, filled, _)| total.saturating_sub(filled.len() as u32))
3973                .sum()
3974        };
3975        let lots = inner.seats.get(account).cloned().unwrap_or_default();
3976        let total: u32 = lots.iter().map(|(_, total, _, _)| *total).sum();
3977        if !ask.order.is_empty() {
3978            if let Some((owner, principal, _, room, need)) = inner.room_orders.get(&ask.order).cloned() {
3979                if owner != account {
3980                    return Err(ControlError::InvalidArgument(format!(
3981                        "order {} was granted to {owner}, not to {account}",
3982                        ask.order
3983                    )));
3984                }
3985                return Ok(EmployeeAnswer {
3986                    principal: inner.principals.get(&principal).cloned(),
3987                    room_bytes: room,
3988                    seats: total,
3989                    seats_open: open(&lots),
3990                    need_bytes: need,
3991                    unused_bytes: None,
3992                    shortfall_bytes: 0,
3993                    unchanged: true,
3994                });
3995            }
3996        }
3997        if named {
3998            if ask.id == account {
3999                return Err(ControlError::InvalidArgument(format!(
4000                    "{account} is the account itself, not an employee of it"
4001                )));
4002            }
4003            if inner.principals.contains_key(&ask.id)
4004                || inner.principals.values().any(|p| p.name == ask.name)
4005            {
4006                return Err(ControlError::AlreadyExists(format!(
4007                    "principal {} already exists, and an existing principal can never become \
4008                     an employee",
4009                    ask.id
4010                )));
4011            }
4012            if inner.namespaces.contains_key(&ask.id) {
4013                return Err(ControlError::AlreadyExists(format!(
4014                    "namespace {} exists, so an employee may not be called that",
4015                    ask.id
4016                )));
4017            }
4018        }
4019        let lot = if ask.from_seat {
4020            Some(
4021                lots.iter()
4022                    .position(|(_, total, filled, _)| (filled.len() as u32) < *total)
4023                    .ok_or_else(|| {
4024                        ControlError::FailedPrecondition(format!(
4025                            "{account} has no open seat: every seat it holds is filled. Its \
4026                             admin adds users first"
4027                        ))
4028                    })?,
4029            )
4030        } else {
4031            None
4032        };
4033        let room_bytes = lot.map_or(ask.room_bytes, |lot| lots[lot].0);
4034        let need_bytes = match (lot, ask.seats) {
4035            (Some(_), _) => 0,
4036            (None, 0) => room_bytes,
4037            (None, seats) => room_bytes.saturating_mul(u64::from(seats)),
4038        };
4039        let committed = ask.cap.map(|_| {
4040            let promised: u64 = lots
4041                .iter()
4042                .map(|(room, total, filled, _)| room * u64::from(total.saturating_sub(filled.len() as u32)))
4043                .sum::<u64>()
4044                + inner
4045                    .employers
4046                    .iter()
4047                    .filter(|(_, employer)| *employer == account)
4048                    .filter_map(|(id, _)| inner.rooms.get(id).map(|(room, _)| *room))
4049                    .sum::<u64>();
4050            promised.saturating_add(ask.stored)
4051        });
4052        let unused_bytes = ask.cap.zip(committed).map(|(cap, committed)| cap.saturating_sub(committed));
4053        let shortfall_bytes = ask
4054            .cap
4055            .zip(committed)
4056            .map_or(0, |(cap, committed)| need_bytes.saturating_add(committed).saturating_sub(cap));
4057        let mut answer = EmployeeAnswer {
4058            principal: None,
4059            room_bytes,
4060            seats: total,
4061            seats_open: open(&lots),
4062            need_bytes,
4063            unused_bytes,
4064            shortfall_bytes,
4065            unchanged: ask.dry_run || shortfall_bytes > 0,
4066        };
4067        if answer.unchanged {
4068            return Ok(answer);
4069        }
4070        let mut lots = lots;
4071        if named {
4072            let principal = Principal {
4073                id: ask.id.clone(),
4074                name: ask.name.clone(),
4075                kind: PrincipalKind::User as i32,
4076                created_at_unix_ms: now_ms(),
4077            };
4078            inner.principals.insert(ask.id.clone(), principal.clone());
4079            inner.employers.insert(ask.id.clone(), account.to_owned());
4080            inner.grants.push(Grant {
4081                principal_id: ask.id.clone(),
4082                namespace: account.to_owned(),
4083                repo: String::new(),
4084                level: GrantLevel::Read as i32,
4085                ..Default::default()
4086            });
4087            let order = match lot {
4088                Some(lot) => {
4089                    lots[lot].2.push(ask.id.clone());
4090                    lots[lot].3.clone()
4091                }
4092                None => ask.order.clone(),
4093            };
4094            if room_bytes > 0 {
4095                inner.rooms.insert(ask.id.clone(), (room_bytes, order));
4096            }
4097            answer.principal = Some(principal);
4098        } else {
4099            lots.push((room_bytes, ask.seats, Vec::new(), ask.order.clone()));
4100        }
4101        answer.seats = lots.iter().map(|(_, total, _, _)| *total).sum();
4102        answer.seats_open = open(&lots);
4103        if !lots.is_empty() {
4104            inner.seats.insert(account.to_owned(), lots);
4105        }
4106        if !ask.order.is_empty() {
4107            inner.room_orders.insert(
4108                ask.order.clone(),
4109                (
4110                    account.to_owned(),
4111                    if named { ask.id.clone() } else { String::new() },
4112                    ask.seats,
4113                    room_bytes,
4114                    need_bytes,
4115                ),
4116            );
4117        }
4118        Ok(answer)
4119    }
4120
4121    fn seats(&self, account: &str) -> ControlResult<(u32, u32)> {
4122        let inner = self.inner.lock().unwrap();
4123        let lots = inner.seats.get(account).cloned().unwrap_or_default();
4124        Ok((
4125            lots.iter().map(|(_, total, _, _)| *total).sum(),
4126            lots.iter()
4127                .map(|(_, total, filled, _)| total.saturating_sub(filled.len() as u32))
4128                .sum(),
4129        ))
4130    }
4131
4132    fn employees(&self, account: &str) -> ControlResult<Vec<Principal>> {
4133        let inner = self.inner.lock().unwrap();
4134        Ok(inner
4135            .employers
4136            .iter()
4137            .filter(|(_, employer)| *employer == account)
4138            .filter_map(|(id, _)| inner.principals.get(id).cloned())
4139            .collect())
4140    }
4141
4142    /// The double keeps no feed: the operator is the ASKED name, by the same
4143    /// [`operator_name`] rule (a plane-level test of the fed half lives where
4144    /// the feed is, in `gunnar-server`).
4145    /// The double keeps the same rule as the shipped plane: the operator must
4146    /// be a person; once he holds a passkey the bootstrap is spent (and a
4147    /// leftover one withdrawn); otherwise the invite is (re)recorded.
4148    fn operator_bootstrap_invite(
4149        &self,
4150        operator: &str,
4151        secret_sha256: &str,
4152        now_unix_ms: i64,
4153        ttl_ms: i64,
4154    ) -> ControlResult<BootstrapInvite> {
4155        let hash = bootstrap_hash(secret_sha256)?;
4156        let ttl_ms = bootstrap_ttl(ttl_ms)?;
4157        let mut inner = self.inner.lock().unwrap();
4158        let principal = inner
4159            .principals
4160            .get(operator)
4161            .cloned()
4162            .ok_or_else(|| ControlError::NotFound(format!("principal {operator}")))?;
4163        if principal.kind != PrincipalKind::User as i32 {
4164            return Err(ControlError::FailedPrecondition(format!(
4165                "principal {operator} is not a person: the operator's first invite is a person's"
4166            )));
4167        }
4168        if inner
4169            .credentials
4170            .values()
4171            .any(|stored| stored.credential.principal_id == operator)
4172        {
4173            inner.invites.remove(OPERATOR_INVITE_ID);
4174            return Ok(BootstrapInvite::Spent);
4175        }
4176        let renewed = inner
4177            .invites
4178            .get(OPERATOR_INVITE_ID)
4179            .is_some_and(|(row, held)| *held == hash && row.principal_id == operator);
4180        let row = gunnar_proto::Invite {
4181            invite_id: OPERATOR_INVITE_ID.to_owned(),
4182            principal_id: operator.to_owned(),
4183            created_by: "installer".to_owned(),
4184            created_at_unix_ms: now_unix_ms,
4185            expires_at_unix_ms: now_unix_ms + ttl_ms,
4186            note: "the operator's first passkey: the installer's first invite".to_owned(),
4187            server_level: GrantLevel::Unspecified as i32,
4188        };
4189        inner.invites.insert(OPERATOR_INVITE_ID.to_owned(), (row, hash));
4190        Ok(if renewed { BootstrapInvite::Renewed } else { BootstrapInvite::Recorded })
4191    }
4192    fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim> {
4193        let (name, named_by) = operator_name(None, asked)?;
4194        let held = {
4195            let inner = self.inner.lock().unwrap();
4196            inner
4197                .principals
4198                .get(name)
4199                .cloned()
4200                .or_else(|| inner.principals.values().find(|p| p.name == name).cloned())
4201        };
4202        let (principal, created) = match held {
4203            Some(p) if p.kind != PrincipalKind::User as i32 => {
4204                return Err(ControlError::FailedPrecondition(format!(
4205                    "the operator's name {name:?} is held by a principal that is not a person"
4206                )))
4207            }
4208            Some(p) => (p, false),
4209            None => (self.add_principal(name, name, PrincipalKind::User)?, true),
4210        };
4211        let mut inner = self.inner.lock().unwrap();
4212        let adopted_namespace = created && inner.namespaces.contains_key(&principal.id);
4213        if !inner.namespaces.contains_key(&principal.id) {
4214            inner.namespaces.insert(
4215                principal.id.clone(),
4216                Namespace {
4217                    name: principal.id.clone(),
4218                    created_at_unix_ms: now_ms(),
4219                    ..Default::default()
4220                },
4221            );
4222        }
4223        inner
4224            .server_grants
4225            .insert(principal.id.clone(), GrantLevel::Admin);
4226        Ok(OperatorClaim {
4227            principal,
4228            created,
4229            adopted_namespace,
4230            named_by,
4231        })
4232    }
4233
4234    fn list_principals(&self) -> ControlResult<Vec<Principal>> {
4235        Ok(self
4236            .inner
4237            .lock()
4238            .unwrap()
4239            .principals
4240            .values()
4241            .cloned()
4242            .collect())
4243    }
4244
4245    fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>> {
4246        Ok(self
4247            .inner
4248            .lock()
4249            .unwrap()
4250            .principals
4251            .values()
4252            .find(|p| p.name == name)
4253            .cloned())
4254    }
4255
4256    fn add_credential(
4257        &self,
4258        principal_id: &str,
4259        credential_id: &[u8],
4260        public_key: &[u8],
4261        label: &str,
4262        sign_count: u32,
4263    ) -> ControlResult<gunnar_proto::WebCredential> {
4264        let mut inner = self.inner.lock().unwrap();
4265        let principal = inner
4266            .principals
4267            .get(principal_id)
4268            .cloned()
4269            .ok_or_else(|| ControlError::NotFound(format!("principal {principal_id}")))?;
4270        if principal.kind != PrincipalKind::User as i32 {
4271            return Err(ControlError::FailedPrecondition(format!(
4272                "principal {principal_id} is not a person: no human credential on a machine principal"
4273            )));
4274        }
4275        let key = crate::webauthn::b64url(credential_id);
4276        if inner.credentials.contains_key(&key) {
4277            return Err(ControlError::AlreadyExists(format!("web credential {key}")));
4278        }
4279        let credential = gunnar_proto::WebCredential {
4280            credential_id: credential_id.to_vec(),
4281            principal_id: principal_id.to_owned(),
4282            label: label.to_owned(),
4283            created_at_unix_ms: now_ms(),
4284            last_used_unix_ms: None,
4285            sign_count,
4286        };
4287        inner.credentials.insert(
4288            key,
4289            StoredCredential {
4290                credential: credential.clone(),
4291                public_key: public_key.to_vec(),
4292            },
4293        );
4294        Ok(credential)
4295    }
4296
4297    fn credentials(&self, principal_id: Option<&str>) -> ControlResult<Vec<gunnar_proto::WebCredential>> {
4298        Ok(self
4299            .inner
4300            .lock()
4301            .unwrap()
4302            .credentials
4303            .values()
4304            .filter(|c| principal_id.is_none_or(|want| c.credential.principal_id == want))
4305            .map(|c| c.credential.clone())
4306            .collect())
4307    }
4308
4309    fn credential(&self, credential_id: &[u8]) -> ControlResult<Option<StoredCredential>> {
4310        Ok(self
4311            .inner
4312            .lock()
4313            .unwrap()
4314            .credentials
4315            .get(&crate::webauthn::b64url(credential_id))
4316            .cloned())
4317    }
4318
4319    fn revoke_credential(&self, credential_id: &[u8]) -> ControlResult<Option<gunnar_proto::WebCredential>> {
4320        let key = crate::webauthn::b64url(credential_id);
4321        let mut inner = self.inner.lock().unwrap();
4322        let removed = inner.credentials.remove(&key).map(|c| c.credential);
4323        // Its session keys go with it.
4324        for fingerprint in inner.credential_sessions.remove(&key).unwrap_or_default() {
4325            inner.keys.remove(&fingerprint);
4326            inner.openssh.remove(&fingerprint);
4327        }
4328        Ok(removed)
4329    }
4330
4331    fn rename_credential(
4332        &self,
4333        credential_id: &[u8],
4334        label: &str,
4335    ) -> ControlResult<Option<gunnar_proto::WebCredential>> {
4336        let mut inner = self.inner.lock().unwrap();
4337        Ok(inner
4338            .credentials
4339            .get_mut(&crate::webauthn::b64url(credential_id))
4340            .map(|stored| {
4341                stored.credential.label = label.to_owned();
4342                stored.credential.clone()
4343            }))
4344    }
4345
4346    fn create_invite(&self, invite: NewInvite) -> ControlResult<gunnar_proto::Invite> {
4347        let mut inner = self.inner.lock().unwrap();
4348        let principal = inner
4349            .principals
4350            .get(&invite.principal_id)
4351            .cloned()
4352            .ok_or_else(|| ControlError::NotFound(format!("principal {}", invite.principal_id)))?;
4353        if principal.kind != PrincipalKind::User as i32 {
4354            return Err(ControlError::FailedPrecondition(format!(
4355                "principal {} is not a person: no human credential on a machine principal",
4356                invite.principal_id
4357            )));
4358        }
4359        if invite.server_level == GrantLevel::Admin {
4360            return Err(ControlError::InvalidArgument(crate::service::access::NO_ADMIN_BY_INVITE.to_owned()));
4361        }
4362        if inner.invites.contains_key(&invite.id) {
4363            return Err(ControlError::AlreadyExists(format!("invite {}", invite.id)));
4364        }
4365        let row = gunnar_proto::Invite {
4366            invite_id: invite.id.clone(),
4367            principal_id: invite.principal_id,
4368            created_by: invite.created_by,
4369            created_at_unix_ms: invite.created_at_unix_ms,
4370            expires_at_unix_ms: invite.expires_at_unix_ms,
4371            note: invite.note,
4372            server_level: invite.server_level as i32,
4373        };
4374        inner.invites.insert(invite.id, (row.clone(), invite.secret_hash));
4375        Ok(row)
4376    }
4377
4378    fn invites(&self) -> ControlResult<Vec<gunnar_proto::Invite>> {
4379        let mut rows: Vec<gunnar_proto::Invite> =
4380            self.inner.lock().unwrap().invites.values().map(|(row, _)| row.clone()).collect();
4381        rows.sort_by(|a, b| (a.created_at_unix_ms, &a.invite_id).cmp(&(b.created_at_unix_ms, &b.invite_id)));
4382        Ok(rows)
4383    }
4384
4385    fn revoke_invite(&self, id: &str) -> ControlResult<Option<gunnar_proto::Invite>> {
4386        Ok(self.inner.lock().unwrap().invites.remove(id).map(|(row, _)| row))
4387    }
4388
4389    fn check_invite(&self, secret_hash: &str, now_unix_ms: i64) -> ControlResult<InviteCheck> {
4390        let inner = self.inner.lock().unwrap();
4391        if let Some((row, _)) = inner.invites.values().find(|(_, held)| held == secret_hash) {
4392            return Ok(if row.expires_at_unix_ms > now_unix_ms { InviteCheck::Valid(row.clone()) } else { InviteCheck::Expired });
4393        }
4394        Ok(if inner.redeemed_invites.contains(secret_hash) { InviteCheck::Used } else { InviteCheck::Unknown })
4395    }
4396
4397    fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool> {
4398        Ok(self.inner.lock().unwrap().server_grants.contains_key(principal_id))
4399    }
4400
4401    fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>> {
4402        Ok(self.inner.lock().unwrap().employers.get(principal_id).cloned())
4403    }
4404
4405    fn web_login(&self, commit: WebLoginCommit) -> ControlResult<WebLoginOutcome> {
4406        let key = crate::webauthn::b64url(&commit.credential_id);
4407        let mut fields = commit.session_openssh.split_whitespace();
4408        let algo = fields.next().unwrap_or_default().to_owned();
4409        let fingerprint = fingerprint_of_openssh(&commit.session_openssh).ok_or_else(|| {
4410            ControlError::InvalidArgument("the session key is not an OpenSSH public key".into())
4411        })?;
4412        let mut inner = self.inner.lock().unwrap();
4413        let mut stored = inner
4414            .credentials
4415            .get(&key)
4416            .cloned()
4417            .ok_or_else(|| ControlError::NotFound(format!("web credential {key}")))?;
4418        let principal = inner
4419            .principals
4420            .get(&stored.credential.principal_id)
4421            .cloned()
4422            .ok_or_else(|| ControlError::NotFound(format!("principal {}", stored.credential.principal_id)))?;
4423        let held = stored.credential.sign_count;
4424        if held != 0 && commit.sign_count != 0 && commit.sign_count <= held {
4425            return Err(ControlError::PermissionDenied(format!(
4426                "the signature counter of web credential {key} went from {held} to {}: a cloned \
4427                 authenticator, or a replay",
4428                commit.sign_count
4429            )));
4430        }
4431        if inner.keys.contains_key(&fingerprint) {
4432            return Err(ControlError::AlreadyExists(format!("key {fingerprint}")));
4433        }
4434        if commit.sign_count != 0 {
4435            stored.credential.sign_count = commit.sign_count;
4436        }
4437        stored.credential.last_used_unix_ms = Some(commit.now_unix_ms);
4438        inner.credentials.insert(key.clone(), stored.clone());
4439        inner
4440            .credential_sessions
4441            .entry(key)
4442            .or_default()
4443            .push(fingerprint.clone());
4444        inner
4445            .openssh
4446            .insert(fingerprint.clone(), commit.session_openssh.trim().to_owned());
4447        inner.keys.insert(
4448            fingerprint.clone(),
4449            PublicKey {
4450                fingerprint: fingerprint.clone(),
4451                algo,
4452                principal_id: principal.id.clone(),
4453                comment: commit.comment,
4454                added_at_unix_ms: commit.now_unix_ms,
4455                last_used_unix_ms: None,
4456                expires_at_unix_ms: Some(commit.expires_at_unix_ms),
4457            },
4458        );
4459        Ok(WebLoginOutcome {
4460            principal,
4461            credential: stored.credential,
4462            fingerprint,
4463        })
4464    }
4465
4466    fn web_register(&self, registration: WebRegistration) -> ControlResult<WebRegistered> {
4467        const INVALID: &str =
4468            "this invite is not valid: it is unknown, already used, expired, or for somebody else";
4469        let mut inner = self.inner.lock().unwrap();
4470        let (target, level, redeemed) = match &registration.invite_secret_hash {
4471            Some(hash) => {
4472                let (row, _) = inner
4473                    .invites
4474                    .values()
4475                    .find(|(row, held)| held == hash && row.expires_at_unix_ms > registration.now_unix_ms)
4476                    .cloned()
4477                    .ok_or_else(|| ControlError::PermissionDenied(INVALID.into()))?;
4478                if registration
4479                    .principal_id
4480                    .as_deref()
4481                    .is_some_and(|asked| asked != row.principal_id)
4482                {
4483                    return Err(ControlError::PermissionDenied(INVALID.into()));
4484                }
4485                (
4486                    row.principal_id.clone(),
4487                    GrantLevel::try_from(row.server_level).unwrap_or(GrantLevel::Unspecified),
4488                    Some(row.invite_id),
4489                )
4490            }
4491            None => (
4492                registration.principal_id.clone().ok_or_else(|| {
4493                    ControlError::InvalidArgument("name the principal, or redeem an invite".into())
4494                })?,
4495                GrantLevel::Unspecified,
4496                None,
4497            ),
4498        };
4499        let principal = inner
4500            .principals
4501            .get(&target)
4502            .cloned()
4503            .ok_or_else(|| ControlError::NotFound(format!("principal {target}")))?;
4504        if principal.kind != PrincipalKind::User as i32 {
4505            return Err(ControlError::FailedPrecondition(format!(
4506                "principal {target} is not a person: no human credential on a machine principal"
4507            )));
4508        }
4509        let key = crate::webauthn::b64url(&registration.credential_id);
4510        if inner.credentials.contains_key(&key) {
4511            return Err(ControlError::AlreadyExists(format!("web credential {key}")));
4512        }
4513        let credential = gunnar_proto::WebCredential {
4514            credential_id: registration.credential_id.clone(),
4515            principal_id: target.clone(),
4516            label: registration.label,
4517            created_at_unix_ms: registration.now_unix_ms,
4518            last_used_unix_ms: None,
4519            sign_count: registration.sign_count,
4520        };
4521        inner.credentials.insert(
4522            key,
4523            StoredCredential {
4524                credential: credential.clone(),
4525                public_key: registration.public_key,
4526            },
4527        );
4528        if let Some(id) = redeemed {
4529            inner.invites.remove(&id);
4530            if let Some(hash) = registration.invite_secret_hash.clone() {
4531                inner.redeemed_invites.insert(hash);
4532            }
4533        }
4534        if level != GrantLevel::Unspecified {
4535            inner.server_grants.insert(target, level);
4536        }
4537        Ok(WebRegistered {
4538            credential,
4539            principal,
4540            server_level: level,
4541        })
4542    }
4543
4544    /// The double keeps the shipped plane's rule, in one lock: the machine
4545    /// made or found (a person under the id refused), the grant, the
4546    /// credential, the approver's counter.
4547    fn approve_machine_enrolment(&self, approval: MachineApproval) -> ControlResult<MachineApproved> {
4548        let mut inner = self.inner.lock().unwrap();
4549        let key = crate::webauthn::b64url(&approval.credential_id);
4550        if inner.credentials.contains_key(&key) {
4551            return Err(ControlError::AlreadyExists(format!("web credential {key}")));
4552        }
4553        let mut step_up = None;
4554        if let Some((id, presented)) = &approval.step_up {
4555            let held = crate::webauthn::b64url(id);
4556            let stored = inner
4557                .credentials
4558                .get(&held)
4559                .cloned()
4560                .ok_or_else(|| ControlError::NotFound(format!("web credential {held}")))?;
4561            let count = stored.credential.sign_count;
4562            if count != 0 && *presented != 0 && *presented <= count {
4563                return Err(ControlError::PermissionDenied(format!(
4564                    "the signature counter of web credential {held} went from {count} to {presented}: \
4565                     a cloned authenticator, or a replay"
4566                )));
4567            }
4568            step_up = Some((held, stored, *presented));
4569        }
4570        let (principal, created) = match inner.principals.get(&approval.machine).cloned() {
4571            Some(p) if p.kind != PrincipalKind::Machine as i32 => {
4572                return Err(ControlError::FailedPrecondition(format!(
4573                    "principal {} is not a machine: a machine's credential never lands on a person",
4574                    approval.machine
4575                )))
4576            }
4577            Some(p) => (p, false),
4578            None => {
4579                if let Some(holder) = Inner::name_holder(&inner, &approval.machine, &approval.machine) {
4580                    return Err(ControlError::AlreadyExists(format!(
4581                        "the name \"{}\" is already taken by principal {holder}",
4582                        approval.machine
4583                    )));
4584                }
4585                let p = Principal {
4586                    id: approval.machine.clone(),
4587                    name: approval.machine.clone(),
4588                    kind: PrincipalKind::Machine as i32,
4589                    created_at_unix_ms: approval.now_unix_ms,
4590                };
4591                inner.principals.insert(p.id.clone(), p.clone());
4592                (p, true)
4593            }
4594        };
4595        if let Some((held, mut stored, presented)) = step_up {
4596            if presented != 0 {
4597                stored.credential.sign_count = presented;
4598            }
4599            stored.credential.last_used_unix_ms = Some(approval.now_unix_ms);
4600            inner.credentials.insert(held, stored);
4601        }
4602        inner
4603            .server_grants
4604            .insert(principal.id.clone(), approval.server_level);
4605        let credential = gunnar_proto::WebCredential {
4606            credential_id: approval.credential_id.clone(),
4607            principal_id: principal.id.clone(),
4608            label: approval.label,
4609            created_at_unix_ms: approval.now_unix_ms,
4610            last_used_unix_ms: None,
4611            sign_count: approval.sign_count,
4612        };
4613        inner.credentials.insert(
4614            key,
4615            StoredCredential {
4616                credential: credential.clone(),
4617                public_key: approval.public_key,
4618            },
4619        );
4620        Ok(MachineApproved {
4621            principal,
4622            credential,
4623            created,
4624        })
4625    }
4626
4627    fn rename_principal(&self, principal_id: &str, name: &str) -> ControlResult<Principal> {
4628        if name.is_empty() {
4629            return Err(ControlError::InvalidArgument("the new name is empty".into()));
4630        }
4631        let mut inner = self.inner.lock().unwrap();
4632        if !inner.principals.contains_key(principal_id) {
4633            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4634        }
4635        if let Some(holder) = Inner::name_holder(&inner, name, principal_id) {
4636            return Err(ControlError::AlreadyExists(format!(
4637                "the name \"{name}\" is already taken by principal {holder}"
4638            )));
4639        }
4640        let row = inner.principals.get_mut(principal_id).expect("checked above");
4641        row.name = name.to_string();
4642        Ok(row.clone())
4643    }
4644
4645    fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)> {
4646        let mut inner = self.inner.lock().unwrap();
4647        inner.employers.remove(principal_id);
4648        if inner.principals.remove(principal_id).is_none() {
4649            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4650        }
4651        let before_keys = inner.keys.len();
4652        inner.keys.retain(|_, k| k.principal_id != principal_id);
4653        let live: Vec<String> = inner.keys.keys().cloned().collect();
4654        inner.openssh.retain(|fp, _| live.contains(fp));
4655        let before_grants = inner.grants.len();
4656        inner.grants.retain(|g| g.principal_id != principal_id);
4657        inner.server_grants.remove(principal_id);
4658        inner
4659            .credentials
4660            .retain(|_, c| c.credential.principal_id != principal_id);
4661        inner
4662            .invites
4663            .retain(|_, (row, _)| row.principal_id != principal_id);
4664        Ok((
4665            (before_keys - inner.keys.len()) as u32,
4666            (before_grants - inner.grants.len()) as u32,
4667        ))
4668    }
4669
4670    fn add_key(
4671        &self,
4672        principal_id: &str,
4673        openssh_public_key: &str,
4674        comment: &str,
4675        expires_at_unix_ms: Option<i64>,
4676        caller: &str,
4677        attestation: Option<SignedKeyAttestation>,
4678    ) -> ControlResult<PublicKey> {
4679        let mut fields = openssh_public_key.split_whitespace();
4680        let (Some(algo), Some(_blob)) = (fields.next(), fields.next()) else {
4681            return Err(ControlError::InvalidArgument(
4682                "not an OpenSSH public key line: expected `<algo> <base64> [comment]`".into(),
4683            ));
4684        };
4685        if !(algo.starts_with("ssh-") || algo.starts_with("ecdsa-") || algo.starts_with("sk-")) {
4686            return Err(ControlError::InvalidArgument(format!(
4687                "unrecognised public key algorithm {algo}"
4688            )));
4689        }
4690        let mut inner = self.inner.lock().unwrap();
4691        // Principal first, then key material: "there is no such principal" is
4692        // the more useful answer, and the ordering is pinned by
4693        // `access_rejects_the_things_that_would_break_the_ssh_side`.
4694        if !inner.principals.contains_key(principal_id) {
4695            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4696        }
4697        let fingerprint = fingerprint_of_openssh(openssh_public_key).ok_or_else(|| {
4698            ControlError::InvalidArgument(format!(
4699                "the {algo} key does not parse as an OpenSSH public key line; the fingerprint \
4700                 the SSH listener will match against cannot be computed from it"
4701            ))
4702        })?;
4703        if inner.keys.contains_key(&fingerprint) {
4704            return Err(ControlError::AlreadyExists(format!("key {fingerprint}")));
4705        }
4706        let holders: Vec<String> = inner
4707            .keys
4708            .values()
4709            .filter(|key| key.principal_id == principal_id)
4710            .filter(|key| key.expires_at_unix_ms.is_none_or(|expires| now_ms() < expires))
4711            .map(|key| key.fingerprint.clone())
4712            .collect();
4713        require_attestation(principal_id, caller, &holders, &fingerprint, attestation.as_ref())?;
4714        inner
4715            .openssh
4716            .insert(fingerprint.clone(), openssh_public_key.trim().to_owned());
4717        let key = PublicKey {
4718            fingerprint: fingerprint.clone(),
4719            algo: algo.to_string(),
4720            principal_id: principal_id.to_string(),
4721            comment: comment.to_string(),
4722            added_at_unix_ms: now_ms(),
4723            last_used_unix_ms: None,
4724            expires_at_unix_ms,
4725        };
4726        inner.keys.insert(fingerprint, key.clone());
4727        Ok(key)
4728    }
4729
4730    fn revoke_key(&self, fingerprint: &str) -> ControlResult<()> {
4731        let mut inner = self.inner.lock().unwrap();
4732        // Both together, by construction: a key whose material outlived its
4733        // record is a credential that still verifies.
4734        inner.openssh.remove(fingerprint);
4735        inner
4736            .keys
4737            .remove(fingerprint)
4738            .map(|_| ())
4739            .ok_or_else(|| ControlError::NotFound(format!("key {fingerprint}")))
4740    }
4741
4742    fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>> {
4743        Ok(self
4744            .inner
4745            .lock()
4746            .unwrap()
4747            .keys
4748            .values()
4749            .filter(|k| principal_id.is_none_or(|want| want == k.principal_id))
4750            .cloned()
4751            .collect())
4752    }
4753
4754    fn grant(
4755        &self,
4756        principal_id: &str,
4757        namespace: &str,
4758        repo: &str,
4759        level: GrantLevel,
4760    ) -> ControlResult<Grant> {
4761        if level == GrantLevel::Unspecified {
4762            return Err(ControlError::InvalidArgument(
4763                "grant level unspecified".into(),
4764            ));
4765        }
4766        if namespace.is_empty() {
4767            return Err(ControlError::InvalidArgument(
4768                "a grant must name a namespace; multi-tenancy has no ungrouped repos".into(),
4769            ));
4770        }
4771        let mut inner = self.inner.lock().unwrap();
4772        if !inner.principals.contains_key(principal_id) {
4773            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4774        }
4775        if !inner.namespaces.contains_key(namespace) {
4776            return Err(ControlError::NotFound(format!("namespace {namespace}")));
4777        }
4778        if !repo.is_empty()
4779            && !inner
4780                .repos
4781                .contains_key(&(namespace.to_string(), repo.to_string()))
4782        {
4783            return Err(ControlError::NotFound(format!(
4784                "repository {namespace}/{repo}"
4785            )));
4786        }
4787        let grant = Grant {
4788            principal_id: principal_id.to_string(),
4789            namespace: namespace.to_string(),
4790            repo: repo.to_string(),
4791            level: level as i32,
4792            granted_at_unix_ms: now_ms(),
4793        };
4794        inner.grants.retain(|g| {
4795            !(g.principal_id == principal_id && g.namespace == namespace && g.repo == repo)
4796        });
4797        inner.grants.push(grant.clone());
4798        Ok(grant)
4799    }
4800
4801    fn revoke(&self, principal_id: &str, namespace: &str, repo: &str) -> ControlResult<()> {
4802        let mut inner = self.inner.lock().unwrap();
4803        let before = inner.grants.len();
4804        inner.grants.retain(|g| {
4805            !(g.principal_id == principal_id && g.namespace == namespace && g.repo == repo)
4806        });
4807        if inner.grants.len() == before {
4808            return Err(ControlError::NotFound(format!(
4809                "grant for {principal_id} on {namespace}/{repo}"
4810            )));
4811        }
4812        Ok(())
4813    }
4814
4815    fn list_grants(
4816        &self,
4817        principal_id: Option<&str>,
4818        namespace: Option<&str>,
4819        repo: Option<&str>,
4820    ) -> ControlResult<Vec<Grant>> {
4821        Ok(self
4822            .inner
4823            .lock()
4824            .unwrap()
4825            .grants
4826            .iter()
4827            .filter(|g| principal_id.is_none_or(|w| w == g.principal_id))
4828            .filter(|g| namespace.is_none_or(|w| w == g.namespace))
4829            .filter(|g| repo.is_none_or(|w| w == g.repo))
4830            .cloned()
4831            .collect())
4832    }
4833
4834    fn get_protection(
4835        &self,
4836        path: &RepoPath,
4837        ref_pattern: Option<&str>,
4838    ) -> ControlResult<Vec<BranchProtection>> {
4839        self.get_repo(path)?;
4840        let inner = self.inner.lock().unwrap();
4841        let Some(rules) = inner.protections.get(&MemoryControlPlane::key(path)) else {
4842            return Ok(Vec::new());
4843        };
4844        Ok(rules
4845            .values()
4846            .filter(|p| ref_pattern.is_none_or(|w| w == p.ref_pattern))
4847            .cloned()
4848            .collect())
4849    }
4850
4851    fn set_protection(
4852        &self,
4853        path: &RepoPath,
4854        protection: BranchProtection,
4855    ) -> ControlResult<BranchProtection> {
4856        if protection.ref_pattern.is_empty() {
4857            return Err(ControlError::InvalidArgument("ref_pattern is empty".into()));
4858        }
4859        self.get_repo(path)?;
4860        let mut inner = self.inner.lock().unwrap();
4861        inner
4862            .protections
4863            .entry(MemoryControlPlane::key(path))
4864            .or_default()
4865            .insert(protection.ref_pattern.clone(), protection.clone());
4866        Ok(protection)
4867    }
4868
4869    fn delete_protection(&self, path: &RepoPath, ref_pattern: &str) -> ControlResult<()> {
4870        self.get_repo(path)?;
4871        let mut inner = self.inner.lock().unwrap();
4872        let removed = inner
4873            .protections
4874            .get_mut(&MemoryControlPlane::key(path))
4875            .and_then(|rules| rules.remove(ref_pattern));
4876        if removed.is_none() {
4877            return Err(ControlError::NotFound(format!(
4878                "protection {ref_pattern} on {path}"
4879            )));
4880        }
4881        Ok(())
4882    }
4883
4884    fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy> {
4885        self.get_repo(path)?;
4886        Ok(self
4887            .inner
4888            .lock()
4889            .unwrap()
4890            .ref_policies
4891            .get(&MemoryControlPlane::key(path))
4892            .cloned()
4893            .unwrap_or_default())
4894    }
4895
4896    fn set_ref_name_policy(
4897        &self,
4898        path: &RepoPath,
4899        policy: RefNamePolicy,
4900    ) -> ControlResult<RefNamePolicy> {
4901        self.get_repo(path)?;
4902        self.inner
4903            .lock()
4904            .unwrap()
4905            .ref_policies
4906            .insert(MemoryControlPlane::key(path), policy.clone());
4907        Ok(policy)
4908    }
4909
4910    // ── forge ──────────────────────────────────────────────────────────────
4911    //
4912    // **Unimplemented, on purpose.** Verifying an `sshsig` is `gunnar-core`'s
4913    // job and this crate does not depend on it; a memory plane that appended
4914    // without verifying would be a second, weaker version of the one rule that
4915    // makes a review event evidence. `Unimplemented` is the honest answer, and
4916    // it is the value that must not quietly become an empty successful
4917    // response — an empty approval list is indistinguishable from a pull
4918    // request nobody has approved, and one of those may merge.
4919    fn forge_append(&self, _event: SignedForgeEvent) -> ControlResult<u64> {
4920        Err(ControlError::Unimplemented("Forge.Append"))
4921    }
4922
4923    fn forge_review(
4924        &self,
4925        _repo: &str,
4926        _pull: u64,
4927    ) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)> {
4928        Err(ControlError::Unimplemented("Forge.GetReview"))
4929    }
4930
4931    fn forge_reviews(&self, _repo: &str) -> ControlResult<Vec<ReviewState>> {
4932        Err(ControlError::Unimplemented("Forge.ListReviews"))
4933    }
4934
4935    fn forge_issue(
4936        &self,
4937        _repo: &str,
4938        _number: u64,
4939    ) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>> {
4940        Err(ControlError::Unimplemented("Forge.GetIssue"))
4941    }
4942
4943    fn forge_issues(&self, _repo: &str) -> ControlResult<Vec<IssueState>> {
4944        Err(ControlError::Unimplemented("Forge.ListIssues"))
4945    }
4946
4947    fn counts(&self) -> ControlResult<ControlCounts> {
4948        let inner = self.inner.lock().unwrap();
4949        Ok(ControlCounts {
4950            namespaces: inner.namespaces.len() as u64,
4951            repos: inner.repos.len() as u64,
4952            principals: inner.principals.len() as u64,
4953            keys: inner.keys.len() as u64,
4954            grants: inner.grants.len() as u64,
4955        })
4956    }
4957
4958    fn storage_info(&self, path: Option<&RepoPath>) -> ControlResult<StorageInfoResponse> {
4959        let repos = match path {
4960            Some(path) => vec![self.get_repo(path)?],
4961            None => self.list_repos(None)?,
4962        };
4963        Ok(StorageInfoResponse {
4964            // The test double holds no bytes and therefore no tombstones.
4965            total_tombstoned_bytes: 0,
4966            tombstoned: Vec::new(),
4967            // **No machine behind this double, so no storage reality.** `None`
4968            // is the honest answer: a fabricated one here would let a console
4969            // render an installation that does not exist, which is the exact
4970            // failure the real implementation refuses by never inferring.
4971            reality: None,
4972            // Honest: this test double has no store behind it. It must not
4973            // claim "tiered(redb,znippy)" and let a UI report a backend that
4974            // is not there.
4975            object_backend: "none (MemoryControlPlane test double)".into(),
4976            ref_backend: "none (MemoryControlPlane test double)".into(),
4977            // No engine behind a test double: none is named.
4978            engine: None,
4979            total_hot_bytes: 0,
4980            total_cold_bytes: 0,
4981            // No store, so no cold tier measured.
4982            cold_measured: false,
4983            // No store behind this double, so no cache and no budget. Zero is
4984            // what is measured, not a plausible number.
4985            total_explode_bytes: 0,
4986            total_explode_budget_bytes: 0,
4987            repos: repos
4988                .into_iter()
4989                .map(|repo| RepoStorage {
4990                    repo: repo.path.clone(),
4991                    hash_kind: repo.hash_kind,
4992                    ..Default::default()
4993                })
4994                .collect(),
4995        })
4996    }
4997
4998    fn mirror_add(
4999        &self,
5000        path: &RepoPath,
5001        remote_url: &str,
5002        prune: bool,
5003        create_repo: bool,
5004    ) -> ControlResult<MirrorConfig> {
5005        if remote_url.is_empty() {
5006            return Err(ControlError::InvalidArgument("remote_url is empty".into()));
5007        }
5008        if self.get_repo(path).is_err() {
5009            if !create_repo {
5010                return Err(ControlError::NotFound(format!("repository {path}")));
5011            }
5012            self.create_repo(NewRepo {
5013                path: path.clone(),
5014                hash_kind: HashKind::Unspecified,
5015                default_branch: String::new(),
5016                visibility: Visibility::Private,
5017                description: format!("mirror of {remote_url}"),
5018                adopt: false,
5019            })?;
5020        }
5021        let mut inner = self.inner.lock().unwrap();
5022        let key = MemoryControlPlane::key(path);
5023        if inner.mirrors.contains_key(&key) {
5024            return Err(ControlError::AlreadyExists(format!("mirror for {path}")));
5025        }
5026        let mirror = MirrorConfig {
5027            repo: Some(path.clone()),
5028            remote_url: remote_url.to_string(),
5029            prune,
5030            state: MirrorState::Idle as i32,
5031            added_at_unix_ms: now_ms(),
5032            last_sync_at_unix_ms: None,
5033            last_error: String::new(),
5034            sync_count: 0,
5035            credential: MirrorCredentialKind::Unspecified as i32,
5036            credential_username: String::new(),
5037            job: None,
5038            repo_pending: false,
5039            lfs_not_copied: false,
5040        };
5041        inner.mirrors.insert(key, mirror.clone());
5042        Ok(mirror)
5043    }
5044
5045    fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig> {
5046        let inner = self.inner.lock().unwrap();
5047        if !inner.mirrors.contains_key(&MemoryControlPlane::key(path)) {
5048            return Err(ControlError::NotFound(format!("mirror for {path}")));
5049        }
5050        // Enqueuing needs the real queue, the real client and a real store to
5051        // write into. Answering with a job that will never run — or with "0
5052        // refs changed", which is what this used to be built to avoid — would
5053        // be a silent green of exactly the kind that proves nothing.
5054        Err(ControlError::Unimplemented("Mirror.Sync"))
5055    }
5056
5057    /// **The credential is recorded here as a KIND and the value is dropped**,
5058    /// which is not a stub: this plane has no sealed store to put it in, and
5059    /// keeping a private key in a test double's `BTreeMap` is a plaintext
5060    /// secret in a process that had no reason to hold one. What the double owes
5061    /// its callers is the `MirrorConfig` shape, and that it gives exactly.
5062    fn mirror_set_credential(
5063        &self,
5064        path: &RepoPath,
5065        kind: MirrorCredentialKind,
5066        secret: &str,
5067        username: &str,
5068    ) -> ControlResult<MirrorConfig> {
5069        credential_arguments(kind, secret)?;
5070        let mut inner = self.inner.lock().unwrap();
5071        let mirror = inner
5072            .mirrors
5073            .get_mut(&MemoryControlPlane::key(path))
5074            .ok_or_else(|| ControlError::NotFound(format!("mirror for {path}")))?;
5075        mirror.credential = kind as i32;
5076        mirror.credential_username = if kind == MirrorCredentialKind::Token {
5077            username.to_owned()
5078        } else {
5079            String::new()
5080        };
5081        Ok(mirror.clone())
5082    }
5083
5084    fn mirror_status(
5085        &self,
5086        namespace: Option<&str>,
5087        path: Option<&RepoPath>,
5088    ) -> ControlResult<Vec<MirrorConfig>> {
5089        Ok(self
5090            .inner
5091            .lock()
5092            .unwrap()
5093            .mirrors
5094            .iter()
5095            .filter(|((ns, _), _)| namespace.is_none_or(|w| w == ns))
5096            .filter(|((ns, repo), _)| path.is_none_or(|w| w.namespace == *ns && w.repo == *repo))
5097            .map(|(_, mirror)| mirror.clone())
5098            .collect())
5099    }
5100
5101    fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()> {
5102        self.inner
5103            .lock()
5104            .unwrap()
5105            .mirrors
5106            .remove(&MemoryControlPlane::key(path))
5107            .map(|_| ())
5108            .ok_or_else(|| ControlError::NotFound(format!("mirror for {path}")))
5109    }
5110}
5111
5112/// **`UsageBatch.ReadMany`'s page token and page cut** (2026-10-06), shared by
5113/// the default [`ControlPlane::usage_page`] and `gunnar-server`'s ledger.
5114///
5115/// The token is opaque to the caller and carries two things: the CURSOR the
5116/// read began at (so every page of one read answers the same cursor, and a
5117/// change that lands mid-read is answered again next time), and the last
5118/// namespace served (the read resumes after it). `v1;<cursor>;<after>` — a
5119/// namespace name cannot hold `;`, and a token that does not parse starts the
5120/// read over rather than failing it.
5121pub mod usage_page_token {
5122    /// `(cursor, after)` out of a token; `("", "")` for the first page or a
5123    /// token this build did not write.
5124    pub fn decode(token: &str) -> (String, String) {
5125        let mut parts = token.splitn(3, ';');
5126        match (parts.next(), parts.next(), parts.next()) {
5127            (Some("v1"), Some(cursor), Some(after)) => (cursor.to_owned(), after.to_owned()),
5128            _ => (String::new(), String::new()),
5129        }
5130    }
5131
5132    /// The token resuming after `after`, for a read that began at `cursor`.
5133    pub fn encode(cursor: &str, after: &str) -> String {
5134        format!("v1;{cursor};{after}")
5135    }
5136
5137    /// **Cut one page** out of rows SORTED by namespace: the rows after
5138    /// `after`, at most `page_size`, and the token to the next page (empty on
5139    /// the last).
5140    pub fn page(
5141        rows: Vec<gunnar_proto::NamespaceUsage>,
5142        after: &str,
5143        page_size: usize,
5144        cursor: String,
5145        changed_only: bool,
5146    ) -> gunnar_proto::ReadManyUsageResponse {
5147        let page_size = page_size.max(1);
5148        let start = if after.is_empty() { 0 } else { rows.partition_point(|r| r.namespace.as_str() <= after) };
5149        let mut rest: Vec<gunnar_proto::NamespaceUsage> = rows.into_iter().skip(start).collect();
5150        let more = rest.len() > page_size;
5151        rest.truncate(page_size);
5152        let next_page_token = match (more, rest.last()) {
5153            (true, Some(last)) => encode(&cursor, &last.namespace),
5154            _ => String::new(),
5155        };
5156        gunnar_proto::ReadManyUsageResponse {
5157            usages: rest,
5158            next_page_token,
5159            cursor,
5160            changed_only,
5161            as_of_unix_ms: crate::bus::now_unix_ms(),
5162        }
5163    }
5164
5165    #[cfg(test)]
5166    mod tests {
5167        use super::*;
5168
5169        fn row(ns: &str) -> gunnar_proto::NamespaceUsage {
5170            gunnar_proto::NamespaceUsage { namespace: ns.into(), ..Default::default() }
5171        }
5172
5173        #[test]
5174        fn pages_resume_after_the_last_namespace_and_the_last_page_has_no_token() {
5175            let rows = || vec![row("a"), row("b"), row("c"), row("d"), row("e")];
5176            let one = page(rows(), "", 2, "E.1.7".into(), true);
5177            assert_eq!(one.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["a", "b"]);
5178            let (cursor, after) = decode(&one.next_page_token);
5179            assert_eq!((cursor.as_str(), after.as_str()), ("E.1.7", "b"));
5180            let two = page(rows(), &after, 2, cursor.clone(), true);
5181            assert_eq!(two.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["c", "d"]);
5182            let (_, after) = decode(&two.next_page_token);
5183            let three = page(rows(), &after, 2, cursor, true);
5184            assert_eq!(three.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["e"]);
5185            assert!(three.next_page_token.is_empty(), "the last page says so");
5186            assert_eq!(decode("garbage"), (String::new(), String::new()), "a foreign token starts over");
5187        }
5188    }
5189}