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    // ── accounts: the gate, and the per-user hard purge ────────────────────
1387
1388    /// **Lock `account` out, completely** (`Accounts.Gate`).
1389    ///
1390    /// Returns the gate as stored, and whether the account was **already**
1391    /// gated when the request arrived. Idempotent; the newest writer owns the
1392    /// row, because an operator must be able to suspend an account a purge is
1393    /// already holding.
1394    ///
1395    /// The default is [`ControlError::Unimplemented`] and not a silent success:
1396    /// an adapter that cannot lock anybody out must not answer "locked", or
1397    /// [`ControlPlane::purge_account`] would walk an account that is still
1398    /// live and a caller would be told the user was shut out when they were
1399    /// not.
1400    fn gate_account(
1401        &self,
1402        account: &str,
1403        actor: &str,
1404        note: &str,
1405    ) -> ControlResult<(AccountGate, bool)> {
1406        let _ = (account, actor, note);
1407        Err(ControlError::Unimplemented("Accounts.Gate"))
1408    }
1409
1410    /// **Let `account` back in** (`Accounts.Ungate`). Returns what was lifted,
1411    /// or `None` if it was not gated.
1412    ///
1413    /// Unconditional — it does not consult the holder — because this is the
1414    /// operator's override, and an override that could be blocked by a lease
1415    /// belonging to a process that no longer exists is not an override.
1416    fn ungate_account(&self, account: &str) -> ControlResult<Option<AccountGate>> {
1417        let _ = account;
1418        Err(ControlError::Unimplemented("Accounts.Ungate"))
1419    }
1420
1421    /// **Every account locked out right now** (`Accounts.ListAccountGates`).
1422    ///
1423    /// Empty is a legitimate answer; the default is still `Unimplemented`, for
1424    /// the reason [`ControlPlane::list_deleted`]'s is. A plane that cannot read
1425    /// the gate table must not report that nobody is locked out.
1426    fn account_gates(&self) -> ControlResult<Vec<AccountGate>> {
1427        Err(ControlError::Unimplemented("Accounts.ListAccountGates"))
1428    }
1429
1430    /// **Empty an account: gate it NOW, enqueue the walk** (`Accounts.Purge`).
1431    ///
1432    /// Returns when the account is locked out and the job is accepted, **not**
1433    /// when the account is empty. The two halves are deliberately different in
1434    /// timing: the gate is synchronous so there is no window in which a purge
1435    /// has been accepted and the user can still push, and the walk is queued
1436    /// because it is minutes of unlink I/O and an RPC that held a tonic worker
1437    /// for it would make a console button look broken.
1438    ///
1439    /// The returned [`AccountGate`] is the one now in force — which may be an
1440    /// administrative gate that was already there rather than this job's lease.
1441    fn purge_account(
1442        &self,
1443        account: &str,
1444        options: PurgeAccountOptions,
1445    ) -> ControlResult<(PurgeJob, AccountGate)> {
1446        let _ = (account, options);
1447        Err(ControlError::Unimplemented("Accounts.Purge"))
1448    }
1449
1450    /// **How an account's purge is going** (`Accounts.PurgeStatus`), and the
1451    /// lockout as it stands.
1452    ///
1453    /// `Ok((None, None))` is a real answer: no job is being held for this
1454    /// account and it is not gated. A plane that cannot see the queue answers
1455    /// `Unimplemented` instead — "nothing is running" and "I cannot tell" must
1456    /// not look alike.
1457    fn purge_account_status(
1458        &self,
1459        account: &str,
1460    ) -> ControlResult<(Option<PurgeJob>, Option<AccountGate>)> {
1461        let _ = account;
1462        Err(ControlError::Unimplemented("Accounts.PurgeStatus"))
1463    }
1464
1465    /// **Write one repository's archive bytes above `from_generation` into
1466    /// `sink`** (`Repos.ArchiveOut`).
1467    ///
1468    /// The bytes are exactly what `gunnar repo archive-out --to FILE` writes,
1469    /// so what comes off the wire restores with `archive-in` and nothing new
1470    /// has to understand a second format.
1471    ///
1472    /// **Blocking, on purpose.** It reads files and writes a sink; the service
1473    /// runs it on the blocking pool rather than pretending it is async, which
1474    /// is the same shape `replicate_once` already uses for send-pack.
1475    ///
1476    /// The default is [`ControlError::Unimplemented`] and not an empty
1477    /// success: an archive stream that ends immediately looks exactly like a
1478    /// repository with no bytes, and a backup that silently archived nothing is
1479    /// the failure this whole surface exists to prevent.
1480    fn archive_out(
1481        &self,
1482        path: &RepoPath,
1483        from_generation: u64,
1484        sink: &mut dyn std::io::Write,
1485    ) -> ControlResult<ArchiveCut> {
1486        let _ = (path, from_generation, sink);
1487        Err(ControlError::Unimplemented("Repos.ArchiveOut"))
1488    }
1489
1490    // ── refs and objects (the storage seam: RefStore / ObjectStore) ────────
1491    fn list_refs(&self, path: &RepoPath, prefix: &str, peel: bool)
1492        -> ControlResult<Vec<RefTarget>>;
1493    fn get_ref(&self, path: &RepoPath, ref_name: &str, peel: bool) -> ControlResult<RefTarget>;
1494    fn log(
1495        &self,
1496        path: &RepoPath,
1497        start: &str,
1498        limit: u32,
1499        touching: &str,
1500    ) -> ControlResult<Vec<Commit>>;
1501    /// [`ControlPlane::log`] walked from every branch and tag tip (and `HEAD`)
1502    /// at once, newest committer time first, at most `limit` commits.
1503    fn log_all_refs(
1504        &self,
1505        path: &RepoPath,
1506        limit: u32,
1507        touching: &str,
1508    ) -> ControlResult<Vec<Commit>> {
1509        let _ = (path, limit, touching);
1510        Err(ControlError::Unimplemented("Refs.Log all_refs"))
1511    }
1512
1513    // ── pull requests ──────────────────────────────────────────────────────
1514    /// Open a pull request. `author` is the authenticated principal.
1515    fn create_pull(
1516        &self,
1517        path: &RepoPath,
1518        author: &str,
1519        title: &str,
1520        description: &str,
1521        source_ref: &str,
1522        target_ref: &str,
1523    ) -> ControlResult<PullRequest>;
1524    /// Every pull in one repository; `state` zero means all.
1525    fn list_pulls(&self, path: &RepoPath, state: i32) -> ControlResult<Vec<PullRequest>>;
1526    fn get_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>;
1527    fn close_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>;
1528    /// FAST-FORWARD ONLY. Moves the target ref through the store's CAS and
1529    /// returns what moved, so the service can publish the same `RefUpdated`
1530    /// a push would.
1531    ///
1532    /// # Why the caller's identity crosses this seam
1533    ///
1534    /// It did not, until GUN-004. `merge_pull(path, number)` moved a
1535    /// **protected** ref and the principal-dependent half of branch protection
1536    /// — `allowed_principals` — was impossible to evaluate on the far side,
1537    /// because the far side did not know who was asking. So `PullRequests.Merge`
1538    /// read the same policy document the push path reads, took
1539    /// `forbid_self_approval` and `required_approvals` out of it, and wrote the
1540    /// ref with `update_ref`: an explicitly configured per-ref protection,
1541    /// bypassed by a side door, while the push path refused correctly and the
1542    /// protection *looked* like it worked.
1543    ///
1544    /// `principal_id` is `Caller::principal_id`, the same string
1545    /// `ServiceContext::require` just checked a grant for.
1546    fn merge_pull(
1547        &self,
1548        path: &RepoPath,
1549        number: u64,
1550        principal_id: &str,
1551    ) -> ControlResult<(PullRequest, MergedRef)>;
1552    /// **A new repository's first commit, written by the server**
1553    /// (`CreateRepoRequest.readme`): `README.md` holding `readme` on the default
1554    /// branch. Answers the commit id (hex). A control plane that keeps no git
1555    /// store refuses by name.
1556    fn write_first_commit(&self, path: &RepoPath, readme: &str) -> ControlResult<String> {
1557        let _ = readme;
1558        Err(ControlError::FailedPrecondition(format!("this control plane writes no commit into {path}: it keeps no git store")))
1559    }
1560    fn read_object(
1561        &self,
1562        path: &RepoPath,
1563        oid: &str,
1564        max_bytes: u64,
1565    ) -> ControlResult<ReadObjectResponse>;
1566
1567    /// **One LFS object, by the sha256 its pointer names** (`Refs.ReadLfsObject`).
1568    ///
1569    /// A file kept in LFS is a POINTER in the tree, so a caller that reads the
1570    /// tree gets 131 bytes of text where the file should be. This is the way
1571    /// to the bytes for a caller that already holds Read on the repository and
1572    /// speaks gRPC rather than one of `git lfs`'s transfer adapters.
1573    ///
1574    /// Whole object or nothing: a prefix of a git blob is a readable prefix of
1575    /// a file, and a prefix of an LFS object is a broken PDF. Past the ceiling
1576    /// it is refused, and the caller that wants it anyway wants `git lfs`.
1577    ///
1578    /// `range` is `Some((offset, length))` for a ranged read (the request's
1579    /// `offset`/`length`, not both 0): the bytes from `offset`, at most
1580    /// `length` (0 = to the ceiling), the ceiling applied to the range, an
1581    /// offset past the end refused. `None` is the whole-or-refused read.
1582    ///
1583    /// Defaulted to `Unimplemented` so a control plane that has no LFS store —
1584    /// the seams in this crate's tests — needs no stub.
1585    fn read_lfs_object(
1586        &self,
1587        path: &RepoPath,
1588        oid: &str,
1589        max_bytes: u64,
1590        range: Option<(u64, u64)>,
1591    ) -> ControlResult<ReadLfsObjectResponse> {
1592        let _ = (path, oid, max_bytes, range);
1593        Err(ControlError::Unimplemented("Refs.ReadLfsObject"))
1594    }
1595    fn diff(
1596        &self,
1597        path: &RepoPath,
1598        from: &str,
1599        to: &str,
1600        path_prefixes: &[String],
1601    ) -> ControlResult<Vec<FileChange>>;
1602
1603    /// **Blame a file at a revision** (`Refs.Blame`).
1604    ///
1605    /// The walk itself lives in `gunnar-server`'s `blame` module — the ONE
1606    /// implementation the store-local CLI verb already uses (one implementation) — and it
1607    /// runs here against the store the running server already holds, so a
1608    /// no-shell appliance can be blamed over the control plane while the
1609    /// exclusive lock that verb takes makes it impossible to run down.
1610    ///
1611    /// The default is `Unimplemented`, not an empty blame: a control plane that
1612    /// cannot walk must refuse, because zero lines reads as "an empty file",
1613    /// which is a wrong answer a console reader would believe.
1614    fn blame(
1615        &self,
1616        path: &RepoPath,
1617        rev: &str,
1618        file: &str,
1619    ) -> ControlResult<gunnar_proto::BlameResponse> {
1620        let _ = (path, rev, file);
1621        Err(ControlError::Unimplemented("Refs.Blame"))
1622    }
1623
1624    /// **Search one revision's files for a literal string** (`Refs.Search`).
1625    ///
1626    /// The same store-shared, walk-once-live implementation as [`blame`]: the
1627    /// `search` module in `gunnar-server`, reached against the running server's
1628    /// open store. `max_file_bytes` and `max_matches` of zero mean the module's
1629    /// defaults. Empty is a legitimate answer (a string that is not there), so
1630    /// the refusal below is the walk that could not START, never a miss.
1631    ///
1632    /// [`blame`]: ControlPlane::blame
1633    fn search(
1634        &self,
1635        path: &RepoPath,
1636        rev: &str,
1637        pattern: &str,
1638        max_file_bytes: u64,
1639        max_matches: u32,
1640    ) -> ControlResult<gunnar_proto::SearchResponse> {
1641        let _ = (path, rev, pattern, max_file_bytes, max_matches);
1642        Err(ControlError::Unimplemented("Refs.Search"))
1643    }
1644
1645    // ── identity and access (the ACL model) ────────────────────────────────
1646    /// Create a principal with `id` as its identity (and account) and `name`
1647    /// as its display name. The service resolves an empty wire `id` to the
1648    /// name before calling, so this never sees an empty id.
1649    fn add_principal(&self, id: &str, name: &str, kind: PrincipalKind)
1650        -> ControlResult<Principal>;
1651    /// **Enroll an employee: a NEW user created by and bound to `account`**
1652    /// (`Accounts.EnrollEmployee`). `AlreadyExists` for an id or name that is
1653    /// taken — an existing principal never becomes an employee — and for an id
1654    /// that names an existing namespace (its owner-by-name would be the
1655    /// employee). `NotFound` when `account` is not a namespace. The employee
1656    /// gets no namespace of its own and `Read` on the account's.
1657    ///
1658    /// With room ("Add users"): see [`EmployeeAsk`]. `FailedPrecondition`
1659    /// for a claim with no open seat; a short account is answered with
1660    /// `shortfall_bytes` and nothing written.
1661    fn enroll_employee(&self, account: &str, ask: &EmployeeAsk) -> ControlResult<EmployeeAnswer>;
1662    /// The account's employees, by id.
1663    fn employees(&self, account: &str) -> ControlResult<Vec<Principal>>;
1664    /// **Has the account enrolled anyone?** What `Access.WhoAmI` asks on every call; a plane
1665    /// that can answer without listing (CoreControlPlane) says so.
1666    fn has_employees(&self, account: &str) -> ControlResult<bool> {
1667        Ok(!self.employees(account)?.is_empty())
1668    }
1669    /// The account's seats: every seat granted, and the open ones.
1670    fn seats(&self, account: &str) -> ControlResult<(u32, u32)> {
1671        let _ = account;
1672        Ok((0, 0))
1673    }
1674    /// **Clans** (2026-10-05, [`crate::clans::ClanPlane`]), when this plane
1675    /// keeps an identity table that holds them. `None`: every `Clans` verb is
1676    /// UNIMPLEMENTED and no namespace is a clan's.
1677    fn clans(&self) -> Option<&dyn crate::clans::ClanPlane> {
1678        None
1679    }
1680    /// **The estate's operator, created on first claim** (`Accounts.ClaimOperator`,
1681    /// 2026-10-04). `asked` is the caller's name for it, used only when nothing
1682    /// was fed ([`operator_name`]). Idempotent; an ownerless namespace of that
1683    /// name is adopted; a machine under the name is `FAILED_PRECONDITION`. The
1684    /// principal is created keyless and gunnar-admin. The default is the
1685    /// honest answer of a plane that keeps no operator.
1686    fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim> {
1687        let _ = asked;
1688        Err(ControlError::Unimplemented("Accounts.ClaimOperator"))
1689    }
1690    /// **The operator's first invite** (Stage L, 2026-10-04): record
1691    /// `secret_sha256` (checked by [`bootstrap_hash`]) as the invite
1692    /// [`OPERATOR_INVITE_ID`] for `operator`'s first passkey — no server
1693    /// grant, expiring `ttl_ms` after `now` (checked by [`bootstrap_ttl`]:
1694    /// [`OPERATOR_INVITE_TTL_MS`] for the kit's own estates, shorter for a
1695    /// SOLD estate's first-login link) — unless the operator already holds a
1696    /// passkey ([`BootstrapInvite::Spent`]). The console's first-admin
1697    /// registration, or the buyer's first-login link, redeems it through
1698    /// `Access.WebRegister`.
1699    fn operator_bootstrap_invite(
1700        &self,
1701        operator: &str,
1702        secret_sha256: &str,
1703        now_unix_ms: i64,
1704        ttl_ms: i64,
1705    ) -> ControlResult<BootstrapInvite> {
1706        let _ = (operator, secret_sha256, now_unix_ms, ttl_ms);
1707        Err(ControlError::Unimplemented("Accounts.ClaimOperator (bootstrap invite)"))
1708    }
1709    fn list_principals(&self) -> ControlResult<Vec<Principal>>;
1710    /// **When each principal last signed in and last changed something**
1711    /// (2026-10-21), by principal id; a principal with no entry was never seen.
1712    /// The default knows nothing — a plane that keeps no stamps answers
1713    /// "never" for everybody, never an error.
1714    fn principal_activity(&self) -> ControlResult<std::collections::BTreeMap<String, PrincipalSeen>> {
1715        Ok(std::collections::BTreeMap::new())
1716    }
1717    /// One principal by its display name, or `None`.
1718    fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>>;
1719    /// One principal by its ID, or `None` (`Access.WhoAmI`). The default walks
1720    /// [`ControlPlane::list_principals`]; a plane with an id index may answer
1721    /// it directly.
1722    fn principal(&self, id: &str) -> ControlResult<Option<Principal>> {
1723        Ok(self.list_principals()?.into_iter().find(|p| p.id == id))
1724    }
1725    /// **Does `principal_id` hold a SERVER-scope grant** (the seller's mark,
1726    /// `Namespace.owner_server_grant`)? The default: no grant is known here.
1727    fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool> {
1728        let _ = principal_id;
1729        Ok(false)
1730    }
1731    /// Change the display name and nothing else. `AlreadyExists` when another
1732    /// principal carries the name (as name or id); `NotFound` for an unknown id.
1733    fn rename_principal(&self, principal_id: &str, name: &str) -> ControlResult<Principal>;
1734    /// **Register a web credential on a principal** (`Access.AddCredential`,
1735    /// Stage L). `NotFound` for an unknown principal; `FailedPrecondition` for
1736    /// one that is not a USER (no human credential on a machine principal);
1737    /// `AlreadyExists` for a credential id already registered. The public key
1738    /// is validated by the service before it gets here. Removed with the
1739    /// principal. Defaults to `Unimplemented`: a plane that keeps no identity
1740    /// table keeps no passkeys either.
1741    fn add_credential(
1742        &self,
1743        principal_id: &str,
1744        credential_id: &[u8],
1745        public_key: &[u8],
1746        label: &str,
1747        sign_count: u32,
1748    ) -> ControlResult<gunnar_proto::WebCredential> {
1749        let _ = (principal_id, credential_id, public_key, label, sign_count);
1750        Err(ControlError::Unimplemented("Access.AddCredential"))
1751    }
1752    /// One principal's web credentials, or every one (`None`).
1753    fn credentials(&self, principal_id: Option<&str>) -> ControlResult<Vec<gunnar_proto::WebCredential>> {
1754        let _ = principal_id;
1755        Err(ControlError::Unimplemented("Access.ListCredentials"))
1756    }
1757    /// One web credential with its public key, or `None`.
1758    fn credential(&self, credential_id: &[u8]) -> ControlResult<Option<StoredCredential>> {
1759        let _ = credential_id;
1760        Err(ControlError::Unimplemented("Access.RevokeCredential"))
1761    }
1762    /// Change a web credential's label; the updated row, or `None` if there
1763    /// is no such credential.
1764    fn rename_credential(
1765        &self,
1766        credential_id: &[u8],
1767        label: &str,
1768    ) -> ControlResult<Option<gunnar_proto::WebCredential>> {
1769        let _ = (credential_id, label);
1770        Err(ControlError::Unimplemented("Access.RenameCredential"))
1771    }
1772    /// **Store an invite** (`Access.CreateInvite`, Stage L). `NotFound` for an
1773    /// unknown principal, `FailedPrecondition` for one that is not a USER,
1774    /// `AlreadyExists` for an id taken. Removed with the principal.
1775    fn create_invite(&self, invite: NewInvite) -> ControlResult<gunnar_proto::Invite> {
1776        let _ = invite;
1777        Err(ControlError::Unimplemented("Access.CreateInvite"))
1778    }
1779    /// Every stored invite, expired ones included — the service decides what
1780    /// is pending. Never a secret: the plane holds only hashes.
1781    fn invites(&self) -> ControlResult<Vec<gunnar_proto::Invite>> {
1782        Err(ControlError::Unimplemented("Access.ListInvites"))
1783    }
1784    /// Withdraw an invite; the row it was, or `None`.
1785    fn revoke_invite(&self, id: &str) -> ControlResult<Option<gunnar_proto::Invite>> {
1786        let _ = id;
1787        Err(ControlError::Unimplemented("Access.RevokeInvite"))
1788    }
1789    /// **What the invite with this secret hash is now** (`Access.CheckInvite`,
1790    /// 2026-10-19): valid, used, expired or unknown. Read-only: never spends it.
1791    fn check_invite(&self, secret_hash: &str, now_unix_ms: i64) -> ControlResult<InviteCheck> {
1792        let _ = (secret_hash, now_unix_ms);
1793        Err(ControlError::Unimplemented("Access.CheckInvite"))
1794    }
1795    /// **Commit a verified web login** (`Access.WebLogin`, Stage L): see
1796    /// [`WebLoginCommit`]. `NotFound` for a credential that is gone;
1797    /// `PermissionDenied` when its counter did not increase (re-checked in
1798    /// the update); `AlreadyExists` for a session key already registered.
1799    fn web_login(&self, commit: WebLoginCommit) -> ControlResult<WebLoginOutcome> {
1800        let _ = commit;
1801        Err(ControlError::Unimplemented("Access.WebLogin"))
1802    }
1803    /// **Register a passkey from a creation ceremony** (`Access.WebRegister`,
1804    /// Stage L): see [`WebRegistration`]. An invite that is unknown, used or
1805    /// expired — or names another principal — is `PermissionDenied`, in the
1806    /// same words for all of them.
1807    fn web_register(&self, registration: WebRegistration) -> ControlResult<WebRegistered> {
1808        let _ = registration;
1809        Err(ControlError::Unimplemented("Access.WebRegister"))
1810    }
1811    /// **Commit an approved machine enrolment** (2026-10-05): see
1812    /// [`MachineApproval`]. The ONE way a web credential lands on a MACHINE
1813    /// principal — `add_credential` and `web_register` still refuse one. A
1814    /// step-up counter that did not increase is `PermissionDenied`. The
1815    /// operator's first invite is never touched: it is the owner's.
1816    fn approve_machine_enrolment(&self, approval: MachineApproval) -> ControlResult<MachineApproved> {
1817        let _ = approval;
1818        Err(ControlError::Unimplemented("Access.ApproveMachineEnrolment"))
1819    }
1820    /// The account that enrolled `principal_id` as an employee, if any.
1821    fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>> {
1822        let _ = principal_id;
1823        Err(ControlError::Unimplemented("Accounts.EnrollEmployee"))
1824    }
1825    /// **Company SSO's storage** ([`crate::sso::plane::SsoPlane`]), when this
1826    /// plane has an identity table that holds it. `None`: every `Sso` verb
1827    /// answers UNIMPLEMENTED, and `Access.WebLogin` knows of no `require_sso`.
1828    fn sso(&self) -> Option<&dyn crate::sso::plane::SsoPlane> {
1829        None
1830    }
1831    /// Remove a web credential; the row it was, or `None` if there was none.
1832    fn revoke_credential(&self, credential_id: &[u8]) -> ControlResult<Option<gunnar_proto::WebCredential>> {
1833        let _ = credential_id;
1834        Err(ControlError::Unimplemented("Access.RevokeCredential"))
1835    }
1836
1837    /// Returns (keys revoked, grants revoked).
1838    fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)>;
1839    /// Register a key on `principal_id`.
1840    ///
1841    /// `caller` is the principal on the connection (empty under allow-all), and
1842    /// `attestation` the target's signed word that this key may act as them.
1843    /// The rule for when the attestation is required is
1844    /// `gunnar_core::key_attestation::Requirement::of`, one writer both planes
1845    /// ask; the plane derives the new key's fingerprint itself and checks the
1846    /// attestation against it before anything is written.
1847    #[allow(clippy::too_many_arguments)]
1848    fn add_key(
1849        &self,
1850        principal_id: &str,
1851        openssh_public_key: &str,
1852        comment: &str,
1853        expires_at_unix_ms: Option<i64>,
1854        caller: &str,
1855        attestation: Option<SignedKeyAttestation>,
1856    ) -> ControlResult<PublicKey>;
1857    fn revoke_key(&self, fingerprint: &str) -> ControlResult<()>;
1858    fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>>;
1859    fn grant(
1860        &self,
1861        principal_id: &str,
1862        namespace: &str,
1863        repo: &str,
1864        level: GrantLevel,
1865    ) -> ControlResult<Grant>;
1866    fn revoke(&self, principal_id: &str, namespace: &str, repo: &str) -> ControlResult<()>;
1867    fn list_grants(
1868        &self,
1869        principal_id: Option<&str>,
1870        namespace: Option<&str>,
1871        repo: Option<&str>,
1872    ) -> ControlResult<Vec<Grant>>;
1873
1874    // ── groups (punkt 9) ───────────────────────────────────────────────────
1875    //
1876    // A group is a principal (`add_principal` with `PrincipalKind::Group`);
1877    // these manage its membership. A member inherits the group's grants
1878    // through `may`, so the RPC gates each of these on Admin exactly as it gates
1879    // `grant`. Default bodies return `Unimplemented` so a control plane that has
1880    // no membership store (the in-memory test double) compiles unchanged.
1881    //
1882    // Add and Remove take the acting admin's `signed` event and, like
1883    // `forge_append`, verify it against the ACL key table before applying and
1884    // append it to the signed membership log afterwards — the verification
1885    // lives behind this seam so there is one implementation of "is this
1886    // evidence" and not one per transport (one implementation). They return the log index
1887    // the event landed at, the index an inclusion proof is taken at.
1888    /// Verify the acting admin's signed event, make `member_id` a member of
1889    /// `group_id`, and append the event to the membership log.
1890    fn add_group_member(
1891        &self,
1892        group_id: &str,
1893        member_id: &str,
1894        signed: SignedMembershipEvent,
1895    ) -> ControlResult<u64> {
1896        let _ = (group_id, member_id, signed);
1897        Err(ControlError::Unimplemented("Access.AddGroupMember"))
1898    }
1899    /// Verify the signed event, remove `member_id` from `group_id`, and append
1900    /// it. Returns whether it was a member and the log index.
1901    fn remove_group_member(
1902        &self,
1903        group_id: &str,
1904        member_id: &str,
1905        signed: SignedMembershipEvent,
1906    ) -> ControlResult<(bool, u64)> {
1907        let _ = (group_id, member_id, signed);
1908        Err(ControlError::Unimplemented("Access.RemoveGroupMember"))
1909    }
1910    /// The direct members of a group.
1911    fn list_group_members(&self, group_id: &str) -> ControlResult<Vec<Principal>> {
1912        let _ = group_id;
1913        Err(ControlError::Unimplemented("Access.ListGroupMembers"))
1914    }
1915    /// The signed membership log, optionally filtered to one group, in append
1916    /// order — the evidence behind the live table.
1917    fn membership_events(
1918        &self,
1919        group_id: Option<&str>,
1920    ) -> ControlResult<Vec<SignedMembershipEvent>> {
1921        let _ = group_id;
1922        Err(ControlError::Unimplemented("Access.ListGroupMembershipEvents"))
1923    }
1924
1925    // ── policy (the pre-receive gate) ──────────────────────────────────────
1926    fn get_protection(
1927        &self,
1928        path: &RepoPath,
1929        ref_pattern: Option<&str>,
1930    ) -> ControlResult<Vec<BranchProtection>>;
1931    fn set_protection(
1932        &self,
1933        path: &RepoPath,
1934        protection: BranchProtection,
1935    ) -> ControlResult<BranchProtection>;
1936    fn delete_protection(&self, path: &RepoPath, ref_pattern: &str) -> ControlResult<()>;
1937    fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy>;
1938    fn set_ref_name_policy(
1939        &self,
1940        path: &RepoPath,
1941        policy: RefNamePolicy,
1942    ) -> ControlResult<RefNamePolicy>;
1943
1944    // ── forge: the review half (roadmap phase 1) ───────────────────────────
1945    //
1946    // The server never signs. `forge_append` takes an event the caller signed,
1947    // verifies it against the ACL key table, and appends it; the verification
1948    // lives behind this seam rather than in the service so that there is one
1949    // implementation of "is this evidence" and not one per transport (one implementation).
1950    //
1951    // There is no `next_pull` here: the number belongs to the record, and
1952    // `create_pull` assigns it. A second numbering authority is exactly the
1953    // twin this split exists to avoid.
1954    /// Verify a signed review event against the registered keys and append it.
1955    ///
1956    /// Returns the index it landed at — the index an inclusion proof is taken
1957    /// at. `InvalidArgument` for an event that does not verify, because a
1958    /// signature that does not check out is a malformed request and not a
1959    /// server fault.
1960    fn forge_append(&self, event: SignedForgeEvent) -> ControlResult<u64>;
1961    /// One pull request's review, folded, with every approval re-checked now.
1962    fn forge_review(
1963        &self,
1964        repo: &str,
1965        pull: u64,
1966    ) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)>;
1967    /// The review on every pull request in `repo` that has one.
1968    fn forge_reviews(&self, repo: &str) -> ControlResult<Vec<ReviewState>>;
1969
1970    // ── forge: the issue half ──────────────────────────────────────────────
1971    //
1972    // The SAME log and the SAME `forge_append`: an issue event is signed,
1973    // verified against the ACL key table and appended exactly as a review event
1974    // is. What differs is only the fold — an issue owns its open/closed state
1975    // where a review defers it to the pull request record — so these read the
1976    // one log through a different fold rather than opening a second store.
1977    /// One issue, folded from its events, or `None` if nothing opened that
1978    /// number. The whole thread is returned alongside — the opening, every
1979    /// comment, and the close if there is one.
1980    fn forge_issue(
1981        &self,
1982        repo: &str,
1983        number: u64,
1984    ) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>>;
1985    /// Every issue in `repo`, folded, in the order they were opened.
1986    fn forge_issues(&self, repo: &str) -> ControlResult<Vec<IssueState>>;
1987
1988    // ── admin ──────────────────────────────────────────────────────────────
1989    fn counts(&self) -> ControlResult<ControlCounts>;
1990
1991    /// **Tombstones and replication, for `Admin.Stats`.**
1992    ///
1993    /// The default is the honest empty one and NOT `Unimplemented`: unlike a
1994    /// ref list, "no tombstones and no standbys" is a real state a caller can
1995    /// act on, and a `Stats` that failed because a test double has no
1996    /// replicator would make the whole call unusable.
1997    fn stats_extras(&self) -> ControlResult<StatsExtras> {
1998        Ok(StatsExtras::default())
1999    }
2000    fn storage_info(&self, path: Option<&RepoPath>) -> ControlResult<StorageInfoResponse>;
2001
2002    /// **"Which CA does this box mint with?"** (`Admin.IssuerInfo`).
2003    ///
2004    /// A read of the PKI control document: the live issuer's certificate and
2005    /// fingerprint, and the trusted-CA list. `gunnar-server` answers it
2006    /// through [`ServerStores::pki_readonly`], whose secret store refuses
2007    /// every call — so this seam cannot mint, cannot rotate, and cannot reach
2008    /// the private key even if someone later edits the body of the one
2009    /// implementation.
2010    ///
2011    /// The default is `Unimplemented` and NOT `has_issuer: false`, for the
2012    /// reason [`Self::capacity`] gives above: a control plane with no PKI
2013    /// behind it has nothing to read, and "this box has no issuer" is a
2014    /// SPECIFIC and actionable fact — it is what a never-served data root
2015    /// looks like. A caller told that when the truth is "I cannot say" would
2016    /// go and mint one over the top of a CA their whole fleet pins.
2017    fn issuer_info(&self) -> ControlResult<gunnar_proto::IssuerInfoResponse> {
2018        Err(ControlError::Unimplemented("Admin.IssuerInfo"))
2019    }
2020
2021    /// **"Is the record of what happened here still complete?"**
2022    /// (`Admin.AuditTrail`, lane T11, 2026-09-21).
2023    ///
2024    /// The shape of the control plane's tamper-evident audit chain — entry
2025    /// count, tip hash, whether it verifies, whether it is replicated into the
2026    /// transaction log, and optionally the hash at one sequence number. Never
2027    /// an entry body: this answers completeness, not contents.
2028    ///
2029    /// The default is `Unimplemented` and NOT `present: false`, for the reason
2030    /// [`Self::issuer_info`] gives: "this box keeps no trail" is a specific and
2031    /// alarming fact, and a caller told it when the truth is "I cannot say"
2032    /// would report a box as unaudited that is not.
2033    fn audit_trail(&self, _at_seq: u64) -> ControlResult<gunnar_proto::AuditTrailResponse> {
2034        Err(ControlError::Unimplemented("Admin.AuditTrail"))
2035    }
2036
2037    /// **"If a tenant's cap rose to N bytes, could you serve it?"**
2038    /// (`Admin.Capacity`) — the question a seller must ask BEFORE the money
2039    /// moves, and OPEN SOURCE, ALWAYS ON: a self-hoster wants to know whether
2040    /// their disk can grow as much as a service provider does, so this is not
2041    /// behind `metered`.
2042    ///
2043    /// Not "how much free space is there". The answer carries a verdict AND,
2044    /// independently, whether the data set can ever hold more than it does —
2045    /// because on the appliance the second answer is NO and a caller that read
2046    /// a single small number would buy a disk that is billed and invisible.
2047    /// `gunnar-server`'s `capacity` module is the ONE implementation (one implementation);
2048    /// it measures with the same `statvfs` the push door refuses under.
2049    ///
2050    /// The default is `Unimplemented`, not a plausible answer: a control plane
2051    /// with no filesystem behind it has nothing to measure, and "0 bytes free"
2052    /// from it would read as a full disk rather than as an unanswered
2053    /// question — which is exactly the blind guard to avoid.
2054    fn capacity(
2055        &self,
2056        _request: &gunnar_proto::CapacityRequest,
2057    ) -> ControlResult<gunnar_proto::CapacityResponse> {
2058        Err(ControlError::Unimplemented("Admin.Capacity"))
2059    }
2060
2061    // ── The runtime data-set growth (`Admin.Grow*`, `Bootstrap.GrowStatus`) ──
2062    //
2063    // `DATA-SET-GROWTH-FLOW.md` §3, ruling 1. `gunnar-server`'s `grow` module
2064    // is the ONE implementation; a control plane without it answers an honest
2065    // `idle` with `engine_present: false` on status — never `Unimplemented`,
2066    // because the credential-free status is what an outside actor reads to
2067    // decide about the volumes, and "I cannot say" must not look like "ask
2068    // again later" — and refuses the two mutators by name.
2069
2070    /// Begin a runtime growth: drain, unmount, wait, grow, mount, reopen.
2071    fn grow_start(
2072        &self,
2073        _request: &gunnar_proto::GrowStartRequest,
2074    ) -> ControlResult<gunnar_proto::GrowStatusResponse> {
2075        Err(ControlError::Unimplemented(
2076            "Admin.GrowStart: this control plane has no runtime grow engine (gunnar-server built without `grow-runtime`)",
2077        ))
2078    }
2079
2080    /// The growth's phase, members and journal. Answered with and without a
2081    /// credential (`Admin.GrowStatus`, `Bootstrap.GrowStatus`).
2082    fn grow_status(&self) -> ControlResult<gunnar_proto::GrowStatusResponse> {
2083        Ok(gunnar_proto::GrowStatusResponse {
2084            phase: "idle".to_owned(),
2085            engine_present: false,
2086            serving: true,
2087            server_unix_ms: crate::bus::now_unix_ms(),
2088            detail: "this control plane has no runtime grow engine; the data set grows only at a restart, if at all".to_owned(),
2089            ..Default::default()
2090        })
2091    }
2092
2093    /// The outside actor says "look again" (or the operator gives up).
2094    fn grow_resume(&self, _give_up: bool) -> ControlResult<gunnar_proto::GrowStatusResponse> {
2095        Err(ControlError::Unimplemented(
2096            "Admin.GrowResume: this control plane has no runtime grow engine (gunnar-server built without `grow-runtime`)",
2097        ))
2098    }
2099
2100    /// **Per-namespace meters** (`Usage.Read` / `Usage.ReadAll`), the numbers a
2101    /// bill is made of — and OPEN SOURCE, ALWAYS ON: a self-hoster wants them
2102    /// as much as a service provider does, so this is not behind `metered`.
2103    ///
2104    /// `Some(ns)` answers one namespace and is `NotFound` for one the catalog
2105    /// does not hold; `None` answers every namespace, including ones with no
2106    /// repositories (`repos = 0`, everything else 0) — a tenant with nothing
2107    /// is a real row, not an absent one. What each meter counts, file by
2108    /// file, is written in `proto/gunnar/v1/usage.proto`; the ONE
2109    /// implementation is `gunnar-server`'s (one implementation), which measures the same
2110    /// directories `Admin.StorageInfo` walks, attributed per file.
2111    ///
2112    /// The default is `Unimplemented`, not an empty answer: a control plane
2113    /// with no store behind it has no bytes to count, and "0 bytes" from it
2114    /// would read as a free tenant rather than as an unanswered question.
2115    fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<gunnar_proto::NamespaceUsage>> {
2116        let _ = namespace;
2117        Err(ControlError::Unimplemented("Usage"))
2118    }
2119
2120    /// **One namespace's repositories, each with what it stores**
2121    /// (`Usage.Read`'s `repos`, 2026-10-04) — measured by the same walk that
2122    /// sums [`ControlPlane::usage`], so the rows' `stored_bytes` add up to the
2123    /// namespace's `pack_bytes`. Sorted by repository name. `NotFound` for a
2124    /// namespace the catalog does not hold; the default is `Unimplemented`, for
2125    /// [`ControlPlane::usage`]'s reason.
2126    fn repo_usage(&self, namespace: &str) -> ControlResult<Vec<gunnar_proto::RepoStored>> {
2127        let _ = namespace;
2128        Err(ControlError::Unimplemented("Usage"))
2129    }
2130
2131    /// **One page of every namespace's meters** (`UsageBatch.ReadMany`,
2132    /// 2026-10-06), in namespace order, at most `page_size` rows, resuming
2133    /// after `page_token` ([`usage_page_token`]); with a `changed_since` cursor
2134    /// it may answer only the namespaces whose row changed.
2135    ///
2136    /// The default is CORRECT and not cheap: it pages over
2137    /// [`ControlPlane::usage`]`(None)` and never honours `changed_since`
2138    /// (`changed_only = false`, no cursor) — every plane that can read usage can
2139    /// page it. `gunnar-server` overrides it with its usage ledger, which is the
2140    /// whole point of the call.
2141    fn usage_page(
2142        &self,
2143        page_token: &str,
2144        page_size: usize,
2145        changed_since: &str,
2146    ) -> ControlResult<gunnar_proto::ReadManyUsageResponse> {
2147        let _ = changed_since;
2148        let (_, after) = usage_page_token::decode(page_token);
2149        let mut rows = self.usage(None)?;
2150        rows.sort_by(|a, b| a.namespace.cmp(&b.namespace));
2151        Ok(usage_page_token::page(rows, &after, page_size, String::new(), false))
2152    }
2153
2154    /// **The FLOW series** (`Usage.Activity`): one row per UTC day this
2155    /// namespace moved anything, oldest first — and OPEN SOURCE, ALWAYS ON,
2156    /// for [`ControlPlane::usage`]'s reason.
2157    ///
2158    /// The window is inclusive at both ends and resolved to whole UTC days;
2159    /// `0` for `since_unix_ms` means the whole kept window, `0` for
2160    /// `until_unix_ms` means now. A day with no traffic is ABSENT rather than
2161    /// returned as a row of zeroes: the caller knows which days it asked for,
2162    /// and a day that was never written and a day on which nothing happened
2163    /// are the same fact.
2164    ///
2165    /// The second half of the answer is how many days this server keeps, which
2166    /// is what lets a caller tell an empty series that means *nothing
2167    /// happened* from one that means *you asked about a time this box no
2168    /// longer remembers*.
2169    ///
2170    /// The default is `Unimplemented`, not an empty series — for exactly the
2171    /// reason [`ControlPlane::usage`]'s is. A control plane with no flow ledger
2172    /// behind it counted nothing, and "no days" from it would read as a silent
2173    /// tenant rather than as an unanswered question. That distinction is the
2174    /// whole point of this call.
2175    fn activity(
2176        &self,
2177        namespace: &str,
2178        since_unix_ms: i64,
2179        until_unix_ms: i64,
2180    ) -> ControlResult<(Vec<gunnar_proto::DayActivity>, i64)> {
2181        let _ = (namespace, since_unix_ms, until_unix_ms);
2182        Err(ControlError::Unimplemented("Usage.Activity"))
2183    }
2184
2185    /// **The admission plan for a namespace** (`Usage.AdmitPlan`): its usage
2186    /// over the rolling window, the budget, and per kind of work the class
2187    /// the gate would give the next transfer, the learnt estimate and the
2188    /// plan's sentence. Open source and always on — a build without
2189    /// `admit-share` answers `pass-through`, which is the truth about it.
2190    ///
2191    /// The default is `Unimplemented`, for [`ControlPlane::usage`]'s reason: a
2192    /// control plane with no admission gate behind it has no plan to report.
2193    fn admit_plan(&self, namespace: &str) -> ControlResult<gunnar_proto::AdmitPlanResponse> {
2194        let _ = namespace;
2195        Err(ControlError::Unimplemented("Usage.AdmitPlan"))
2196    }
2197
2198    // ── the METERED appliance: `Entitlement` ────────────────────────────────
2199    //
2200    // Compiled only with the `metered` feature, and then the defaults are a
2201    // refusal BY NAME — the in-memory test double has no monetize key to
2202    // verify a fact against and no control document to keep it in. Off, these
2203    // methods do not exist: a `ControlPlane` with no `entitlement_set` is the
2204    // whole point (the metered build split), not a stub that returns
2205    // `Unimplemented` from a binary that still carries the code.
2206
2207    /// **The fact being ENFORCED for one namespace**, or `None` = Free.
2208    ///
2209    /// # The contract, and the one implementation that does not keep it
2210    ///
2211    /// "Enforced", not "stored". Two things read a namespace's plan on this
2212    /// box and they read different places:
2213    ///
2214    /// * `gunnar_ssh::PlanGate` — the push door, `Entitlement.Set`'s whole
2215    ///   purpose — asks the CACHE (`EntitlementCache::allows`), which holds
2216    ///   only facts that verified under the configured monetize key;
2217    /// * this method, on `gunnar-server`, read the STORE until 2026-09-21
2218    ///   (`EntitlementSlot::get` → `EntitlementStore::get`), which holds
2219    ///   whatever was written, unverified on the way out. **It now reads the
2220    ///   cache** — the fix described below is made, and
2221    ///   `gunnar-server/tests/entitlement.rs`
2222    ///   `a_box_restored_under_the_wrong_key_reports_no_plan_it_does_not_enforce`
2223    ///   pins it. The rest of this note is the reason, kept.
2224    ///
2225    /// They agreed on every path except one, and it is a path that runs on
2226    /// every restart. `EntitlementSlot::open` loads each stored fact and pushes
2227    /// it into the cache **through `monetize_embed::signing::verify_fact`**; a
2228    /// fact that no longer verifies — the monetize key was rotated, the control
2229    /// document was edited — is logged (`entitlement.boot.refused`) and left
2230    /// out, and that namespace is Free for everything the cache answers.
2231    ///
2232    /// The store still has it. So `Usage.Read` reports its caps as the
2233    /// namespace's `Allowance`, `Entitlement.Get` returns it, and
2234    /// `Entitlement.List` lists it, while NOTHING on the box enforces any of
2235    /// it. That is precisely the defect `Allowance.enforcement` was added to
2236    /// close, coming back through a different door and worse: there the tenant
2237    /// was shown a measured number labelled as a limit, here they are shown a
2238    /// `pack_bytes` cap labelled `ENFORCED` — by
2239    /// [`crate::service::meters::METERS`], correctly, since that table
2240    /// describes the CODE — with no fact in force behind it.
2241    ///
2242    /// **A rejected fact must not be reported as an allowance.** The fix is in
2243    /// the implementation and not here: `EntitlementSlot::get` must answer out
2244    /// of the same snapshot `PlanGate` reads, so that a fact refused at boot is
2245    /// `None` to both. Reporting a plan the box will not act on is a promise
2246    /// nobody keeps, and it is silent — the only trace today is one
2247    /// `tracing::error!` at startup.
2248    ///
2249    /// Refusing to serve `Usage.Read` at all in that state would be the wrong
2250    /// repair: the METERS are still true, and a tenant whose signature broke
2251    /// still deserves to see what they hold.
2252    #[cfg(feature = "metered")]
2253    fn entitlement_get(
2254        &self,
2255        namespace: &str,
2256    ) -> ControlResult<Option<gunnar_proto::EntitlementFact>> {
2257        let _ = namespace;
2258        Err(ControlError::Unimplemented("Entitlement.Get"))
2259    }
2260
2261    /// Every fact, one per namespace.
2262    #[cfg(feature = "metered")]
2263    fn entitlement_list(&self) -> ControlResult<Vec<gunnar_proto::EntitlementFact>> {
2264        Err(ControlError::Unimplemented("Entitlement.List"))
2265    }
2266
2267    /// The stored facts this box refused when it booted (`Entitlement.List`'s
2268    /// `refused_at_boot`). Default `None`: nothing refused, or nothing held.
2269    #[cfg(feature = "metered")]
2270    fn entitlement_refused_at_boot(&self) -> ControlResult<Option<gunnar_proto::RefusedAtBoot>> {
2271        Ok(None)
2272    }
2273
2274    /// **Verify, log, store, swap.** Returns the state now enforced and the
2275    /// sequence number (line index) of the attestation line the accepted
2276    /// fact was appended as. A fact that does not verify is
2277    /// [`ControlError::PermissionDenied`] naming why, and NOTHING moves — not
2278    /// the snapshot, not the document, not the log.
2279    #[cfg(feature = "metered")]
2280    fn entitlement_set(
2281        &self,
2282        fact: gunnar_proto::EntitlementFact,
2283    ) -> ControlResult<(gunnar_proto::EntitlementState, u64)> {
2284        let _ = fact;
2285        Err(ControlError::Unimplemented("Entitlement.Set"))
2286    }
2287
2288    // ── the METERED appliance: `Billing` ────────────────────────────────────
2289    //
2290    // Two methods and a deliberate shape: this side of the seam holds the
2291    // KEY, and the other side holds the ticket FORMAT.
2292    //
2293    // `crate::service::billing` builds the ticket, asks here for a signature
2294    // over bytes, and pastes the result in. It is never handed key material.
2295    // The alternative — a seam method that returns the private key — would put
2296    // a signing key into a crate whose job is to parse requests, and would add
2297    // a second place on this box that can produce the appliance's signature
2298    // over anything at all. The alternative in the other direction — a seam
2299    // method that mints the whole ticket — would move the format into
2300    // `gunnar-server` and leave the grant check and the thing it guards in two
2301    // different crates.
2302
2303    /// **The public half of the actor-ticket key, 64 lowercase hex characters**
2304    /// (`Billing.ApplianceIdentity`).
2305    ///
2306    /// The default is a refusal BY NAME rather than a fabricated key: a control
2307    /// plane that cannot reach a secret store has no key, and answering with
2308    /// something that looks like one would be advertising a key that verifies
2309    /// nothing this box will ever sign.
2310    #[cfg(feature = "metered")]
2311    fn actor_ticket_public_key_hex(&self) -> ControlResult<String> {
2312        Err(ControlError::Unimplemented("Billing.ApplianceIdentity"))
2313    }
2314
2315    /// **Sign `message` with the actor-ticket key** — raw Ed25519, 64 bytes, no
2316    /// envelope.
2317    ///
2318    /// `message` is `monetize_embed::ticket::ticket_message` of an unsigned
2319    /// ticket, and this method has no opinion about that: it takes bytes,
2320    /// because the half that holds the key is not the half that knows the
2321    /// format, and an opinion here would be a second place the format is
2322    /// written down.
2323    ///
2324    /// **Every call to this is a ticket that exists.** The caller must have
2325    /// passed its grant check before it gets here — `service::billing` does,
2326    /// and `tests/billing.rs` counts the calls to prove a refused request makes
2327    /// none.
2328    #[cfg(feature = "metered")]
2329    fn actor_ticket_sign(&self, message: &[u8]) -> ControlResult<[u8; 64]> {
2330        let _ = message;
2331        Err(ControlError::Unimplemented("Billing.MintActorTicket"))
2332    }
2333
2334    /// **The named sealed slot `name`** (2026-10-09): its owner's principal id
2335    /// and its value, or `None` when there is no such slot. The ACL is the
2336    /// service's (`service::sealed`); the plane stores and reads.
2337    #[cfg(feature = "metered")]
2338    fn sealed_get(&self, name: &str) -> ControlResult<Option<(String, Vec<u8>)>> {
2339        let _ = name;
2340        Err(ControlError::Unimplemented("Sealed.Get"))
2341    }
2342
2343    /// **Store `value` in the slot `name`, owned by `owner`** (a principal id):
2344    /// sealed under the KEK, one `Secret` frame. Answers whether it was created.
2345    #[cfg(feature = "metered")]
2346    fn sealed_put(&self, name: &str, owner: &str, value: &[u8]) -> ControlResult<bool> {
2347        let _ = (name, owner, value);
2348        Err(ControlError::Unimplemented("Sealed.Put"))
2349    }
2350
2351    /// **Walk the provenance chain** (`Admin.VerifyChain`, roadmap phase 2).
2352    ///
2353    /// The walk itself lives in `gunnar-server`'s `verify` module — the one
2354    /// implementation the CLI verb already uses (one implementation) — so the default here
2355    /// is `Unimplemented`, not an empty chain: a control plane that cannot
2356    /// walk the chain must refuse, because an empty chain with no BROKEN link
2357    /// reads as merely incomplete, and "the backend cannot verify anything"
2358    /// must never be dressed up as "nothing was disproven".
2359    fn verify_chain(
2360        &self,
2361        request: &gunnar_proto::VerifyChainRequest,
2362    ) -> ControlResult<gunnar_proto::VerifyChainResponse> {
2363        let _ = request;
2364        Err(ControlError::Unimplemented("Admin.VerifyChain"))
2365    }
2366
2367    /// Index work in flight. Default: none — an engine without a background
2368    /// indexer has nothing to report, and that is not an error.
2369    fn index_progress(&self) -> ControlResult<Vec<IndexProgressRow>> {
2370        Ok(Vec::new())
2371    }
2372
2373    // ── mirror (mirroring) ─────────────────────────────────────────────────
2374    fn mirror_add(
2375        &self,
2376        path: &RepoPath,
2377        remote_url: &str,
2378        prune: bool,
2379        create_repo: bool,
2380    ) -> ControlResult<MirrorConfig>;
2381    /// **Enqueue a sync, and return without syncing.**
2382    ///
2383    /// Returns the registration with [`MirrorConfig::job`] set to the job now
2384    /// responsible for the repository — a new one, or the one that was already
2385    /// queued or running for it. It does NOT return what moved: a fetch is
2386    /// minutes of somebody else's network and a unary RPC that waited for one
2387    /// would time out. Poll `mirror_status` until the job is DONE.
2388    ///
2389    /// It used to return `(MirrorConfig, Vec<RefUpdate>)` and to be
2390    /// `Unimplemented` in every implementation, so nothing ever read that pair.
2391    fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig>;
2392    /// **Store, replace or forget the credential this mirror dials with.**
2393    ///
2394    /// `secret` is the value and is **write-only**: nothing returns it, and the
2395    /// `MirrorConfig` that comes back says only which kind is set. A `kind` of
2396    /// `Unspecified` forgets it, and is the only spelling of that; see
2397    /// `mirror.proto` for why this is a verb of its own rather than two more
2398    /// fields on `mirror_add`.
2399    fn mirror_set_credential(
2400        &self,
2401        path: &RepoPath,
2402        kind: MirrorCredentialKind,
2403        secret: &str,
2404        username: &str,
2405    ) -> ControlResult<MirrorConfig>;
2406    fn mirror_status(
2407        &self,
2408        namespace: Option<&str>,
2409        path: Option<&RepoPath>,
2410    ) -> ControlResult<Vec<MirrorConfig>>;
2411    fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()>;
2412
2413    // ── replication: the OPERATOR half ─────────────────────────────────────
2414    //
2415    // Default bodies, and the default is a REFUSAL BY NAME. Two callers cannot
2416    // do this: a build without `gunnar-server`'s `replication` feature, and the
2417    // in-memory test double, which has no replicator to reconfigure. Both must
2418    // say so out loud — an empty `Ok` here would be a control plane that
2419    // accepts a standby and silently keeps none, which is the exact shape of
2420    // the defect replication's whole file is written against.
2421    //
2422    // They exist at all because the appliance has NO SHELL. `replication.json`
2423    // is the truth, and until these three verbs there was no way to write it on
2424    // an OS-less image that runs as PID 1.
2425
2426    /// Add or replace one standby by name, persist it, and start replicating.
2427    fn replication_set(
2428        &self,
2429        _target: ReplicationTarget,
2430    ) -> ControlResult<ReplicationStatusResponse> {
2431        Err(ControlError::Unimplemented(
2432            "this server was built without replication (`--features replication`), so it              cannot be given a standby",
2433        ))
2434    }
2435    /// Drop one standby by name, persist that, and stop replicating to it.
2436    fn replication_remove(&self, _name: &str) -> ControlResult<ReplicationStatusResponse> {
2437        Err(ControlError::Unimplemented(
2438            "this server was built without replication (`--features replication`), so it              has no standby to remove",
2439        ))
2440    }
2441    /// Drop EVERY standby at once — the failover demote. Persist the empty
2442    /// roster and stop replicating anywhere.
2443    fn replication_demote(&self) -> ControlResult<ReplicationStatusResponse> {
2444        Err(ControlError::Unimplemented(
2445            "this server was built without replication (`--features replication`), so it              has no standby to demote",
2446        ))
2447    }
2448    /// **Set this box's role**: `STANDBY` freezes client writes (a read-only
2449    /// standby); `PRIMARY` is a writable primary — the takeover flip; `BROWSE`
2450    /// is a frozen standby that serves anonymous reads and can never be
2451    /// promoted (the browse replica) — an implementation refuses
2452    /// `PRIMARY` on a browse box with `FailedPrecondition`. Persist it and
2453    /// flip the shared write-gate the front doors read.
2454    fn replication_set_role(
2455        &self,
2456        _role: gunnar_proto::ReplicationRole,
2457    ) -> ControlResult<ReplicationStatusResponse> {
2458        Err(ControlError::Unimplemented(
2459            "this server was built without replication (`--features replication`), so it              has no role to set",
2460        ))
2461    }
2462    /// **Abdicate**: freeze client writes, flush pending to the standbys, then
2463    /// demote — the master steps down cleanly, in that order.
2464    fn replication_abdicate(&self) -> ControlResult<ReplicationStatusResponse> {
2465        Err(ControlError::Unimplemented(
2466            "this server was built without replication (`--features replication`), so it              cannot abdicate",
2467        ))
2468    }
2469    /// What is configured, and what each running standby has done.
2470    ///
2471    /// `include_refs` asks for the expensive half — the count of refs a mirror
2472    /// does not carry, which opens every repository's reference database. False
2473    /// (a plain `replication list`) answers the cheap operational signal and
2474    /// opens no store; true (`--refs`) takes the walk.
2475    fn replication_status(
2476        &self,
2477        _include_refs: bool,
2478    ) -> ControlResult<ReplicationStatusResponse> {
2479        Err(ControlError::Unimplemented(
2480            "replication: this server was built without the `replication` feature",
2481        ))
2482    }
2483    /// **Backfill**: queue every repository one standby covers, or one named
2484    /// repository, so a standby added AFTER the repositories were written gets
2485    /// them. Adding a standby enqueues nothing.
2486    fn replication_resync(
2487        &self,
2488        _name: &str,
2489        _repo: Option<&RepoPath>,
2490        _except: &[RepoPath],
2491    ) -> ControlResult<ResyncReplicationResponse> {
2492        Err(ControlError::Unimplemented(
2493            "replication: this server was built without the `replication` feature, so it              has no standby to backfill",
2494        ))
2495    }
2496
2497    /// **The replication watermark** (`Mirror.FlushReplication`,
2498    /// `DATA-SET-GROWTH-FLOW.md` G2/T11): push everything pending to every
2499    /// standby, wait — bounded — and report the tuple that proves (or does
2500    /// not) that the twin is caught up. Changes no role and no roster.
2501    fn replication_flush(
2502        &self,
2503        _request: &gunnar_proto::FlushReplicationRequest,
2504    ) -> ControlResult<gunnar_proto::FlushReplicationResponse> {
2505        Err(ControlError::Unimplemented(
2506            "replication: this server was built without the `replication` feature, so it has no standby to flush to",
2507        ))
2508    }
2509
2510    // ── The transaction log: what the twin fetches ──────────────────────────
2511
2512    /// The log's head — `head_seq`, `first_seq`, bytes, and on a twin what it
2513    /// has applied. Default `Unimplemented` for a plane with no log.
2514    fn log_head(&self) -> ControlResult<gunnar_proto::LogHeadResponse> {
2515        Err(ControlError::Unimplemented(
2516            "transaction log: this control plane keeps none",
2517        ))
2518    }
2519
2520    /// Entries `from..=to`, whole, at most `max_bytes` of blob in total but
2521    /// never fewer than one entry. `from` below the oldest retained sequence
2522    /// is `FailedPrecondition` naming it.
2523    fn log_fetch(
2524        &self,
2525        _from: u64,
2526        _to: u64,
2527        _max_bytes: u64,
2528    ) -> ControlResult<Vec<gunnar_proto::LogEntry>> {
2529        Err(ControlError::Unimplemented(
2530            "transaction log: this control plane keeps none",
2531        ))
2532    }
2533
2534    /// **[`ControlPlane::log_fetch`], streamed** (twin-streaming, 2026-10-06): the same
2535    /// refusals, answered NOW — before the call's first piece, so `Mirror.LogFetch` keeps its
2536    /// status codes (`FailedPrecondition` for a range retention dropped, and the rest) — then a
2537    /// producer that hands the range to its sink as `LogChunk` pieces of at most
2538    /// [`LOG_PIECE_BYTES`], `meta` on an entry's first, `last` on its last. The service runs it
2539    /// on a blocking thread behind a few pieces of window, so the sink's backpressure paces it.
2540    ///
2541    /// The default gathers through `log_fetch` and cuts each entry into pieces, which is right
2542    /// for a plane that holds its entries in memory anyway. A plane that keeps a real log
2543    /// overrides it and reads each entry off the disk a piece at a time: a 512 MiB pack must not
2544    /// cost the primary 512 MiB (it cost twice that before: the entry, then its pieces).
2545    fn log_fetch_stream(&self, from: u64, to: u64, max_bytes: u64) -> ControlResult<LogFetchProducer> {
2546        let entries = self.log_fetch(from, to, max_bytes)?;
2547        Ok(Box::new(move |sink| {
2548            for e in entries {
2549                log_entry_pieces(e, sink)?;
2550            }
2551            Ok(())
2552        }))
2553    }
2554
2555    /// **[`ControlPlane::log_fetch_stream`] with the FIRST entry started at byte `from_offset`
2556    /// of its blob** (`LogFetchRequest.from_offset`, restore-large-entry 2026-10-07): a restore
2557    /// resuming an entry the box being filled holds part of. Every piece says its `offset`.
2558    /// 0 is [`ControlPlane::log_fetch_stream`] exactly. The default gathers through
2559    /// `log_fetch` and cuts the first entry from the offset; a plane with a real log overrides it,
2560    /// reads the blob from the offset only and stamps the stored crc on the last piece for the
2561    /// receiver to check.
2562    fn log_fetch_stream_at(&self, from: u64, from_offset: u64, to: u64, max_bytes: u64) -> ControlResult<LogFetchProducer> {
2563        if from_offset == 0 {
2564            return self.log_fetch_stream(from, to, max_bytes);
2565        }
2566        let entries = self.log_fetch(from, to, max_bytes)?;
2567        Ok(Box::new(move |sink| {
2568            for e in entries {
2569                let at = if e.seq == from { from_offset } else { 0 };
2570                log_entry_pieces_from(e, at, sink)?;
2571            }
2572            Ok(())
2573        }))
2574    }
2575
2576    /// **On a twin: its primary's heartbeat** (when the primary last answered),
2577    /// with the silence bound `silent_after_ms`. Default `None`: not a twin.
2578    fn primary_heartbeat(&self, _silent_after_ms: u64) -> Option<gunnar_proto::PrimaryHeartbeat> {
2579        None
2580    }
2581
2582    /// **What `Admin.Topology` needs from the plane** (2026-10-04): the
2583    /// pairing feed's peers, which principals run on the front, the plugin
2584    /// registration, this box's own address and data volume. Default: a box
2585    /// that knows only itself — which is the truthful answer for a plane with
2586    /// no pairing (a solo gunnar, a test).
2587    fn topology_facts(&self) -> crate::topology::Facts {
2588        crate::topology::Facts::default()
2589    }
2590
2591    /// **Is this box a twin right now?** Default `false`. Read by the browse refusal: a twin is
2592    /// frozen like a browse replica, yet it must take its OWN data-set growth exactly as main
2593    /// does (owner ruling 2026-09-22, T21), so the grow verbs pass on a twin and on no browse box.
2594    fn is_twin(&self) -> bool {
2595        false
2596    }
2597
2598    /// **Does this box's transaction log FOLLOW another's** (a twin, or a main
2599    /// being restored)? It originates no transaction until it opens as the
2600    /// primary. Default `false` (wiring tracer B 2026-10-08: `Entitlement.Set`
2601    /// answers such a box's "try again" before it asks whether a namespace
2602    /// exists — a namespace made after the twin's snapshot is not replayed yet).
2603    fn log_follows(&self) -> bool {
2604        false
2605    }
2606
2607    /// **States this box is in that an operator must act on and no console reaches**
2608    /// (`Admin.Health.alarms`). Default: none.
2609    fn health_alarms(&self) -> Vec<gunnar_proto::HealthAlarm> {
2610        Vec::new()
2611    }
2612
2613    /// **Why this box reports `maintenance`** (`Admin.Health.status`), one reason each; empty is
2614    /// `serving`. It still serves either way. Default: none.
2615    fn maintenance_reasons(&self) -> Vec<String> {
2616        Vec::new()
2617    }
2618
2619    /// Is `principal_id` a member of `group`? The twin's log gate asks it
2620    /// (`Mirror.LogHead` / `LogFetch` for a member of group `twin`). Default
2621    /// `false`: a plane with no groups has no twin members.
2622    fn is_group_member(&self, _principal_id: &str, _group: &str) -> ControlResult<bool> {
2623        Ok(false)
2624    }
2625
2626    /// `Bootstrap.BecomeTwin`: a fresh box becomes a twin of `primary`;
2627    /// answers the twin's public key line.
2628    fn become_twin(&self, _primary: &str) -> ControlResult<String> {
2629        Err(ControlError::Unimplemented("twin: this control plane cannot become a twin"))
2630    }
2631
2632    /// `Bootstrap.AssignTunnelAddress` (2026-09-21, T17): `address` empty =
2633    /// answer only; otherwise record it as this box's WireGuard tunnel address
2634    /// and raise `wg0`. Default: a plane with no tunnel has no address to take.
2635    fn assign_tunnel_address(&self, _address: &str) -> ControlResult<gunnar_proto::AssignTunnelAddressResponse> {
2636        Err(ControlError::Unimplemented("tunnel: this control plane has no WireGuard tunnel (built without wg-client)"))
2637    }
2638
2639    /// `Admin.SetTunnelAddress` (2026-09-21, T17): the SAME answer as
2640    /// [`ControlPlane::assign_tunnel_address`] for a box past its bootstrap
2641    /// window. The caller has already been held to server-wide Admin by the
2642    /// service; the plane does not re-check the window.
2643    fn set_tunnel_address(&self, _address: &str) -> ControlResult<gunnar_proto::AssignTunnelAddressResponse> {
2644        Err(ControlError::Unimplemented("tunnel: this control plane has no WireGuard tunnel (built without wg-client)"))
2645    }
2646
2647    /// `Bootstrap.TwinCsr`: this twin's replication certificate request,
2648    /// minting its TLS key on the box on first call (lane T9).
2649    fn twin_csr(&self) -> ControlResult<String> {
2650        Err(ControlError::Unimplemented("Bootstrap.TwinCsr"))
2651    }
2652
2653    /// `Bootstrap.InstallTwinCertificate`: take the primary-signed certificate
2654    /// and pin its issuer; answers whole days left.
2655    fn install_twin_certificate(&self, _cert_pem: &str, _ca_pem: &str, _replication_port: u16, _replication_host: &str) -> ControlResult<i64> {
2656        Err(ControlError::Unimplemented("Bootstrap.InstallTwinCertificate"))
2657    }
2658
2659    /// `Admin.IssueTwinCertificate`, on the primary: `(cert, issuer, port, host)`
2660    /// — `host` is the address the listener is bound on when that is not the
2661    /// joined one (its tunnel address), empty otherwise.
2662    fn issue_twin_certificate(&self, _principal: &str, _csr_pem: &str) -> ControlResult<(String, String, u16, String)> {
2663        Err(ControlError::Unimplemented("Admin.IssueTwinCertificate"))
2664    }
2665
2666    /// [`ControlPlane::log_head`], **signed over the caller's nonce** — what a
2667    /// twin's poll actually calls (2026-09-21, lane T4). The default ignores the
2668    /// nonce and answers unsigned, which is the honest answer for a plane with
2669    /// no key of its own; a twin that has pinned a key refuses it.
2670    fn log_head_signed(&self, _nonce: &str) -> ControlResult<gunnar_proto::LogHeadResponse> {
2671        self.log_head()
2672    }
2673
2674    /// **A twin's proof that a NEWER generation of this box's log exists**
2675    /// (`LogHeadRequest.generation_proof`, stinky row 18, the main's half).
2676    /// Called before the answer is made. A box that finds the proof genuine — its
2677    /// own log, its own host key, a generation above its own — fences itself and
2678    /// originates nothing from then on; anything else is ignored. The default
2679    /// keeps no log and has nothing to fence.
2680    fn fence_by_generation(&self, _proof: &gunnar_proto::GenerationProof) -> ControlResult<()> {
2681        Ok(())
2682    }
2683
2684    /// **Is this box's git listener still bound where a primary's was?**
2685    ///
2686    /// A separate question from `become_twin`'s answer because it stays true
2687    /// until the box restarts, and can be asked again afterwards. The bind is
2688    /// chosen once at start-up; a box conferred the twin role over the wire is
2689    /// past that point. Nothing is SERVED through that socket — the front door
2690    /// refuses every git service on a twin from the instant the role lands —
2691    /// but it is still bound on an address a twin's socket does not belong on.
2692    ///
2693    /// `false` for a plane that has no git listener to be wrong about.
2694    fn twin_git_listener_needs_restart(&self) -> bool {
2695        false
2696    }
2697
2698    /// `Bootstrap.Restore`, start: the next sequence this box takes. `owner` is
2699    /// who the stream's credential proved the caller to be
2700    /// ([`crate::auth::authenticate_restore`]): a box already restoring resumes
2701    /// only for the owner that started it, and refuses anybody else
2702    /// `PERMISSION_DENIED` (h-v1-6a, 2026-09-28).
2703    fn restore_begin(&self, _owner: &str) -> ControlResult<u64> {
2704        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2705    }
2706
2707    /// **The keys this box takes a `Bootstrap.Restore` from** (h-v1-6a,
2708    /// 2026-09-28), as OpenSSH public key lines. A fresh box has no identity
2709    /// table to check a credential against, so these are the keys it was FED
2710    /// before anybody could enrol: what the installer's door handed it, and
2711    /// what the medium named. Default: none — a plane that was fed nothing
2712    /// refuses every restore by name, which is the fail-closed answer.
2713    fn restore_keys(&self) -> ControlResult<Vec<String>> {
2714        Ok(Vec::new())
2715    }
2716
2717    /// `Bootstrap.Restore`, the room check: has this box's data set space for a stream of
2718    /// `announced_bytes` of log? Asked ONCE, on the first chunk, before anything of the restore
2719    /// is written — the restore-onto-different-hardware case. `0` = the sender did not say, and
2720    /// no guess is made. A default `Ok(())`: a plane with no data set of its own cannot run out.
2721    fn restore_room(&self, _announced_bytes: u64) -> ControlResult<()> {
2722        Ok(())
2723    }
2724
2725    /// **`Bootstrap.Restore`, the stream's intake** (restore-large-entry, 2026-10-07): what every
2726    /// chunk of one stream is fed into. Starting one is [`ControlPlane::restore_begin`] — the
2727    /// owner check, the next sequence — and, on a plane that stages, holding the box's ONE intake
2728    /// until it is dropped. `Ok(None)`: another stream's intake still holds the box (a stream the
2729    /// sender abandoned whose apply has not returned yet); the caller waits a little and asks
2730    /// again rather than racing it over the same staged entry.
2731    ///
2732    /// The default gathers every entry whole in memory and applies it with
2733    /// [`ControlPlane::restore_apply`]: right for a plane with no disk of its own.
2734    fn restore_intake<'a>(&'a self, owner: &str) -> ControlResult<Option<Box<dyn RestoreIntake + 'a>>> {
2735        let next = self.restore_begin(owner)?;
2736        Ok(Some(Box::new(GatherIntake { plane: self, next, open: None, want_len: 0, applied: 0, lost: 0 })))
2737    }
2738
2739    /// `Bootstrap.Restore`, one whole entry: `true` when a replayed CAS lost.
2740    fn restore_apply(&self, _entry: gunnar_proto::LogEntry) -> ControlResult<bool> {
2741        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2742    }
2743
2744    /// `Bootstrap.Restore`, end: the head reached. `announced_head` is the largest
2745    /// `LogChunk.restore_head` the stream carried (0 = none): a box whose head is below it
2746    /// refuses to finish and stays restoring.
2747    fn restore_finish(&self, _announced_head: u64, _log_id_of_source: &str) -> ControlResult<u64> {
2748        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2749    }
2750
2751    /// `Mirror.SnapshotFetch`: a consistent snapshot streamed into `sink` as `LogChunk` pieces —
2752    /// items with `seq` 0, at most 1 MiB of blob a piece, `blob_len` the item's total on its
2753    /// last piece and `u64::MAX` before it, the `SnapshotAt` terminator last. Never gathered:
2754    /// the sink's backpressure paces the producer. Answers the sequence it was taken at.
2755    fn snapshot_stream(&self, _sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<u64> {
2756        Err(ControlError::Unimplemented("snapshot: this control plane keeps no log"))
2757    }
2758
2759    /// **[`ControlPlane::snapshot_stream`] from item `from_item`, byte `from_offset` on**
2760    /// (`SnapshotFetchRequest`, restore-after-snapshot 2026-10-07): a twin's kept base, resumed
2761    /// for a box being restored across capped calls; every piece names its item, offset and base
2762    /// (`LogChunk.snapshot_item`, `offset`, `snapshot_id`). `expect_id`: the base the asker holds
2763    /// part of — another one now is refused `[snapshot-changed]`. The default resumes nothing:
2764    /// asked from its start it is `snapshot_stream`, otherwise refused by name.
2765    fn snapshot_stream_from(&self, from_item: u64, from_offset: u64, expect_id: &str, sink: &mut dyn FnMut(gunnar_proto::LogChunk) -> ControlResult<()>) -> ControlResult<u64> {
2766        if from_item == 0 && from_offset == 0 && expect_id.trim().is_empty() {
2767            return self.snapshot_stream(sink);
2768        }
2769        Err(ControlError::Unimplemented("snapshot: this control plane cannot resume a snapshot"))
2770    }
2771
2772    /// `Mirror.LogRetain`: drop every whole segment below `from` (0 = head + 1); answers the
2773    /// head as `LogHead` does. Refused on a twin.
2774    fn log_retain(&self, _from: u64) -> ControlResult<gunnar_proto::LogHeadResponse> {
2775        Err(ControlError::Unimplemented("retention: this control plane keeps no log"))
2776    }
2777
2778    /// ★ **The log's segments, for a byte-bounded retention** (cov-13, 2026-10-06):
2779    /// `(first, last, bytes)`, oldest first, the open one last (`last = first - 1` while it
2780    /// holds nothing). Only whole segments are dropped, so this is what a byte budget is
2781    /// decided over. Default `Unimplemented`: a plane with no log.
2782    fn log_layout(&self) -> ControlResult<Vec<(u64, u64, u64)>> {
2783        Err(ControlError::Unimplemented("retention: this control plane keeps no log"))
2784    }
2785
2786    /// ★ **The data volume the log lives on**: `(total, used)` bytes, statvfs — what the
2787    /// retention's byte budget is a share of, and what a twin silent through main's growth
2788    /// is owed (cov-13). `None` when unknown, and while the set is released for a growth
2789    /// (statvfs would read the box's root, not the set).
2790    fn data_volume(&self) -> Option<(u64, u64)> {
2791        None
2792    }
2793
2794    /// ★ `twin-owed.json` (cov-13): what each twin is owed, as last written; empty = never.
2795    /// Default `Unimplemented`: the book then lives in memory only.
2796    fn twin_owed_read(&self) -> ControlResult<Vec<u8>> {
2797        Err(ControlError::Unimplemented("twin-owed: this control plane keeps no data root"))
2798    }
2799
2800    /// ★ Write `twin-owed.json` (replace, crash-safe).
2801    fn twin_owed_write(&self, _bytes: &[u8]) -> ControlResult<()> {
2802        Err(ControlError::Unimplemented("twin-owed: this control plane keeps no data root"))
2803    }
2804
2805    /// `Bootstrap.Restore`, one snapshot piece (`seq` 0), staged on disk: `Some(at)` once the
2806    /// terminator installed the snapshot, so the restore continues from `at + 1`.
2807    fn restore_snapshot_chunk(&self, _chunk: &gunnar_proto::LogChunk) -> ControlResult<Option<u64>> {
2808        Err(ControlError::Unimplemented("restore: this control plane keeps no log to restore into"))
2809    }
2810
2811    // ── Outbound webhooks: delivery to an external URL ─────────────────────
2812    //
2813    // A webhook is a subscriber to the SAME event bus the replicator and the SSE
2814    // fan-out consume (one implementation), which POSTs each matching event to a URL, signed
2815    // with an sshsig the receiver verifies against the server's published key.
2816    // Configuration lives in `<data-root>/webhooks.json` and is set over the
2817    // control plane because the appliance has no shell — exactly the reason the
2818    // replication verbs above exist. Default `Unimplemented`, so a server built
2819    // without the `webhooks` feature says so rather than silently accepting a
2820    // configuration it will never deliver on (measured, not asserted — the silent green).
2821
2822    /// Add or replace one endpoint by name, persist it, and start delivering.
2823    fn webhook_add(&self, _request: AddWebhookRequest) -> ControlResult<WebhookListResponse> {
2824        Err(ControlError::Unimplemented(
2825            "outbound webhooks: this server was built without the `webhooks` feature, so it              cannot be given an outbound endpoint",
2826        ))
2827    }
2828
2829    /// What is configured, plus the signing public key a receiver verifies with.
2830    fn webhook_list(&self) -> ControlResult<WebhookListResponse> {
2831        Err(ControlError::Unimplemented(
2832            "outbound webhooks: this server was built without the `webhooks` feature, so it              has no outbound endpoints",
2833        ))
2834    }
2835
2836    /// Drop one endpoint by name, persist that, and stop delivering to it.
2837    fn webhook_remove(&self, _name: &str) -> ControlResult<WebhookListResponse> {
2838        Err(ControlError::Unimplemented(
2839            "outbound webhooks: this server was built without the `webhooks` feature, so it              has no outbound endpoint to remove",
2840        ))
2841    }
2842
2843    /// **The owner's enterprise list** (2026-10-05): the account audit log,
2844    /// the operator's aggregates and look, the name filter
2845    /// ([`crate::enterprise::EnterprisePlane`]), when this plane has the
2846    /// stores behind it. `None`: nothing is recorded, nothing is shielded,
2847    /// every name is clean, and every `Enterprise` verb answers UNIMPLEMENTED.
2848    fn enterprise(&self) -> Option<&dyn crate::enterprise::EnterprisePlane> {
2849        None
2850    }
2851}
2852
2853// ───────────────────────────────────────────────────────────────────────────
2854// The test double
2855// ───────────────────────────────────────────────────────────────────────────
2856
2857/// An in-memory [`ControlPlane`]. **A test double, not a backend.**
2858///
2859/// It implements the catalogue and identity halves so the services can be
2860/// exercised end to end, and answers [`ControlError::Unimplemented`] for
2861/// everything that genuinely needs `gunnar-store`: refs, objects, diffs and
2862/// mirror sync. Returning an empty success for those would be worse than
2863/// useless: an empty ref list looks exactly like a working server with no refs
2864/// (measured, not asserted — the silent green).
2865#[derive(Debug, Default)]
2866pub struct MemoryControlPlane {
2867    inner: Mutex<Inner>,
2868    /// **A slow synchronous call, on demand** (R5.1-G): milliseconds every
2869    /// `bootstrap_state` sleeps on the calling thread before it answers — the
2870    /// shape of a real control-plane call (a disk walk, an fsync) that holds a
2871    /// worker without yielding. Zero, the default, answers at once.
2872    stall_ms: std::sync::atomic::AtomicU64,
2873}
2874
2875/// One seat lot: `(room per seat, total, filled ids, order)`.
2876type SeatLot = (u64, u32, Vec<String>, String);
2877
2878#[derive(Debug, Default)]
2879struct Inner {
2880    namespaces: BTreeMap<String, Namespace>,
2881    repos: BTreeMap<(String, String), Repo>,
2882    principals: BTreeMap<String, Principal>,
2883    /// Employee id → the account that enrolled it (`enroll_employee`).
2884    employers: BTreeMap<String, String>,
2885    /// Employee id → the room it holds and the order it came with.
2886    rooms: BTreeMap<String, (u64, String)>,
2887    /// Account → its seat lots `(room per seat, total, filled ids, order)`.
2888    seats: BTreeMap<String, Vec<SeatLot>>,
2889    /// Order → `(account, principal or "", seats, room, need)`.
2890    room_orders: BTreeMap<String, (String, String, u32, u64, u64)>,
2891    /// namespace → what a test says gunnar measured for it (`usage`).
2892    /// Empty in every other use, and then `usage` answers `Unimplemented`
2893    /// exactly as before: the double has no store to count.
2894    usage: BTreeMap<String, gunnar_proto::NamespaceUsage>,
2895    /// Web credentials (Stage L): credential id (base64url) → the row and
2896    /// its public key.
2897    credentials: BTreeMap<String, StoredCredential>,
2898    /// Web credential (base64url) → the session keys its logins enrolled.
2899    credential_sessions: BTreeMap<String, Vec<String>>,
2900    /// Invites (Stage L): id → the row and the secret's hash.
2901    invites: BTreeMap<String, (gunnar_proto::Invite, String)>,
2902    /// Redeemed invites: the secret's hash (the plane's USED answer).
2903    redeemed_invites: std::collections::BTreeSet<String>,
2904    keys: BTreeMap<String, PublicKey>,
2905    grants: Vec<Grant>,
2906    protections: BTreeMap<(String, String), BTreeMap<String, BranchProtection>>,
2907    ref_policies: BTreeMap<(String, String), RefNamePolicy>,
2908    mirrors: BTreeMap<(String, String), MirrorConfig>,
2909    /// fingerprint → the `authorized_keys` line, so a credential can be
2910    /// verified. The proto `PublicKey` deliberately does not carry key
2911    /// material; the gate needs it, so the double keeps it beside.
2912    openssh: BTreeMap<String, String>,
2913    /// Client-certificate DER → the identity it was registered under.
2914    ///
2915    /// Keyed by the **bytes**, not by a fingerprint the double computed: the
2916    /// real adapter hashes with `gunnar_core::auth::fingerprint_of_certificate`
2917    /// and a second hash spelled here would be a twin of that decision (one implementation).
2918    /// The double is handed the fingerprint by whoever registers, so the two
2919    /// can never disagree — there is only one of them.
2920    certificates: BTreeMap<Vec<u8>, CertificateIdentity>,
2921    /// `Scope::Server` grants (the ACL model). The proto `Grant` cannot express one, since
2922    /// it requires a namespace, which is a real gap recorded in
2923    /// `gunnar-cli`'s adapter; the double works around it rather than
2924    /// pretending a server-wide grant is a namespace grant on `""`.
2925    server_grants: BTreeMap<String, GrantLevel>,
2926    /// Narrowed principals → the methods each may call (f-principal-reach).
2927    narrowed: BTreeMap<String, Vec<String>>,
2928    /// See [`MemoryControlPlane::force_may`]. `None` in every real use.
2929    forced_may: Option<bool>,
2930    /// See [`MemoryControlPlane::set_twin`]. `false` unless a test says otherwise.
2931    twin: bool,
2932    /// `set_log_follows`: a main being restored.
2933    log_follows: bool,
2934    /// See [`MemoryControlPlane::await_enrolment`]: fingerprint → the fed key's
2935    /// OpenSSH line. Empty unless a test feeds one.
2936    awaiting: BTreeMap<String, String>,
2937    /// See [`MemoryControlPlane::forge_principal_id`]. `None` in every real use.
2938    forged_principal_id: Option<String>,
2939    /// The bootstrap record: `(at, principal, fingerprint)`. Beside the
2940    /// identity maps above and never removed with them, which is the whole
2941    /// property the double has to reproduce.
2942    bootstrap: Option<(i64, String, String)>,
2943    /// namespace → the fact this box holds for it (the METERED appliance).
2944    ///
2945    /// Seeded by a test; empty in every other use. `entitlement_get` answers
2946    /// out of it, which is the truthful thing for a double to do — "no fact"
2947    /// is Free, and that is a real answer rather than a missing one. Writing a
2948    /// fact is still [`ControlError::Unimplemented`]: the double has no
2949    /// monetize key to VERIFY one with, and a `set` that stored whatever it
2950    /// was given would be a double that is more permissive than the thing it
2951    /// stands in for.
2952    #[cfg(feature = "metered")]
2953    entitlements: BTreeMap<String, gunnar_proto::EntitlementFact>,
2954    /// How many times the actor-ticket key has been asked to sign.
2955    ///
2956    /// **An instrument, and the reason it exists is a specific defect.** A
2957    /// handler that signed a ticket and then checked the grant would satisfy a
2958    /// test that only looked at the returned `Status`, while having put a
2959    /// valid, spendable ticket for somebody else's tenant into whatever sits
2960    /// between the handler and the caller. Counting the signatures is how a
2961    /// refusal is held to "nothing was minted" rather than to "nothing was
2962    /// returned".
2963    #[cfg(feature = "metered")]
2964    actor_tickets_signed: u64,
2965    /// The double's actor-ticket key, minted on first use, exactly as the real
2966    /// appliance's is — a real ed25519 key and never a stand-in, because the
2967    /// signature it makes is checked by the OTHER repository's verifier in
2968    /// `tests/billing.rs`.
2969    #[cfg(feature = "metered")]
2970    actor_ticket_key: Option<ed25519_dalek::SigningKey>,
2971}
2972
2973impl Inner {
2974    /// The double's actor-ticket key, minted on first use.
2975    ///
2976    /// Minted from `OsRng` and not from a constant seed. A fixed key would make
2977    /// every run of `tests/billing.rs` produce byte-identical tickets, which
2978    /// reads as reassuring and is the opposite: the contract test would then
2979    /// be checking one recorded signature rather than the agreement between a
2980    /// signer and a verifier.
2981    #[cfg(feature = "metered")]
2982    fn actor_ticket_key(inner: &mut Inner) -> &ed25519_dalek::SigningKey {
2983        inner.actor_ticket_key.get_or_insert_with(|| {
2984            use rand::Rng as _;
2985            let mut seed = [0u8; 32];
2986            rand::rand_core::UnwrapErr(rand::rngs::SysRng).fill_bytes(&mut seed);
2987            ed25519_dalek::SigningKey::from_bytes(&seed)
2988        })
2989    }
2990
2991    /// Who, other than `own_id`, holds `name` — as their name or as their id.
2992    /// The double's spelling of `AuthTable::refuse_taken_name`.
2993    fn name_holder(inner: &Inner, name: &str, own_id: &str) -> Option<String> {
2994        inner
2995            .principals
2996            .values()
2997            .find(|p| p.id != own_id && (p.name == name || p.id == name))
2998            .map(|p| p.id.clone())
2999    }
3000}
3001
3002/// The default branch a repo gets when the request does not name one.
3003pub const DEFAULT_BRANCH: &str = "refs/heads/main";
3004
3005/// sha256 is the default hash kind for new repos.
3006pub const DEFAULT_HASH_KIND: HashKind = HashKind::Sha256;
3007
3008impl MemoryControlPlane {
3009    /// **Say this plane is a twin** (T21): what `CoreControlPlane::is_twin` reads off
3010    /// `twin.json`, for the gate's twin-only grow verbs.
3011    pub fn set_twin(&self, yes: bool) {
3012        self.inner.lock().expect("memory control plane").twin = yes;
3013    }
3014
3015    /// **Say this plane's log follows another's** (`ControlPlane::log_follows`):
3016    /// a main being restored, as `CoreControlPlane` reads it off its txlog.
3017    pub fn set_log_follows(&self, yes: bool) {
3018        self.inner.lock().expect("memory control plane").log_follows = yes;
3019    }
3020
3021    /// **Say this key was fed and is not enrolled yet** (`Identity::awaiting_enrolment`):
3022    /// what a primary's installer feed says of the front's keys until its pairing loop
3023    /// enrolled them.
3024    pub fn await_enrolment(&self, fingerprint: &str, openssh: &str) {
3025        self.inner.lock().expect("memory control plane").awaiting.insert(fingerprint.to_owned(), openssh.to_owned());
3026    }
3027
3028    pub fn new() -> Self {
3029        Self::default()
3030    }
3031
3032    /// Make every `bootstrap_state` (`Bootstrap.Status`) hold its thread for
3033    /// `by` before it answers: a synchronous call that blocks the worker it
3034    /// runs on, which is what the keepalive test needs every handler worker
3035    /// doing at once.
3036    pub fn stall_bootstrap_state(&self, by: std::time::Duration) {
3037        self.stall_ms.store(
3038            by.as_millis().min(u64::MAX as u128) as u64,
3039            std::sync::atomic::Ordering::SeqCst,
3040        );
3041    }
3042
3043    /// Convenience for tests: a namespace and a repo in one call.
3044    pub fn with_repo(namespace: &str, repo: &str) -> Self {
3045        let plane = Self::new();
3046        plane
3047            .create_namespace(namespace, "")
3048            .expect("fresh namespace");
3049        plane
3050            .create_repo(NewRepo {
3051                path: RepoPath::new(namespace, repo),
3052                hash_kind: DEFAULT_HASH_KIND,
3053                default_branch: String::new(),
3054                visibility: Visibility::Private,
3055                description: String::new(),
3056                adopt: false,
3057            })
3058            .expect("fresh repo");
3059        plane
3060    }
3061
3062    fn key(path: &RepoPath) -> (String, String) {
3063        (path.namespace.clone(), path.repo.clone())
3064    }
3065
3066    /// **Narrow a principal to a method list** (f-principal-reach).
3067    pub fn narrow(&self, principal_id: &str, methods: &[&str]) {
3068        self.inner.lock().unwrap().narrowed.insert(principal_id.to_owned(), methods.iter().map(|m| m.to_string()).collect());
3069    }
3070
3071    /// Give a principal a `Scope::Server` grant (the ACL model).
3072    ///
3073    /// Not on [`ControlPlane`]: the proto `Grant` cannot express a server-wide
3074    /// scope (it requires a namespace), which is a gap in `gunnar-proto`
3075    /// recorded in `gunnar-cli`'s adapter. Until that is fixed the double
3076    /// carries server grants beside the namespace/repo ones rather than
3077    /// smuggling one in as a namespace grant on `""`.
3078    pub fn grant_server(&self, principal_id: &str, level: GrantLevel) {
3079        self.inner
3080            .lock()
3081            .unwrap()
3082            .server_grants
3083            .insert(principal_id.to_owned(), level);
3084    }
3085
3086    /// Seed what gunnar measured for one namespace (`Usage.Read`'s row).
3087    pub fn set_usage(&self, usage: gunnar_proto::NamespaceUsage) {
3088        self.inner
3089            .lock()
3090            .unwrap()
3091            .usage
3092            .insert(usage.namespace.clone(), usage);
3093    }
3094
3095    /// Seed the fact this box holds for one namespace (the METERED appliance).
3096    ///
3097    /// The double's stand-in for a fact monetize signed and `Entitlement.Set`
3098    /// verified and stored. It takes the fact as given and checks no signature,
3099    /// which is why it is a test-only setter and not `entitlement_set`.
3100    #[cfg(feature = "metered")]
3101    pub fn set_entitlement_fact(&self, fact: gunnar_proto::EntitlementFact) {
3102        self.inner
3103            .lock()
3104            .unwrap()
3105            .entitlements
3106            .insert(fact.namespace.clone(), fact);
3107    }
3108
3109    /// **How many times the actor-ticket key has been asked to sign.**
3110    ///
3111    /// The instrument that turns "the request was refused" into "nothing was
3112    /// minted". See the field's own comment for the defect it exists to catch.
3113    #[cfg(feature = "metered")]
3114    pub fn actor_tickets_signed(&self) -> u64 {
3115        self.inner.lock().unwrap().actor_tickets_signed
3116    }
3117
3118    /// **A red-first instrument.** Force [`Identity::may`] to a constant answer.
3119    ///
3120    /// `Some(false)` must make every *allow* test in `tests/auth.rs` go red,
3121    /// and `Some(true)` must make every *permission-denied* test go red. Both
3122    /// are asserted there. Without them a handler that had been hard-wired to
3123    /// allow, or a gate that refused everything, would pass a naive suite just
3124    /// as well as a correct one. That is the exact failure this instrument
3125    /// exists to make impossible.
3126    ///
3127    /// `None` restores the real answer.
3128    pub fn force_may(&self, forced: Option<bool>) {
3129        self.inner.lock().unwrap().forced_may = forced;
3130    }
3131
3132    /// **A red-first instrument, for anonymous read.** Make
3133    /// [`Identity::registered_key`] name
3134    /// a principal of the caller's choosing, whatever the key table says.
3135    ///
3136    /// `AuthGate` refuses a credential that resolves to the anonymous principal
3137    /// id. Through `gunnar-core` that state is unreachable (the reserved id
3138    /// cannot be registered and cannot be given a key), so without this the
3139    /// guard could never be *seen* refusing, and a guard nobody has watched
3140    /// fail is worth nothing.
3141    ///
3142    /// Nor is it a contrivance. `AuthGate` takes [`ControlPlane`] as a
3143    /// **trait**, so any adapter, present or future, can return whatever
3144    /// principal id it likes. That is the situation this reproduces.
3145    ///
3146    /// `None` restores the real answer.
3147    pub fn forge_principal_id(&self, id: Option<&str>) {
3148        self.inner.lock().unwrap().forged_principal_id = id.map(str::to_owned);
3149    }
3150}
3151
3152/// `Admin` implies `Write` implies `Read` (the ACL model). One ordering, used by every
3153/// comparison below.
3154fn level_rank(level: GrantLevel) -> u8 {
3155    match level {
3156        GrantLevel::Unspecified => 0,
3157        GrantLevel::Read => 1,
3158        GrantLevel::Write => 2,
3159        GrantLevel::Admin => 3,
3160    }
3161}
3162
3163/// The **test double's** ACL.
3164///
3165/// It is a double of `gunnar_core::AuthTable::may`, not a second production
3166/// decision: the real server routes every `may` through `AuthTable` via the
3167/// adapter, and this exists so the seven services can be exercised without a
3168/// storage backend. Its rules are deliberately the same three: unknown
3169/// principal may nothing, a covering grant must be at least `level`, and a
3170/// namespace grant matches its namespace **exactly** (`team` does not reach
3171/// `team/sub`).
3172impl Identity for MemoryControlPlane {
3173    fn awaiting_enrolment(&self, fingerprint: &str) -> Option<String> {
3174        self.inner.lock().unwrap().awaiting.get(fingerprint).cloned()
3175    }
3176
3177    fn narrowed_methods(&self, principal_id: &str) -> ControlResult<Option<Vec<String>>> {
3178        Ok(self.inner.lock().unwrap().narrowed.get(principal_id).cloned())
3179    }
3180
3181    fn registered_key(&self, fingerprint: &str) -> ControlResult<Option<RegisteredKey>> {
3182        let inner = self.inner.lock().unwrap();
3183        let Some(key) = inner.keys.get(fingerprint) else {
3184            return Ok(None);
3185        };
3186        if let Some(expires) = key.expires_at_unix_ms {
3187            if now_ms() >= expires {
3188                return Ok(None);
3189            }
3190        }
3191        let Some(openssh) = inner.openssh.get(fingerprint) else {
3192            return Ok(None);
3193        };
3194        Ok(Some(RegisteredKey {
3195            principal_id: inner
3196                .forged_principal_id
3197                .clone()
3198                .unwrap_or_else(|| key.principal_id.clone()),
3199            openssh: openssh.clone(),
3200            machine: inner
3201                .principals
3202                .get(&key.principal_id)
3203                .is_some_and(|p| p.kind == PrincipalKind::Machine as i32),
3204        }))
3205    }
3206
3207    fn registered_certificate(&self, der: &[u8]) -> ControlResult<Option<CertificateIdentity>> {
3208        let inner = self.inner.lock().unwrap();
3209        Ok(inner.certificates.get(der).cloned())
3210    }
3211
3212    fn may(&self, principal_id: &str, access: &Access, level: GrantLevel) -> ControlResult<bool> {
3213        if level == GrantLevel::Unspecified {
3214            return Ok(false);
3215        }
3216        let inner = self.inner.lock().unwrap();
3217        if let Some(forced) = inner.forced_may {
3218            return Ok(forced);
3219        }
3220        if !inner.principals.contains_key(principal_id) {
3221            return Ok(false);
3222        }
3223        // A principal is intrinsically admin of the account named after it.
3224        // `gunnar_core::AuthTable::owns_account` is the real decision; this is
3225        // the double keeping the same shape, and `tests/bootstrap.rs` asserts
3226        // both directions of it so the double cannot quietly allow more.
3227        // A MEMBER owns no account by its name (2026-10-07), as in the table.
3228        let member = inner.employers.contains_key(principal_id);
3229        let owns_account = |namespace: &str| !member && !namespace.is_empty() && namespace == principal_id;
3230        match access {
3231            Access::Namespace(name) if owns_account(name) => return Ok(true),
3232            Access::Repo(path) if owns_account(&path.namespace) => return Ok(true),
3233            _ => {}
3234        }
3235        // A PUBLIC repository is readable by every registered principal —
3236        // `AuthTable::may`'s rule for it, kept here so a double cannot quietly
3237        // hide what the real table shows (the outsider's view of an account,
3238        // `tests/account_model.rs`).
3239        if let Access::Repo(path) = access {
3240            if level == GrantLevel::Read
3241                && inner
3242                    .repos
3243                    .get(&(path.namespace.clone(), path.repo.clone()))
3244                    .is_some_and(|repo| repo.visibility == Visibility::Public as i32)
3245            {
3246                return Ok(true);
3247            }
3248        }
3249        let want = level_rank(level);
3250        // **A server grant stops at a customer's account** (2026-10-05):
3251        // `gunnar_core::AuthTable`'s shield, kept here so the double cannot
3252        // quietly show an operator what the real table refuses it.
3253        // The box's own first identity (bootstrap) is the estate, as a
3254        // machine is: `gunnar_core::AuthTable::is_box_identity`.
3255        let box_identity = inner.bootstrap.as_ref().map(|(_, id, _)| id.as_str());
3256        let machine = box_identity == Some(principal_id)
3257            || inner
3258                .principals
3259                .get(principal_id)
3260                .is_some_and(|p| p.kind == PrincipalKind::Machine as i32);
3261        let shielded = |namespace: &str| {
3262            let account = namespace.split('/').next().unwrap_or("");
3263            !machine
3264                && !account.is_empty()
3265                && box_identity != Some(account)
3266                && account != principal_id
3267                && inner
3268                    .principals
3269                    .get(account)
3270                    .is_some_and(|p| p.kind == PrincipalKind::User as i32)
3271                && !inner.employers.contains_key(account)
3272                && inner.employers.get(principal_id).map(String::as_str) != Some(account)
3273        };
3274        let inside_closed_account = match access {
3275            Access::Server => false,
3276            Access::Namespace(name) => shielded(name),
3277            Access::Repo(path) => shielded(&path.namespace),
3278        };
3279        let server = inner
3280            .server_grants
3281            .get(principal_id)
3282            .copied()
3283            .map(level_rank)
3284            .unwrap_or(0);
3285        if server >= want && !inside_closed_account {
3286            return Ok(true);
3287        }
3288        let scoped = inner
3289            .grants
3290            .iter()
3291            .filter(|grant| grant.principal_id == principal_id)
3292            .filter(|grant| match access {
3293                // A server-scoped question is never answered by a namespace or
3294                // repo grant. Widening it there would be silent escalation.
3295                Access::Server => false,
3296                Access::Namespace(name) => grant.repo.is_empty() && &grant.namespace == name,
3297                Access::Repo(path) => {
3298                    grant.namespace == path.namespace
3299                        && (grant.repo.is_empty() || grant.repo == path.repo)
3300                }
3301            })
3302            .filter_map(|grant| GrantLevel::try_from(grant.level).ok())
3303            .map(level_rank)
3304            .max()
3305            .unwrap_or(0);
3306        Ok(scoped >= want)
3307    }
3308}
3309
3310/// The double's first-boot door.
3311///
3312/// It mirrors `gunnar_core::AuthTable::install_first_identity` in the one thing
3313/// that matters: the record is checked and written **under the same lock** as
3314/// the identity it creates, so two concurrent enrolments cannot both win. In
3315/// the real server that lock is a compare-and-swap on `refs/gunnar/auth`; here
3316/// it is the `Mutex` this whole double is behind. `tests/bootstrap.rs` races
3317/// both.
3318impl FirstBoot for MemoryControlPlane {
3319    fn bootstrap_state(&self) -> ControlResult<BootstrapState> {
3320        let stall = self.stall_ms.load(std::sync::atomic::Ordering::SeqCst);
3321        if stall > 0 {
3322            // Blocking on purpose: `std::thread::sleep`, not a timer, so the
3323            // worker is held exactly as a synchronous store call holds it.
3324            std::thread::sleep(std::time::Duration::from_millis(stall));
3325        }
3326        let inner = self.inner.lock().unwrap();
3327        let (at, principal, fingerprint) =
3328            inner
3329                .bootstrap
3330                .clone()
3331                .unwrap_or((0, String::new(), String::new()));
3332        Ok(BootstrapState {
3333            window_open: inner.bootstrap.is_none()
3334                && inner.principals.is_empty()
3335                && inner.keys.is_empty(),
3336            bootstrapped: inner.bootstrap.is_some(),
3337            bootstrapped_at_unix_ms: at,
3338            bootstrapped_principal: principal,
3339            bootstrapped_fingerprint: fingerprint,
3340            principals: inner.principals.len() as u64,
3341            keys: inner.keys.len() as u64,
3342            // An in-memory control plane has no disk to be racing, so it is
3343            // always serving. The boot window is `gunnar-server`'s fact.
3344            serving_git: true,
3345            readiness: "serving git".to_owned(),
3346            build: String::new(),
3347            role: "primary".to_owned(),
3348            ..Default::default()
3349        })
3350    }
3351
3352    fn enroll_first_identity(
3353        &self,
3354        name: &str,
3355        openssh_public_key: &str,
3356    ) -> ControlResult<Enrolled> {
3357        let fingerprint = fingerprint_of_openssh(openssh_public_key).ok_or_else(|| {
3358            ControlError::InvalidArgument(format!(
3359                "{openssh_public_key:?} is not an authorized_keys line"
3360            ))
3361        })?;
3362        let mut inner = self.inner.lock().unwrap();
3363        if let Some((at, principal, _)) = &inner.bootstrap {
3364            return Err(ControlError::FailedPrecondition(format!(
3365                "this server was bootstrapped at {at} by \"{principal}\"; the window is closed \
3366                 permanently and deleting identities does not reopen it"
3367            )));
3368        }
3369        if !inner.principals.is_empty() || !inner.keys.is_empty() {
3370            return Err(ControlError::FailedPrecondition(format!(
3371                "this server already holds {} principals and {} keys, so it is not a fresh server",
3372                inner.principals.len(),
3373                inner.keys.len()
3374            )));
3375        }
3376        if name.is_empty() {
3377            return Err(ControlError::InvalidArgument("`name` is required".into()));
3378        }
3379        let at = now_ms();
3380        let principal = Principal {
3381            id: name.to_owned(),
3382            name: name.to_owned(),
3383            kind: PrincipalKind::User as i32,
3384            created_at_unix_ms: at,
3385        };
3386        let key = PublicKey {
3387            fingerprint: fingerprint.clone(),
3388            algo: String::new(),
3389            principal_id: name.to_owned(),
3390            comment: String::new(),
3391            added_at_unix_ms: at,
3392            last_used_unix_ms: None,
3393            expires_at_unix_ms: None,
3394        };
3395        inner.principals.insert(name.to_owned(), principal.clone());
3396        inner.keys.insert(fingerprint.clone(), key.clone());
3397        inner
3398            .openssh
3399            .insert(fingerprint.clone(), openssh_public_key.trim().to_owned());
3400        // gunnar-admin, the ordinary `Scope::Server` Admin grant.
3401        inner
3402            .server_grants
3403            .insert(name.to_owned(), GrantLevel::Admin);
3404        inner.namespaces.insert(
3405            name.to_owned(),
3406            Namespace {
3407                name: name.to_owned(),
3408                description: String::new(),
3409                created_at_unix_ms: at,
3410                owner_kind: 0,
3411                ..Default::default()
3412            },
3413        );
3414        inner.bootstrap = Some((at, name.to_owned(), fingerprint));
3415        Ok(Enrolled {
3416            principal,
3417            key,
3418            account: name.to_owned(),
3419            bootstrapped_at_unix_ms: at,
3420            // The in-memory control plane has no PKI; the degraded (empty)
3421            // shape is exactly what it should exercise.
3422            client_cert_pem: String::new(),
3423            client_key_pem: String::new(),
3424            issuer_cert_pem: String::new(),
3425        })
3426    }
3427}
3428
3429fn now_ms() -> i64 {
3430    crate::bus::now_unix_ms()
3431}
3432
3433/// The `SHA256:<base64>` fingerprint OpenSSH prints, computed by `ssh-key`
3434/// itself.
3435///
3436/// This used to be a deliberate `PLACEHOLDER-…` string, because computing
3437/// "something like" a fingerprint and letting an integration believe it would
3438/// have been a lie. Since gRPC authentication landed this crate links
3439/// `ssh-key`, so the double can produce the **real** value. It must, because a
3440/// credential carries the fingerprint the client's key actually has and the
3441/// double has to be able to find it.
3442///
3443/// It is not a second definition of gunnar's fingerprint: `ssh-key`'s own
3444/// `PublicKey::fingerprint` and `gunnar_core::fingerprint_of_blob` are both
3445/// sha256 over the same wire blob, and `gunnar-core` only spells it out by hand
3446/// because russh pins an incompatible `ssh-key`. A mismatch between them
3447/// would be caught the first time a real key was used against a real table.
3448fn fingerprint_of_openssh(line: &str) -> Option<String> {
3449    let key = ssh_key::PublicKey::from_openssh(line).ok()?;
3450    Some(key.fingerprint(ssh_key::HashAlg::Sha256).to_string())
3451}
3452
3453/// **The double's spelling of the key-attestation rule**, structurally.
3454///
3455/// The rule and its arithmetic are `gunnar_core::key_attestation` — one
3456/// writer, which the shipped plane asks. This crate does not depend on
3457/// `gunnar-core` (see the note on `fingerprint_of_openssh`'s neighbour), so the
3458/// double reproduces the three arms — nobody to ask, the caller is the target,
3459/// otherwise required — and checks the attestation's SHAPE: present, about
3460/// this principal and this fingerprint, signed by a key the target holds. It
3461/// does **not** verify the signature; the real record is verified on the real
3462/// plane, and `gunnar-cli/tests/add_key_attested.rs` drives that red. The same
3463/// stance the double takes on membership events, which it does not implement
3464/// at all.
3465fn require_attestation(
3466    target: &str,
3467    caller: &str,
3468    holders: &[String],
3469    adding: &str,
3470    attestation: Option<&SignedKeyAttestation>,
3471) -> ControlResult<()> {
3472    if holders.is_empty() || (!caller.is_empty() && caller == target) {
3473        return Ok(());
3474    }
3475    let Some(signed) = attestation else {
3476        return Err(ControlError::FailedPrecondition(format!(
3477            "principal {target} already holds {} key(s), so adding another needs {target}'s own \
3478             attestation over the new key's fingerprint ({adding})",
3479            holders.len()
3480        )));
3481    };
3482    let Some(subject) = signed.attestation.as_ref() else {
3483        return Err(ControlError::InvalidArgument("`attestation.attestation` is required".into()));
3484    };
3485    if subject.principal_id != target || subject.fingerprint != adding {
3486        return Err(ControlError::InvalidArgument(format!(
3487            "the attestation is for key {} on {:?} but the request adds key {adding} to {target:?}",
3488            subject.fingerprint, subject.principal_id
3489        )));
3490    }
3491    if !holders.iter().any(|held| held == &signed.key_fingerprint) {
3492        return Err(ControlError::InvalidArgument(format!(
3493            "the attestation is signed by {}, which is not a key {target:?} holds",
3494            signed.key_fingerprint
3495        )));
3496    }
3497    Ok(())
3498}
3499
3500impl ControlPlane for MemoryControlPlane {
3501    fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<gunnar_proto::NamespaceUsage>> {
3502        let inner = self.inner.lock().unwrap();
3503        if inner.usage.is_empty() {
3504            return Err(ControlError::Unimplemented("Usage"));
3505        }
3506        Ok(inner
3507            .usage
3508            .values()
3509            .filter(|row| namespace.is_none_or(|ns| row.namespace == ns))
3510            .cloned()
3511            .collect())
3512    }
3513
3514    fn is_twin(&self) -> bool {
3515        self.inner.lock().expect("memory control plane").twin
3516    }
3517
3518    fn log_follows(&self) -> bool {
3519        let inner = self.inner.lock().expect("memory control plane");
3520        inner.twin || inner.log_follows
3521    }
3522
3523    /// The fact this box holds, or `None` = Free.
3524    ///
3525    /// The double answers this one rather than refusing, because it now has
3526    /// somewhere to keep a fact ([`MemoryControlPlane::set_entitlement_fact`])
3527    /// and "no fact" is a real answer and not a missing one. `entitlement_set`
3528    /// stays `Unimplemented`: storing is not the half the double is missing,
3529    /// VERIFYING is, and a double that stored an unverified fact would be more
3530    /// permissive than the thing it stands in for.
3531    #[cfg(feature = "metered")]
3532    fn entitlement_get(
3533        &self,
3534        namespace: &str,
3535    ) -> ControlResult<Option<gunnar_proto::EntitlementFact>> {
3536        Ok(self
3537            .inner
3538            .lock()
3539            .unwrap()
3540            .entitlements
3541            .get(namespace)
3542            .cloned())
3543    }
3544
3545    #[cfg(feature = "metered")]
3546    fn entitlement_list(&self) -> ControlResult<Vec<gunnar_proto::EntitlementFact>> {
3547        Ok(self
3548            .inner
3549            .lock()
3550            .unwrap()
3551            .entitlements
3552            .values()
3553            .cloned()
3554            .collect())
3555    }
3556
3557    /// The double's actor-ticket key, minted on first use and stable after —
3558    /// which is what the real store's `get_or_create_with` gives the appliance,
3559    /// reproduced here under this double's one `Mutex`.
3560    #[cfg(feature = "metered")]
3561    fn actor_ticket_public_key_hex(&self) -> ControlResult<String> {
3562        let mut inner = self.inner.lock().unwrap();
3563        let key = Inner::actor_ticket_key(&mut inner);
3564        Ok(key
3565            .verifying_key()
3566            .to_bytes()
3567            .iter()
3568            .map(|byte| format!("{byte:02x}"))
3569            .collect())
3570    }
3571
3572    #[cfg(feature = "metered")]
3573    fn actor_ticket_sign(&self, message: &[u8]) -> ControlResult<[u8; 64]> {
3574        use ed25519_dalek::Signer as _;
3575        let mut inner = self.inner.lock().unwrap();
3576        let signature = Inner::actor_ticket_key(&mut inner).sign(message).to_bytes();
3577        // Counted AFTER the signature exists, so the number means "a ticket
3578        // this box signed" and not "a call that was attempted".
3579        inner.actor_tickets_signed += 1;
3580        Ok(signature)
3581    }
3582
3583    fn create_namespace(&self, name: &str, description: &str) -> ControlResult<Namespace> {
3584        if name.is_empty() {
3585            return Err(ControlError::InvalidArgument("namespace is empty".into()));
3586        }
3587        let mut inner = self.inner.lock().unwrap();
3588        if inner.namespaces.contains_key(name) {
3589            return Err(ControlError::AlreadyExists(format!("namespace {name}")));
3590        }
3591        let namespace = Namespace {
3592            name: name.to_string(),
3593            description: description.to_string(),
3594            created_at_unix_ms: now_ms(),
3595            owner_kind: 0,
3596            ..Default::default()
3597        };
3598        inner.namespaces.insert(name.to_string(), namespace.clone());
3599        Ok(namespace)
3600    }
3601
3602    fn list_namespaces(&self) -> ControlResult<Vec<Namespace>> {
3603        let inner = self.inner.lock().unwrap();
3604        Ok(inner
3605            .namespaces
3606            .values()
3607            .cloned()
3608            .map(|mut ns| {
3609                ns.employees = inner
3610                    .employers
3611                    .values()
3612                    .filter(|account| **account == ns.name)
3613                    .count() as u32;
3614                ns
3615            })
3616            .collect())
3617    }
3618
3619    fn set_namespace_settings(
3620        &self,
3621        name: &str,
3622        public: Option<bool>,
3623        events_public: Option<bool>,
3624    ) -> ControlResult<Namespace> {
3625        {
3626            let mut inner = self.inner.lock().unwrap();
3627            let ns = inner
3628                .namespaces
3629                .get_mut(name)
3630                .ok_or_else(|| ControlError::NotFound(format!("namespace {name}")))?;
3631            if let Some(public) = public {
3632                ns.public = public;
3633            }
3634            if let Some(events_public) = events_public {
3635                ns.events_public = events_public;
3636            }
3637        }
3638        Ok(self
3639            .namespace(name)?
3640            .expect("the namespace was just written"))
3641    }
3642
3643    fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32> {
3644        let mut inner = self.inner.lock().unwrap();
3645        if !inner.namespaces.contains_key(name) {
3646            return Err(ControlError::NotFound(format!("namespace {name}")));
3647        }
3648        let held: Vec<(String, String)> = inner
3649            .repos
3650            .keys()
3651            .filter(|(ns, _)| ns == name)
3652            .cloned()
3653            .collect();
3654        if !held.is_empty() && !delete_repos {
3655            return Err(ControlError::FailedPrecondition(format!(
3656                "namespace {name} holds {} repos; deleting repos is never implicit",
3657                held.len()
3658            )));
3659        }
3660        for key in &held {
3661            inner.repos.remove(key);
3662            inner.mirrors.remove(key);
3663            inner.protections.remove(key);
3664            inner.ref_policies.remove(key);
3665        }
3666        inner.namespaces.remove(name);
3667        inner.grants.retain(|g| g.namespace != name);
3668        Ok(held.len() as u32)
3669    }
3670
3671    fn create_repo(&self, spec: NewRepo) -> ControlResult<Repo> {
3672        spec.path
3673            .validate()
3674            .map_err(|e| ControlError::InvalidArgument(e.to_string()))?;
3675        let mut inner = self.inner.lock().unwrap();
3676        if !inner.namespaces.contains_key(&spec.path.namespace) {
3677            return Err(ControlError::NotFound(format!(
3678                "namespace {}",
3679                spec.path.namespace
3680            )));
3681        }
3682        let key = MemoryControlPlane::key(&spec.path);
3683        if inner.repos.contains_key(&key) {
3684            return Err(ControlError::AlreadyExists(format!(
3685                "repository {}",
3686                spec.path
3687            )));
3688        }
3689        let hash_kind = if spec.hash_kind == HashKind::Unspecified {
3690            DEFAULT_HASH_KIND
3691        } else {
3692            spec.hash_kind
3693        };
3694        let default_branch = if spec.default_branch.is_empty() {
3695            DEFAULT_BRANCH.to_string()
3696        } else {
3697            spec.default_branch.clone()
3698        };
3699        let visibility = if spec.visibility == Visibility::Unspecified {
3700            Visibility::Private
3701        } else {
3702            spec.visibility
3703        };
3704        let repo = Repo {
3705            path: Some(spec.path.clone()),
3706            hash_kind: hash_kind as i32,
3707            default_branch,
3708            visibility: visibility as i32,
3709            created_at_unix_ms: now_ms(),
3710            description: spec.description,
3711            encrypted: false,
3712        };
3713        inner.repos.insert(key, repo.clone());
3714        Ok(repo)
3715    }
3716
3717    fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo> {
3718        self.inner
3719            .lock()
3720            .unwrap()
3721            .repos
3722            .get(&MemoryControlPlane::key(path))
3723            .cloned()
3724            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))
3725    }
3726
3727    fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>> {
3728        Ok(self
3729            .inner
3730            .lock()
3731            .unwrap()
3732            .repos
3733            .iter()
3734            .filter(|((ns, _), _)| namespace.is_none_or(|want| want == ns))
3735            .map(|(_, repo)| repo.clone())
3736            .collect())
3737    }
3738
3739    /// The test double holds no bytes, so both arms of the real verb collapse
3740    /// to removing the row — but the OUTCOME still says which was asked for,
3741    /// because that is the fact the service reports and a double that always
3742    /// said `Purged` would let a soft-delete regression pass.
3743    fn delete_repo(&self, path: &RepoPath, options: DeleteOptions) -> ControlResult<DeleteOutcome> {
3744        let key = MemoryControlPlane::key(path);
3745        let mut inner = self.inner.lock().unwrap();
3746        if inner.repos.remove(&key).is_none() {
3747            return Err(ControlError::NotFound(format!("repository {path}")));
3748        }
3749        inner.mirrors.remove(&key);
3750        inner.protections.remove(&key);
3751        inner.ref_policies.remove(&key);
3752        inner
3753            .grants
3754            .retain(|g| !(g.namespace == path.namespace && g.repo == path.repo));
3755        Ok(if options.purge {
3756            DeleteOutcome::Purged { bytes_reclaimed: 0 }
3757        } else {
3758            DeleteOutcome::Tombstoned {
3759                purge_after_unix_ms: now_ms()
3760                    + i64::from(options.retention_days.unwrap_or(30)) * 86_400_000,
3761                bytes_kept: 0,
3762            }
3763        })
3764    }
3765
3766    fn set_default_branch(&self, path: &RepoPath, default_branch: &str) -> ControlResult<Repo> {
3767        if default_branch.is_empty() {
3768            return Err(ControlError::InvalidArgument(
3769                "default_branch is empty".into(),
3770            ));
3771        }
3772        let mut inner = self.inner.lock().unwrap();
3773        let repo = inner
3774            .repos
3775            .get_mut(&MemoryControlPlane::key(path))
3776            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
3777        repo.default_branch = default_branch.to_string();
3778        Ok(repo.clone())
3779    }
3780
3781    fn set_visibility(&self, path: &RepoPath, visibility: Visibility) -> ControlResult<Repo> {
3782        if visibility == Visibility::Unspecified {
3783            return Err(ControlError::InvalidArgument(
3784                "visibility unspecified".into(),
3785            ));
3786        }
3787        let mut inner = self.inner.lock().unwrap();
3788        let repo = inner
3789            .repos
3790            .get_mut(&MemoryControlPlane::key(path))
3791            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
3792        repo.visibility = visibility as i32;
3793        Ok(repo.clone())
3794    }
3795
3796    fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo> {
3797        let mut inner = self.inner.lock().unwrap();
3798        let repo = inner
3799            .repos
3800            .get_mut(&MemoryControlPlane::key(path))
3801            .ok_or_else(|| ControlError::NotFound(format!("repository {path}")))?;
3802        repo.encrypted = encrypted;
3803        Ok(repo.clone())
3804    }
3805
3806    fn list_refs(
3807        &self,
3808        path: &RepoPath,
3809        _prefix: &str,
3810        _peel: bool,
3811    ) -> ControlResult<Vec<RefTarget>> {
3812        self.get_repo(path)?;
3813        Err(ControlError::Unimplemented("Refs.List"))
3814    }
3815
3816    fn get_ref(&self, path: &RepoPath, _ref_name: &str, _peel: bool) -> ControlResult<RefTarget> {
3817        self.get_repo(path)?;
3818        Err(ControlError::Unimplemented("Refs.Get"))
3819    }
3820
3821    fn log(
3822        &self,
3823        path: &RepoPath,
3824        _start: &str,
3825        _limit: u32,
3826        _touching: &str,
3827    ) -> ControlResult<Vec<Commit>> {
3828        self.get_repo(path)?;
3829        Err(ControlError::Unimplemented("Refs.Log"))
3830    }
3831
3832    fn create_pull(
3833        &self,
3834        path: &RepoPath,
3835        _author: &str,
3836        _title: &str,
3837        _description: &str,
3838        _source_ref: &str,
3839        _target_ref: &str,
3840    ) -> ControlResult<PullRequest> {
3841        self.get_repo(path)?;
3842        Err(ControlError::Unimplemented("PullRequests.Create"))
3843    }
3844
3845    fn list_pulls(&self, path: &RepoPath, _state: i32) -> ControlResult<Vec<PullRequest>> {
3846        self.get_repo(path)?;
3847        Err(ControlError::Unimplemented("PullRequests.List"))
3848    }
3849
3850    fn get_pull(&self, path: &RepoPath, _number: u64) -> ControlResult<PullRequest> {
3851        self.get_repo(path)?;
3852        Err(ControlError::Unimplemented("PullRequests.Get"))
3853    }
3854
3855    fn close_pull(&self, path: &RepoPath, _number: u64) -> ControlResult<PullRequest> {
3856        self.get_repo(path)?;
3857        Err(ControlError::Unimplemented("PullRequests.Close"))
3858    }
3859
3860    fn merge_pull(
3861        &self,
3862        path: &RepoPath,
3863        _number: u64,
3864        _principal_id: &str,
3865    ) -> ControlResult<(PullRequest, MergedRef)> {
3866        self.get_repo(path)?;
3867        Err(ControlError::Unimplemented("PullRequests.Merge"))
3868    }
3869
3870    fn read_object(
3871        &self,
3872        path: &RepoPath,
3873        _oid: &str,
3874        _max_bytes: u64,
3875    ) -> ControlResult<ReadObjectResponse> {
3876        self.get_repo(path)?;
3877        Err(ControlError::Unimplemented("Refs.ReadObject"))
3878    }
3879
3880    fn diff(
3881        &self,
3882        path: &RepoPath,
3883        _from: &str,
3884        _to: &str,
3885        _path_prefixes: &[String],
3886    ) -> ControlResult<Vec<FileChange>> {
3887        self.get_repo(path)?;
3888        Err(ControlError::Unimplemented("Refs.Diff"))
3889    }
3890
3891    fn add_principal(&self, id: &str, name: &str, kind: PrincipalKind) -> ControlResult<Principal> {
3892        if name.is_empty() || id.is_empty() {
3893            return Err(ControlError::InvalidArgument(
3894                "principal name and id are required".into(),
3895            ));
3896        }
3897        let mut inner = self.inner.lock().unwrap();
3898        if inner.principals.contains_key(id) {
3899            return Err(ControlError::AlreadyExists(format!("principal {id}")));
3900        }
3901        // The same rule as the real table: a name is taken by another name
3902        // AND by another id.
3903        if let Some(holder) = Inner::name_holder(&inner, name, id) {
3904            return Err(ControlError::AlreadyExists(format!(
3905                "the name \"{name}\" is already taken by principal {holder}"
3906            )));
3907        }
3908        let principal = Principal {
3909            id: id.to_string(),
3910            name: name.to_string(),
3911            kind: kind as i32,
3912            created_at_unix_ms: now_ms(),
3913        };
3914        inner.principals.insert(id.to_string(), principal.clone());
3915        Ok(principal)
3916    }
3917
3918    fn enroll_employee(&self, account: &str, ask: &EmployeeAsk) -> ControlResult<EmployeeAnswer> {
3919        let named = ask.seats == 0;
3920        if named && (ask.name.is_empty() || ask.id.is_empty()) {
3921            return Err(ControlError::InvalidArgument(
3922                "an employee's id and name are required".into(),
3923            ));
3924        }
3925        let mut inner = self.inner.lock().unwrap();
3926        if !inner.namespaces.contains_key(account) {
3927            return Err(ControlError::NotFound(format!("account {account}")));
3928        }
3929        let open = |lots: &[(u64, u32, Vec<String>, String)]| -> u32 {
3930            lots.iter()
3931                .map(|(_, total, filled, _)| total.saturating_sub(filled.len() as u32))
3932                .sum()
3933        };
3934        let lots = inner.seats.get(account).cloned().unwrap_or_default();
3935        let total: u32 = lots.iter().map(|(_, total, _, _)| *total).sum();
3936        if !ask.order.is_empty() {
3937            if let Some((owner, principal, _, room, need)) = inner.room_orders.get(&ask.order).cloned() {
3938                if owner != account {
3939                    return Err(ControlError::InvalidArgument(format!(
3940                        "order {} was granted to {owner}, not to {account}",
3941                        ask.order
3942                    )));
3943                }
3944                return Ok(EmployeeAnswer {
3945                    principal: inner.principals.get(&principal).cloned(),
3946                    room_bytes: room,
3947                    seats: total,
3948                    seats_open: open(&lots),
3949                    need_bytes: need,
3950                    unused_bytes: None,
3951                    shortfall_bytes: 0,
3952                    unchanged: true,
3953                });
3954            }
3955        }
3956        if named {
3957            if ask.id == account {
3958                return Err(ControlError::InvalidArgument(format!(
3959                    "{account} is the account itself, not an employee of it"
3960                )));
3961            }
3962            if inner.principals.contains_key(&ask.id)
3963                || inner.principals.values().any(|p| p.name == ask.name)
3964            {
3965                return Err(ControlError::AlreadyExists(format!(
3966                    "principal {} already exists, and an existing principal can never become \
3967                     an employee",
3968                    ask.id
3969                )));
3970            }
3971            if inner.namespaces.contains_key(&ask.id) {
3972                return Err(ControlError::AlreadyExists(format!(
3973                    "namespace {} exists, so an employee may not be called that",
3974                    ask.id
3975                )));
3976            }
3977        }
3978        let lot = if ask.from_seat {
3979            Some(
3980                lots.iter()
3981                    .position(|(_, total, filled, _)| (filled.len() as u32) < *total)
3982                    .ok_or_else(|| {
3983                        ControlError::FailedPrecondition(format!(
3984                            "{account} has no open seat: every seat it holds is filled. Its \
3985                             admin adds users first"
3986                        ))
3987                    })?,
3988            )
3989        } else {
3990            None
3991        };
3992        let room_bytes = lot.map_or(ask.room_bytes, |lot| lots[lot].0);
3993        let need_bytes = match (lot, ask.seats) {
3994            (Some(_), _) => 0,
3995            (None, 0) => room_bytes,
3996            (None, seats) => room_bytes.saturating_mul(u64::from(seats)),
3997        };
3998        let committed = ask.cap.map(|_| {
3999            let promised: u64 = lots
4000                .iter()
4001                .map(|(room, total, filled, _)| room * u64::from(total.saturating_sub(filled.len() as u32)))
4002                .sum::<u64>()
4003                + inner
4004                    .employers
4005                    .iter()
4006                    .filter(|(_, employer)| *employer == account)
4007                    .filter_map(|(id, _)| inner.rooms.get(id).map(|(room, _)| *room))
4008                    .sum::<u64>();
4009            promised.saturating_add(ask.stored)
4010        });
4011        let unused_bytes = ask.cap.zip(committed).map(|(cap, committed)| cap.saturating_sub(committed));
4012        let shortfall_bytes = ask
4013            .cap
4014            .zip(committed)
4015            .map_or(0, |(cap, committed)| need_bytes.saturating_add(committed).saturating_sub(cap));
4016        let mut answer = EmployeeAnswer {
4017            principal: None,
4018            room_bytes,
4019            seats: total,
4020            seats_open: open(&lots),
4021            need_bytes,
4022            unused_bytes,
4023            shortfall_bytes,
4024            unchanged: ask.dry_run || shortfall_bytes > 0,
4025        };
4026        if answer.unchanged {
4027            return Ok(answer);
4028        }
4029        let mut lots = lots;
4030        if named {
4031            let principal = Principal {
4032                id: ask.id.clone(),
4033                name: ask.name.clone(),
4034                kind: PrincipalKind::User as i32,
4035                created_at_unix_ms: now_ms(),
4036            };
4037            inner.principals.insert(ask.id.clone(), principal.clone());
4038            inner.employers.insert(ask.id.clone(), account.to_owned());
4039            inner.grants.push(Grant {
4040                principal_id: ask.id.clone(),
4041                namespace: account.to_owned(),
4042                repo: String::new(),
4043                level: GrantLevel::Read as i32,
4044                ..Default::default()
4045            });
4046            let order = match lot {
4047                Some(lot) => {
4048                    lots[lot].2.push(ask.id.clone());
4049                    lots[lot].3.clone()
4050                }
4051                None => ask.order.clone(),
4052            };
4053            if room_bytes > 0 {
4054                inner.rooms.insert(ask.id.clone(), (room_bytes, order));
4055            }
4056            answer.principal = Some(principal);
4057        } else {
4058            lots.push((room_bytes, ask.seats, Vec::new(), ask.order.clone()));
4059        }
4060        answer.seats = lots.iter().map(|(_, total, _, _)| *total).sum();
4061        answer.seats_open = open(&lots);
4062        if !lots.is_empty() {
4063            inner.seats.insert(account.to_owned(), lots);
4064        }
4065        if !ask.order.is_empty() {
4066            inner.room_orders.insert(
4067                ask.order.clone(),
4068                (
4069                    account.to_owned(),
4070                    if named { ask.id.clone() } else { String::new() },
4071                    ask.seats,
4072                    room_bytes,
4073                    need_bytes,
4074                ),
4075            );
4076        }
4077        Ok(answer)
4078    }
4079
4080    fn seats(&self, account: &str) -> ControlResult<(u32, u32)> {
4081        let inner = self.inner.lock().unwrap();
4082        let lots = inner.seats.get(account).cloned().unwrap_or_default();
4083        Ok((
4084            lots.iter().map(|(_, total, _, _)| *total).sum(),
4085            lots.iter()
4086                .map(|(_, total, filled, _)| total.saturating_sub(filled.len() as u32))
4087                .sum(),
4088        ))
4089    }
4090
4091    fn employees(&self, account: &str) -> ControlResult<Vec<Principal>> {
4092        let inner = self.inner.lock().unwrap();
4093        Ok(inner
4094            .employers
4095            .iter()
4096            .filter(|(_, employer)| *employer == account)
4097            .filter_map(|(id, _)| inner.principals.get(id).cloned())
4098            .collect())
4099    }
4100
4101    /// The double keeps no feed: the operator is the ASKED name, by the same
4102    /// [`operator_name`] rule (a plane-level test of the fed half lives where
4103    /// the feed is, in `gunnar-server`).
4104    /// The double keeps the same rule as the shipped plane: the operator must
4105    /// be a person; once he holds a passkey the bootstrap is spent (and a
4106    /// leftover one withdrawn); otherwise the invite is (re)recorded.
4107    fn operator_bootstrap_invite(
4108        &self,
4109        operator: &str,
4110        secret_sha256: &str,
4111        now_unix_ms: i64,
4112        ttl_ms: i64,
4113    ) -> ControlResult<BootstrapInvite> {
4114        let hash = bootstrap_hash(secret_sha256)?;
4115        let ttl_ms = bootstrap_ttl(ttl_ms)?;
4116        let mut inner = self.inner.lock().unwrap();
4117        let principal = inner
4118            .principals
4119            .get(operator)
4120            .cloned()
4121            .ok_or_else(|| ControlError::NotFound(format!("principal {operator}")))?;
4122        if principal.kind != PrincipalKind::User as i32 {
4123            return Err(ControlError::FailedPrecondition(format!(
4124                "principal {operator} is not a person: the operator's first invite is a person's"
4125            )));
4126        }
4127        if inner
4128            .credentials
4129            .values()
4130            .any(|stored| stored.credential.principal_id == operator)
4131        {
4132            inner.invites.remove(OPERATOR_INVITE_ID);
4133            return Ok(BootstrapInvite::Spent);
4134        }
4135        let renewed = inner
4136            .invites
4137            .get(OPERATOR_INVITE_ID)
4138            .is_some_and(|(row, held)| *held == hash && row.principal_id == operator);
4139        let row = gunnar_proto::Invite {
4140            invite_id: OPERATOR_INVITE_ID.to_owned(),
4141            principal_id: operator.to_owned(),
4142            created_by: "installer".to_owned(),
4143            created_at_unix_ms: now_unix_ms,
4144            expires_at_unix_ms: now_unix_ms + ttl_ms,
4145            note: "the operator's first passkey: the installer's first invite".to_owned(),
4146            server_level: GrantLevel::Unspecified as i32,
4147        };
4148        inner.invites.insert(OPERATOR_INVITE_ID.to_owned(), (row, hash));
4149        Ok(if renewed { BootstrapInvite::Renewed } else { BootstrapInvite::Recorded })
4150    }
4151    fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim> {
4152        let (name, named_by) = operator_name(None, asked)?;
4153        let held = {
4154            let inner = self.inner.lock().unwrap();
4155            inner
4156                .principals
4157                .get(name)
4158                .cloned()
4159                .or_else(|| inner.principals.values().find(|p| p.name == name).cloned())
4160        };
4161        let (principal, created) = match held {
4162            Some(p) if p.kind != PrincipalKind::User as i32 => {
4163                return Err(ControlError::FailedPrecondition(format!(
4164                    "the operator's name {name:?} is held by a principal that is not a person"
4165                )))
4166            }
4167            Some(p) => (p, false),
4168            None => (self.add_principal(name, name, PrincipalKind::User)?, true),
4169        };
4170        let mut inner = self.inner.lock().unwrap();
4171        let adopted_namespace = created && inner.namespaces.contains_key(&principal.id);
4172        if !inner.namespaces.contains_key(&principal.id) {
4173            inner.namespaces.insert(
4174                principal.id.clone(),
4175                Namespace {
4176                    name: principal.id.clone(),
4177                    created_at_unix_ms: now_ms(),
4178                    ..Default::default()
4179                },
4180            );
4181        }
4182        inner
4183            .server_grants
4184            .insert(principal.id.clone(), GrantLevel::Admin);
4185        Ok(OperatorClaim {
4186            principal,
4187            created,
4188            adopted_namespace,
4189            named_by,
4190        })
4191    }
4192
4193    fn list_principals(&self) -> ControlResult<Vec<Principal>> {
4194        Ok(self
4195            .inner
4196            .lock()
4197            .unwrap()
4198            .principals
4199            .values()
4200            .cloned()
4201            .collect())
4202    }
4203
4204    fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>> {
4205        Ok(self
4206            .inner
4207            .lock()
4208            .unwrap()
4209            .principals
4210            .values()
4211            .find(|p| p.name == name)
4212            .cloned())
4213    }
4214
4215    fn add_credential(
4216        &self,
4217        principal_id: &str,
4218        credential_id: &[u8],
4219        public_key: &[u8],
4220        label: &str,
4221        sign_count: u32,
4222    ) -> ControlResult<gunnar_proto::WebCredential> {
4223        let mut inner = self.inner.lock().unwrap();
4224        let principal = inner
4225            .principals
4226            .get(principal_id)
4227            .cloned()
4228            .ok_or_else(|| ControlError::NotFound(format!("principal {principal_id}")))?;
4229        if principal.kind != PrincipalKind::User as i32 {
4230            return Err(ControlError::FailedPrecondition(format!(
4231                "principal {principal_id} is not a person: no human credential on a machine principal"
4232            )));
4233        }
4234        let key = crate::webauthn::b64url(credential_id);
4235        if inner.credentials.contains_key(&key) {
4236            return Err(ControlError::AlreadyExists(format!("web credential {key}")));
4237        }
4238        let credential = gunnar_proto::WebCredential {
4239            credential_id: credential_id.to_vec(),
4240            principal_id: principal_id.to_owned(),
4241            label: label.to_owned(),
4242            created_at_unix_ms: now_ms(),
4243            last_used_unix_ms: None,
4244            sign_count,
4245        };
4246        inner.credentials.insert(
4247            key,
4248            StoredCredential {
4249                credential: credential.clone(),
4250                public_key: public_key.to_vec(),
4251            },
4252        );
4253        Ok(credential)
4254    }
4255
4256    fn credentials(&self, principal_id: Option<&str>) -> ControlResult<Vec<gunnar_proto::WebCredential>> {
4257        Ok(self
4258            .inner
4259            .lock()
4260            .unwrap()
4261            .credentials
4262            .values()
4263            .filter(|c| principal_id.is_none_or(|want| c.credential.principal_id == want))
4264            .map(|c| c.credential.clone())
4265            .collect())
4266    }
4267
4268    fn credential(&self, credential_id: &[u8]) -> ControlResult<Option<StoredCredential>> {
4269        Ok(self
4270            .inner
4271            .lock()
4272            .unwrap()
4273            .credentials
4274            .get(&crate::webauthn::b64url(credential_id))
4275            .cloned())
4276    }
4277
4278    fn revoke_credential(&self, credential_id: &[u8]) -> ControlResult<Option<gunnar_proto::WebCredential>> {
4279        let key = crate::webauthn::b64url(credential_id);
4280        let mut inner = self.inner.lock().unwrap();
4281        let removed = inner.credentials.remove(&key).map(|c| c.credential);
4282        // Its session keys go with it.
4283        for fingerprint in inner.credential_sessions.remove(&key).unwrap_or_default() {
4284            inner.keys.remove(&fingerprint);
4285            inner.openssh.remove(&fingerprint);
4286        }
4287        Ok(removed)
4288    }
4289
4290    fn rename_credential(
4291        &self,
4292        credential_id: &[u8],
4293        label: &str,
4294    ) -> ControlResult<Option<gunnar_proto::WebCredential>> {
4295        let mut inner = self.inner.lock().unwrap();
4296        Ok(inner
4297            .credentials
4298            .get_mut(&crate::webauthn::b64url(credential_id))
4299            .map(|stored| {
4300                stored.credential.label = label.to_owned();
4301                stored.credential.clone()
4302            }))
4303    }
4304
4305    fn create_invite(&self, invite: NewInvite) -> ControlResult<gunnar_proto::Invite> {
4306        let mut inner = self.inner.lock().unwrap();
4307        let principal = inner
4308            .principals
4309            .get(&invite.principal_id)
4310            .cloned()
4311            .ok_or_else(|| ControlError::NotFound(format!("principal {}", invite.principal_id)))?;
4312        if principal.kind != PrincipalKind::User as i32 {
4313            return Err(ControlError::FailedPrecondition(format!(
4314                "principal {} is not a person: no human credential on a machine principal",
4315                invite.principal_id
4316            )));
4317        }
4318        if inner.invites.contains_key(&invite.id) {
4319            return Err(ControlError::AlreadyExists(format!("invite {}", invite.id)));
4320        }
4321        let row = gunnar_proto::Invite {
4322            invite_id: invite.id.clone(),
4323            principal_id: invite.principal_id,
4324            created_by: invite.created_by,
4325            created_at_unix_ms: invite.created_at_unix_ms,
4326            expires_at_unix_ms: invite.expires_at_unix_ms,
4327            note: invite.note,
4328            server_level: invite.server_level as i32,
4329        };
4330        inner.invites.insert(invite.id, (row.clone(), invite.secret_hash));
4331        Ok(row)
4332    }
4333
4334    fn invites(&self) -> ControlResult<Vec<gunnar_proto::Invite>> {
4335        let mut rows: Vec<gunnar_proto::Invite> =
4336            self.inner.lock().unwrap().invites.values().map(|(row, _)| row.clone()).collect();
4337        rows.sort_by(|a, b| (a.created_at_unix_ms, &a.invite_id).cmp(&(b.created_at_unix_ms, &b.invite_id)));
4338        Ok(rows)
4339    }
4340
4341    fn revoke_invite(&self, id: &str) -> ControlResult<Option<gunnar_proto::Invite>> {
4342        Ok(self.inner.lock().unwrap().invites.remove(id).map(|(row, _)| row))
4343    }
4344
4345    fn check_invite(&self, secret_hash: &str, now_unix_ms: i64) -> ControlResult<InviteCheck> {
4346        let inner = self.inner.lock().unwrap();
4347        if let Some((row, _)) = inner.invites.values().find(|(_, held)| held == secret_hash) {
4348            return Ok(if row.expires_at_unix_ms > now_unix_ms { InviteCheck::Valid(row.clone()) } else { InviteCheck::Expired });
4349        }
4350        Ok(if inner.redeemed_invites.contains(secret_hash) { InviteCheck::Used } else { InviteCheck::Unknown })
4351    }
4352
4353    fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool> {
4354        Ok(self.inner.lock().unwrap().server_grants.contains_key(principal_id))
4355    }
4356
4357    fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>> {
4358        Ok(self.inner.lock().unwrap().employers.get(principal_id).cloned())
4359    }
4360
4361    fn web_login(&self, commit: WebLoginCommit) -> ControlResult<WebLoginOutcome> {
4362        let key = crate::webauthn::b64url(&commit.credential_id);
4363        let mut fields = commit.session_openssh.split_whitespace();
4364        let algo = fields.next().unwrap_or_default().to_owned();
4365        let fingerprint = fingerprint_of_openssh(&commit.session_openssh).ok_or_else(|| {
4366            ControlError::InvalidArgument("the session key is not an OpenSSH public key".into())
4367        })?;
4368        let mut inner = self.inner.lock().unwrap();
4369        let mut stored = inner
4370            .credentials
4371            .get(&key)
4372            .cloned()
4373            .ok_or_else(|| ControlError::NotFound(format!("web credential {key}")))?;
4374        let principal = inner
4375            .principals
4376            .get(&stored.credential.principal_id)
4377            .cloned()
4378            .ok_or_else(|| ControlError::NotFound(format!("principal {}", stored.credential.principal_id)))?;
4379        let held = stored.credential.sign_count;
4380        if held != 0 && commit.sign_count != 0 && commit.sign_count <= held {
4381            return Err(ControlError::PermissionDenied(format!(
4382                "the signature counter of web credential {key} went from {held} to {}: a cloned \
4383                 authenticator, or a replay",
4384                commit.sign_count
4385            )));
4386        }
4387        if inner.keys.contains_key(&fingerprint) {
4388            return Err(ControlError::AlreadyExists(format!("key {fingerprint}")));
4389        }
4390        if commit.sign_count != 0 {
4391            stored.credential.sign_count = commit.sign_count;
4392        }
4393        stored.credential.last_used_unix_ms = Some(commit.now_unix_ms);
4394        inner.credentials.insert(key.clone(), stored.clone());
4395        inner
4396            .credential_sessions
4397            .entry(key)
4398            .or_default()
4399            .push(fingerprint.clone());
4400        inner
4401            .openssh
4402            .insert(fingerprint.clone(), commit.session_openssh.trim().to_owned());
4403        inner.keys.insert(
4404            fingerprint.clone(),
4405            PublicKey {
4406                fingerprint: fingerprint.clone(),
4407                algo,
4408                principal_id: principal.id.clone(),
4409                comment: commit.comment,
4410                added_at_unix_ms: commit.now_unix_ms,
4411                last_used_unix_ms: None,
4412                expires_at_unix_ms: Some(commit.expires_at_unix_ms),
4413            },
4414        );
4415        Ok(WebLoginOutcome {
4416            principal,
4417            credential: stored.credential,
4418            fingerprint,
4419        })
4420    }
4421
4422    fn web_register(&self, registration: WebRegistration) -> ControlResult<WebRegistered> {
4423        const INVALID: &str =
4424            "this invite is not valid: it is unknown, already used, expired, or for somebody else";
4425        let mut inner = self.inner.lock().unwrap();
4426        let (target, level, redeemed) = match &registration.invite_secret_hash {
4427            Some(hash) => {
4428                let (row, _) = inner
4429                    .invites
4430                    .values()
4431                    .find(|(row, held)| held == hash && row.expires_at_unix_ms > registration.now_unix_ms)
4432                    .cloned()
4433                    .ok_or_else(|| ControlError::PermissionDenied(INVALID.into()))?;
4434                if registration
4435                    .principal_id
4436                    .as_deref()
4437                    .is_some_and(|asked| asked != row.principal_id)
4438                {
4439                    return Err(ControlError::PermissionDenied(INVALID.into()));
4440                }
4441                (
4442                    row.principal_id.clone(),
4443                    GrantLevel::try_from(row.server_level).unwrap_or(GrantLevel::Unspecified),
4444                    Some(row.invite_id),
4445                )
4446            }
4447            None => (
4448                registration.principal_id.clone().ok_or_else(|| {
4449                    ControlError::InvalidArgument("name the principal, or redeem an invite".into())
4450                })?,
4451                GrantLevel::Unspecified,
4452                None,
4453            ),
4454        };
4455        let principal = inner
4456            .principals
4457            .get(&target)
4458            .cloned()
4459            .ok_or_else(|| ControlError::NotFound(format!("principal {target}")))?;
4460        if principal.kind != PrincipalKind::User as i32 {
4461            return Err(ControlError::FailedPrecondition(format!(
4462                "principal {target} is not a person: no human credential on a machine principal"
4463            )));
4464        }
4465        let key = crate::webauthn::b64url(&registration.credential_id);
4466        if inner.credentials.contains_key(&key) {
4467            return Err(ControlError::AlreadyExists(format!("web credential {key}")));
4468        }
4469        let credential = gunnar_proto::WebCredential {
4470            credential_id: registration.credential_id.clone(),
4471            principal_id: target.clone(),
4472            label: registration.label,
4473            created_at_unix_ms: registration.now_unix_ms,
4474            last_used_unix_ms: None,
4475            sign_count: registration.sign_count,
4476        };
4477        inner.credentials.insert(
4478            key,
4479            StoredCredential {
4480                credential: credential.clone(),
4481                public_key: registration.public_key,
4482            },
4483        );
4484        if let Some(id) = redeemed {
4485            inner.invites.remove(&id);
4486            if let Some(hash) = registration.invite_secret_hash.clone() {
4487                inner.redeemed_invites.insert(hash);
4488            }
4489        }
4490        if level != GrantLevel::Unspecified {
4491            inner.server_grants.insert(target, level);
4492        }
4493        Ok(WebRegistered {
4494            credential,
4495            principal,
4496            server_level: level,
4497        })
4498    }
4499
4500    /// The double keeps the shipped plane's rule, in one lock: the machine
4501    /// made or found (a person under the id refused), the grant, the
4502    /// credential, the approver's counter.
4503    fn approve_machine_enrolment(&self, approval: MachineApproval) -> ControlResult<MachineApproved> {
4504        let mut inner = self.inner.lock().unwrap();
4505        let key = crate::webauthn::b64url(&approval.credential_id);
4506        if inner.credentials.contains_key(&key) {
4507            return Err(ControlError::AlreadyExists(format!("web credential {key}")));
4508        }
4509        let mut step_up = None;
4510        if let Some((id, presented)) = &approval.step_up {
4511            let held = crate::webauthn::b64url(id);
4512            let stored = inner
4513                .credentials
4514                .get(&held)
4515                .cloned()
4516                .ok_or_else(|| ControlError::NotFound(format!("web credential {held}")))?;
4517            let count = stored.credential.sign_count;
4518            if count != 0 && *presented != 0 && *presented <= count {
4519                return Err(ControlError::PermissionDenied(format!(
4520                    "the signature counter of web credential {held} went from {count} to {presented}: \
4521                     a cloned authenticator, or a replay"
4522                )));
4523            }
4524            step_up = Some((held, stored, *presented));
4525        }
4526        let (principal, created) = match inner.principals.get(&approval.machine).cloned() {
4527            Some(p) if p.kind != PrincipalKind::Machine as i32 => {
4528                return Err(ControlError::FailedPrecondition(format!(
4529                    "principal {} is not a machine: a machine's credential never lands on a person",
4530                    approval.machine
4531                )))
4532            }
4533            Some(p) => (p, false),
4534            None => {
4535                if let Some(holder) = Inner::name_holder(&inner, &approval.machine, &approval.machine) {
4536                    return Err(ControlError::AlreadyExists(format!(
4537                        "the name \"{}\" is already taken by principal {holder}",
4538                        approval.machine
4539                    )));
4540                }
4541                let p = Principal {
4542                    id: approval.machine.clone(),
4543                    name: approval.machine.clone(),
4544                    kind: PrincipalKind::Machine as i32,
4545                    created_at_unix_ms: approval.now_unix_ms,
4546                };
4547                inner.principals.insert(p.id.clone(), p.clone());
4548                (p, true)
4549            }
4550        };
4551        if let Some((held, mut stored, presented)) = step_up {
4552            if presented != 0 {
4553                stored.credential.sign_count = presented;
4554            }
4555            stored.credential.last_used_unix_ms = Some(approval.now_unix_ms);
4556            inner.credentials.insert(held, stored);
4557        }
4558        inner
4559            .server_grants
4560            .insert(principal.id.clone(), approval.server_level);
4561        let credential = gunnar_proto::WebCredential {
4562            credential_id: approval.credential_id.clone(),
4563            principal_id: principal.id.clone(),
4564            label: approval.label,
4565            created_at_unix_ms: approval.now_unix_ms,
4566            last_used_unix_ms: None,
4567            sign_count: approval.sign_count,
4568        };
4569        inner.credentials.insert(
4570            key,
4571            StoredCredential {
4572                credential: credential.clone(),
4573                public_key: approval.public_key,
4574            },
4575        );
4576        Ok(MachineApproved {
4577            principal,
4578            credential,
4579            created,
4580        })
4581    }
4582
4583    fn rename_principal(&self, principal_id: &str, name: &str) -> ControlResult<Principal> {
4584        if name.is_empty() {
4585            return Err(ControlError::InvalidArgument("the new name is empty".into()));
4586        }
4587        let mut inner = self.inner.lock().unwrap();
4588        if !inner.principals.contains_key(principal_id) {
4589            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4590        }
4591        if let Some(holder) = Inner::name_holder(&inner, name, principal_id) {
4592            return Err(ControlError::AlreadyExists(format!(
4593                "the name \"{name}\" is already taken by principal {holder}"
4594            )));
4595        }
4596        let row = inner.principals.get_mut(principal_id).expect("checked above");
4597        row.name = name.to_string();
4598        Ok(row.clone())
4599    }
4600
4601    fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)> {
4602        let mut inner = self.inner.lock().unwrap();
4603        inner.employers.remove(principal_id);
4604        if inner.principals.remove(principal_id).is_none() {
4605            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4606        }
4607        let before_keys = inner.keys.len();
4608        inner.keys.retain(|_, k| k.principal_id != principal_id);
4609        let live: Vec<String> = inner.keys.keys().cloned().collect();
4610        inner.openssh.retain(|fp, _| live.contains(fp));
4611        let before_grants = inner.grants.len();
4612        inner.grants.retain(|g| g.principal_id != principal_id);
4613        inner.server_grants.remove(principal_id);
4614        inner
4615            .credentials
4616            .retain(|_, c| c.credential.principal_id != principal_id);
4617        inner
4618            .invites
4619            .retain(|_, (row, _)| row.principal_id != principal_id);
4620        Ok((
4621            (before_keys - inner.keys.len()) as u32,
4622            (before_grants - inner.grants.len()) as u32,
4623        ))
4624    }
4625
4626    fn add_key(
4627        &self,
4628        principal_id: &str,
4629        openssh_public_key: &str,
4630        comment: &str,
4631        expires_at_unix_ms: Option<i64>,
4632        caller: &str,
4633        attestation: Option<SignedKeyAttestation>,
4634    ) -> ControlResult<PublicKey> {
4635        let mut fields = openssh_public_key.split_whitespace();
4636        let (Some(algo), Some(_blob)) = (fields.next(), fields.next()) else {
4637            return Err(ControlError::InvalidArgument(
4638                "not an OpenSSH public key line: expected `<algo> <base64> [comment]`".into(),
4639            ));
4640        };
4641        if !(algo.starts_with("ssh-") || algo.starts_with("ecdsa-") || algo.starts_with("sk-")) {
4642            return Err(ControlError::InvalidArgument(format!(
4643                "unrecognised public key algorithm {algo}"
4644            )));
4645        }
4646        let mut inner = self.inner.lock().unwrap();
4647        // Principal first, then key material: "there is no such principal" is
4648        // the more useful answer, and the ordering is pinned by
4649        // `access_rejects_the_things_that_would_break_the_ssh_side`.
4650        if !inner.principals.contains_key(principal_id) {
4651            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4652        }
4653        let fingerprint = fingerprint_of_openssh(openssh_public_key).ok_or_else(|| {
4654            ControlError::InvalidArgument(format!(
4655                "the {algo} key does not parse as an OpenSSH public key line; the fingerprint \
4656                 the SSH listener will match against cannot be computed from it"
4657            ))
4658        })?;
4659        if inner.keys.contains_key(&fingerprint) {
4660            return Err(ControlError::AlreadyExists(format!("key {fingerprint}")));
4661        }
4662        let holders: Vec<String> = inner
4663            .keys
4664            .values()
4665            .filter(|key| key.principal_id == principal_id)
4666            .filter(|key| key.expires_at_unix_ms.is_none_or(|expires| now_ms() < expires))
4667            .map(|key| key.fingerprint.clone())
4668            .collect();
4669        require_attestation(principal_id, caller, &holders, &fingerprint, attestation.as_ref())?;
4670        inner
4671            .openssh
4672            .insert(fingerprint.clone(), openssh_public_key.trim().to_owned());
4673        let key = PublicKey {
4674            fingerprint: fingerprint.clone(),
4675            algo: algo.to_string(),
4676            principal_id: principal_id.to_string(),
4677            comment: comment.to_string(),
4678            added_at_unix_ms: now_ms(),
4679            last_used_unix_ms: None,
4680            expires_at_unix_ms,
4681        };
4682        inner.keys.insert(fingerprint, key.clone());
4683        Ok(key)
4684    }
4685
4686    fn revoke_key(&self, fingerprint: &str) -> ControlResult<()> {
4687        let mut inner = self.inner.lock().unwrap();
4688        // Both together, by construction: a key whose material outlived its
4689        // record is a credential that still verifies.
4690        inner.openssh.remove(fingerprint);
4691        inner
4692            .keys
4693            .remove(fingerprint)
4694            .map(|_| ())
4695            .ok_or_else(|| ControlError::NotFound(format!("key {fingerprint}")))
4696    }
4697
4698    fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>> {
4699        Ok(self
4700            .inner
4701            .lock()
4702            .unwrap()
4703            .keys
4704            .values()
4705            .filter(|k| principal_id.is_none_or(|want| want == k.principal_id))
4706            .cloned()
4707            .collect())
4708    }
4709
4710    fn grant(
4711        &self,
4712        principal_id: &str,
4713        namespace: &str,
4714        repo: &str,
4715        level: GrantLevel,
4716    ) -> ControlResult<Grant> {
4717        if level == GrantLevel::Unspecified {
4718            return Err(ControlError::InvalidArgument(
4719                "grant level unspecified".into(),
4720            ));
4721        }
4722        if namespace.is_empty() {
4723            return Err(ControlError::InvalidArgument(
4724                "a grant must name a namespace; multi-tenancy has no ungrouped repos".into(),
4725            ));
4726        }
4727        let mut inner = self.inner.lock().unwrap();
4728        if !inner.principals.contains_key(principal_id) {
4729            return Err(ControlError::NotFound(format!("principal {principal_id}")));
4730        }
4731        if !inner.namespaces.contains_key(namespace) {
4732            return Err(ControlError::NotFound(format!("namespace {namespace}")));
4733        }
4734        if !repo.is_empty()
4735            && !inner
4736                .repos
4737                .contains_key(&(namespace.to_string(), repo.to_string()))
4738        {
4739            return Err(ControlError::NotFound(format!(
4740                "repository {namespace}/{repo}"
4741            )));
4742        }
4743        let grant = Grant {
4744            principal_id: principal_id.to_string(),
4745            namespace: namespace.to_string(),
4746            repo: repo.to_string(),
4747            level: level as i32,
4748            granted_at_unix_ms: now_ms(),
4749        };
4750        inner.grants.retain(|g| {
4751            !(g.principal_id == principal_id && g.namespace == namespace && g.repo == repo)
4752        });
4753        inner.grants.push(grant.clone());
4754        Ok(grant)
4755    }
4756
4757    fn revoke(&self, principal_id: &str, namespace: &str, repo: &str) -> ControlResult<()> {
4758        let mut inner = self.inner.lock().unwrap();
4759        let before = inner.grants.len();
4760        inner.grants.retain(|g| {
4761            !(g.principal_id == principal_id && g.namespace == namespace && g.repo == repo)
4762        });
4763        if inner.grants.len() == before {
4764            return Err(ControlError::NotFound(format!(
4765                "grant for {principal_id} on {namespace}/{repo}"
4766            )));
4767        }
4768        Ok(())
4769    }
4770
4771    fn list_grants(
4772        &self,
4773        principal_id: Option<&str>,
4774        namespace: Option<&str>,
4775        repo: Option<&str>,
4776    ) -> ControlResult<Vec<Grant>> {
4777        Ok(self
4778            .inner
4779            .lock()
4780            .unwrap()
4781            .grants
4782            .iter()
4783            .filter(|g| principal_id.is_none_or(|w| w == g.principal_id))
4784            .filter(|g| namespace.is_none_or(|w| w == g.namespace))
4785            .filter(|g| repo.is_none_or(|w| w == g.repo))
4786            .cloned()
4787            .collect())
4788    }
4789
4790    fn get_protection(
4791        &self,
4792        path: &RepoPath,
4793        ref_pattern: Option<&str>,
4794    ) -> ControlResult<Vec<BranchProtection>> {
4795        self.get_repo(path)?;
4796        let inner = self.inner.lock().unwrap();
4797        let Some(rules) = inner.protections.get(&MemoryControlPlane::key(path)) else {
4798            return Ok(Vec::new());
4799        };
4800        Ok(rules
4801            .values()
4802            .filter(|p| ref_pattern.is_none_or(|w| w == p.ref_pattern))
4803            .cloned()
4804            .collect())
4805    }
4806
4807    fn set_protection(
4808        &self,
4809        path: &RepoPath,
4810        protection: BranchProtection,
4811    ) -> ControlResult<BranchProtection> {
4812        if protection.ref_pattern.is_empty() {
4813            return Err(ControlError::InvalidArgument("ref_pattern is empty".into()));
4814        }
4815        self.get_repo(path)?;
4816        let mut inner = self.inner.lock().unwrap();
4817        inner
4818            .protections
4819            .entry(MemoryControlPlane::key(path))
4820            .or_default()
4821            .insert(protection.ref_pattern.clone(), protection.clone());
4822        Ok(protection)
4823    }
4824
4825    fn delete_protection(&self, path: &RepoPath, ref_pattern: &str) -> ControlResult<()> {
4826        self.get_repo(path)?;
4827        let mut inner = self.inner.lock().unwrap();
4828        let removed = inner
4829            .protections
4830            .get_mut(&MemoryControlPlane::key(path))
4831            .and_then(|rules| rules.remove(ref_pattern));
4832        if removed.is_none() {
4833            return Err(ControlError::NotFound(format!(
4834                "protection {ref_pattern} on {path}"
4835            )));
4836        }
4837        Ok(())
4838    }
4839
4840    fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy> {
4841        self.get_repo(path)?;
4842        Ok(self
4843            .inner
4844            .lock()
4845            .unwrap()
4846            .ref_policies
4847            .get(&MemoryControlPlane::key(path))
4848            .cloned()
4849            .unwrap_or_default())
4850    }
4851
4852    fn set_ref_name_policy(
4853        &self,
4854        path: &RepoPath,
4855        policy: RefNamePolicy,
4856    ) -> ControlResult<RefNamePolicy> {
4857        self.get_repo(path)?;
4858        self.inner
4859            .lock()
4860            .unwrap()
4861            .ref_policies
4862            .insert(MemoryControlPlane::key(path), policy.clone());
4863        Ok(policy)
4864    }
4865
4866    // ── forge ──────────────────────────────────────────────────────────────
4867    //
4868    // **Unimplemented, on purpose.** Verifying an `sshsig` is `gunnar-core`'s
4869    // job and this crate does not depend on it; a memory plane that appended
4870    // without verifying would be a second, weaker version of the one rule that
4871    // makes a review event evidence. `Unimplemented` is the honest answer, and
4872    // it is the value that must not quietly become an empty successful
4873    // response — an empty approval list is indistinguishable from a pull
4874    // request nobody has approved, and one of those may merge.
4875    fn forge_append(&self, _event: SignedForgeEvent) -> ControlResult<u64> {
4876        Err(ControlError::Unimplemented("Forge.Append"))
4877    }
4878
4879    fn forge_review(
4880        &self,
4881        _repo: &str,
4882        _pull: u64,
4883    ) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)> {
4884        Err(ControlError::Unimplemented("Forge.GetReview"))
4885    }
4886
4887    fn forge_reviews(&self, _repo: &str) -> ControlResult<Vec<ReviewState>> {
4888        Err(ControlError::Unimplemented("Forge.ListReviews"))
4889    }
4890
4891    fn forge_issue(
4892        &self,
4893        _repo: &str,
4894        _number: u64,
4895    ) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>> {
4896        Err(ControlError::Unimplemented("Forge.GetIssue"))
4897    }
4898
4899    fn forge_issues(&self, _repo: &str) -> ControlResult<Vec<IssueState>> {
4900        Err(ControlError::Unimplemented("Forge.ListIssues"))
4901    }
4902
4903    fn counts(&self) -> ControlResult<ControlCounts> {
4904        let inner = self.inner.lock().unwrap();
4905        Ok(ControlCounts {
4906            namespaces: inner.namespaces.len() as u64,
4907            repos: inner.repos.len() as u64,
4908            principals: inner.principals.len() as u64,
4909            keys: inner.keys.len() as u64,
4910            grants: inner.grants.len() as u64,
4911        })
4912    }
4913
4914    fn storage_info(&self, path: Option<&RepoPath>) -> ControlResult<StorageInfoResponse> {
4915        let repos = match path {
4916            Some(path) => vec![self.get_repo(path)?],
4917            None => self.list_repos(None)?,
4918        };
4919        Ok(StorageInfoResponse {
4920            // The test double holds no bytes and therefore no tombstones.
4921            total_tombstoned_bytes: 0,
4922            tombstoned: Vec::new(),
4923            // **No machine behind this double, so no storage reality.** `None`
4924            // is the honest answer: a fabricated one here would let a console
4925            // render an installation that does not exist, which is the exact
4926            // failure the real implementation refuses by never inferring.
4927            reality: None,
4928            // Honest: this test double has no store behind it. It must not
4929            // claim "tiered(redb,znippy)" and let a UI report a backend that
4930            // is not there.
4931            object_backend: "none (MemoryControlPlane test double)".into(),
4932            ref_backend: "none (MemoryControlPlane test double)".into(),
4933            // No engine behind a test double: none is named.
4934            engine: None,
4935            total_hot_bytes: 0,
4936            total_cold_bytes: 0,
4937            // No store, so no cold tier measured.
4938            cold_measured: false,
4939            // No store behind this double, so no cache and no budget. Zero is
4940            // what is measured, not a plausible number.
4941            total_explode_bytes: 0,
4942            total_explode_budget_bytes: 0,
4943            repos: repos
4944                .into_iter()
4945                .map(|repo| RepoStorage {
4946                    repo: repo.path.clone(),
4947                    hash_kind: repo.hash_kind,
4948                    ..Default::default()
4949                })
4950                .collect(),
4951        })
4952    }
4953
4954    fn mirror_add(
4955        &self,
4956        path: &RepoPath,
4957        remote_url: &str,
4958        prune: bool,
4959        create_repo: bool,
4960    ) -> ControlResult<MirrorConfig> {
4961        if remote_url.is_empty() {
4962            return Err(ControlError::InvalidArgument("remote_url is empty".into()));
4963        }
4964        if self.get_repo(path).is_err() {
4965            if !create_repo {
4966                return Err(ControlError::NotFound(format!("repository {path}")));
4967            }
4968            self.create_repo(NewRepo {
4969                path: path.clone(),
4970                hash_kind: HashKind::Unspecified,
4971                default_branch: String::new(),
4972                visibility: Visibility::Private,
4973                description: format!("mirror of {remote_url}"),
4974                adopt: false,
4975            })?;
4976        }
4977        let mut inner = self.inner.lock().unwrap();
4978        let key = MemoryControlPlane::key(path);
4979        if inner.mirrors.contains_key(&key) {
4980            return Err(ControlError::AlreadyExists(format!("mirror for {path}")));
4981        }
4982        let mirror = MirrorConfig {
4983            repo: Some(path.clone()),
4984            remote_url: remote_url.to_string(),
4985            prune,
4986            state: MirrorState::Idle as i32,
4987            added_at_unix_ms: now_ms(),
4988            last_sync_at_unix_ms: None,
4989            last_error: String::new(),
4990            sync_count: 0,
4991            credential: MirrorCredentialKind::Unspecified as i32,
4992            credential_username: String::new(),
4993            job: None,
4994            repo_pending: false,
4995            lfs_not_copied: false,
4996        };
4997        inner.mirrors.insert(key, mirror.clone());
4998        Ok(mirror)
4999    }
5000
5001    fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig> {
5002        let inner = self.inner.lock().unwrap();
5003        if !inner.mirrors.contains_key(&MemoryControlPlane::key(path)) {
5004            return Err(ControlError::NotFound(format!("mirror for {path}")));
5005        }
5006        // Enqueuing needs the real queue, the real client and a real store to
5007        // write into. Answering with a job that will never run — or with "0
5008        // refs changed", which is what this used to be built to avoid — would
5009        // be a silent green of exactly the kind that proves nothing.
5010        Err(ControlError::Unimplemented("Mirror.Sync"))
5011    }
5012
5013    /// **The credential is recorded here as a KIND and the value is dropped**,
5014    /// which is not a stub: this plane has no sealed store to put it in, and
5015    /// keeping a private key in a test double's `BTreeMap` is a plaintext
5016    /// secret in a process that had no reason to hold one. What the double owes
5017    /// its callers is the `MirrorConfig` shape, and that it gives exactly.
5018    fn mirror_set_credential(
5019        &self,
5020        path: &RepoPath,
5021        kind: MirrorCredentialKind,
5022        secret: &str,
5023        username: &str,
5024    ) -> ControlResult<MirrorConfig> {
5025        credential_arguments(kind, secret)?;
5026        let mut inner = self.inner.lock().unwrap();
5027        let mirror = inner
5028            .mirrors
5029            .get_mut(&MemoryControlPlane::key(path))
5030            .ok_or_else(|| ControlError::NotFound(format!("mirror for {path}")))?;
5031        mirror.credential = kind as i32;
5032        mirror.credential_username = if kind == MirrorCredentialKind::Token {
5033            username.to_owned()
5034        } else {
5035            String::new()
5036        };
5037        Ok(mirror.clone())
5038    }
5039
5040    fn mirror_status(
5041        &self,
5042        namespace: Option<&str>,
5043        path: Option<&RepoPath>,
5044    ) -> ControlResult<Vec<MirrorConfig>> {
5045        Ok(self
5046            .inner
5047            .lock()
5048            .unwrap()
5049            .mirrors
5050            .iter()
5051            .filter(|((ns, _), _)| namespace.is_none_or(|w| w == ns))
5052            .filter(|((ns, repo), _)| path.is_none_or(|w| w.namespace == *ns && w.repo == *repo))
5053            .map(|(_, mirror)| mirror.clone())
5054            .collect())
5055    }
5056
5057    fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()> {
5058        self.inner
5059            .lock()
5060            .unwrap()
5061            .mirrors
5062            .remove(&MemoryControlPlane::key(path))
5063            .map(|_| ())
5064            .ok_or_else(|| ControlError::NotFound(format!("mirror for {path}")))
5065    }
5066}
5067
5068/// **`UsageBatch.ReadMany`'s page token and page cut** (2026-10-06), shared by
5069/// the default [`ControlPlane::usage_page`] and `gunnar-server`'s ledger.
5070///
5071/// The token is opaque to the caller and carries two things: the CURSOR the
5072/// read began at (so every page of one read answers the same cursor, and a
5073/// change that lands mid-read is answered again next time), and the last
5074/// namespace served (the read resumes after it). `v1;<cursor>;<after>` — a
5075/// namespace name cannot hold `;`, and a token that does not parse starts the
5076/// read over rather than failing it.
5077pub mod usage_page_token {
5078    /// `(cursor, after)` out of a token; `("", "")` for the first page or a
5079    /// token this build did not write.
5080    pub fn decode(token: &str) -> (String, String) {
5081        let mut parts = token.splitn(3, ';');
5082        match (parts.next(), parts.next(), parts.next()) {
5083            (Some("v1"), Some(cursor), Some(after)) => (cursor.to_owned(), after.to_owned()),
5084            _ => (String::new(), String::new()),
5085        }
5086    }
5087
5088    /// The token resuming after `after`, for a read that began at `cursor`.
5089    pub fn encode(cursor: &str, after: &str) -> String {
5090        format!("v1;{cursor};{after}")
5091    }
5092
5093    /// **Cut one page** out of rows SORTED by namespace: the rows after
5094    /// `after`, at most `page_size`, and the token to the next page (empty on
5095    /// the last).
5096    pub fn page(
5097        rows: Vec<gunnar_proto::NamespaceUsage>,
5098        after: &str,
5099        page_size: usize,
5100        cursor: String,
5101        changed_only: bool,
5102    ) -> gunnar_proto::ReadManyUsageResponse {
5103        let page_size = page_size.max(1);
5104        let start = if after.is_empty() { 0 } else { rows.partition_point(|r| r.namespace.as_str() <= after) };
5105        let mut rest: Vec<gunnar_proto::NamespaceUsage> = rows.into_iter().skip(start).collect();
5106        let more = rest.len() > page_size;
5107        rest.truncate(page_size);
5108        let next_page_token = match (more, rest.last()) {
5109            (true, Some(last)) => encode(&cursor, &last.namespace),
5110            _ => String::new(),
5111        };
5112        gunnar_proto::ReadManyUsageResponse {
5113            usages: rest,
5114            next_page_token,
5115            cursor,
5116            changed_only,
5117            as_of_unix_ms: crate::bus::now_unix_ms(),
5118        }
5119    }
5120
5121    #[cfg(test)]
5122    mod tests {
5123        use super::*;
5124
5125        fn row(ns: &str) -> gunnar_proto::NamespaceUsage {
5126            gunnar_proto::NamespaceUsage { namespace: ns.into(), ..Default::default() }
5127        }
5128
5129        #[test]
5130        fn pages_resume_after_the_last_namespace_and_the_last_page_has_no_token() {
5131            let rows = || vec![row("a"), row("b"), row("c"), row("d"), row("e")];
5132            let one = page(rows(), "", 2, "E.1.7".into(), true);
5133            assert_eq!(one.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["a", "b"]);
5134            let (cursor, after) = decode(&one.next_page_token);
5135            assert_eq!((cursor.as_str(), after.as_str()), ("E.1.7", "b"));
5136            let two = page(rows(), &after, 2, cursor.clone(), true);
5137            assert_eq!(two.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["c", "d"]);
5138            let (_, after) = decode(&two.next_page_token);
5139            let three = page(rows(), &after, 2, cursor, true);
5140            assert_eq!(three.usages.iter().map(|r| r.namespace.as_str()).collect::<Vec<_>>(), ["e"]);
5141            assert!(three.next_page_token.is_empty(), "the last page says so");
5142            assert_eq!(decode("garbage"), (String::new(), String::new()), "a foreign token starts over");
5143        }
5144    }
5145}