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