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