Skip to main content

CoreControlPlane

Struct CoreControlPlane 

Source
pub struct CoreControlPlane { /* private fields */ }
Expand description

The control plane, backed by the real catalog and identity table.

Implementations§

Source§

impl CoreControlPlane

Source

pub fn new(stores: Arc<ServerStores>) -> Self

Serve out of stores, with the default object ceiling.

Source

pub fn with_restore_keys(self, keys: Vec<String>) -> Self

Keys this box takes a Bootstrap.Restore from (h-v1-6a, 2026-09-28): OpenSSH public key lines, from --restore-pubkey. For a box that no installer feeds — the kit, a drill — this is the only way a restore can be taken at all.

Source

pub fn with_bus(self, bus: Arc<EventBus>) -> Self

Publish this plane’s event-stream events onto bus.

Today that is one event and one producer: MirrorSynced, from the mirror queue’s worker, when a sync actually completes. It is a slot rather than a constructor argument because CoreControlPlane::new is what every test calls and a bus is not something a test of the disk should have to build.

Source

pub fn with_twin_role(self, twin_role: bool) -> Self

This box was started --role twin (serve.rs). Bootstrap.Status then answers role twin before the join, when twin.json does not exist yet and ServerStores::is_twin still says no — the moment an installer door and a re-image verify ask.

Source

pub fn with_git_listener(self, listener: GitListener) -> Self

The address the git listener took, handed in by serve::start from local_addr() beside the address that was asked for.

This is what makes Bootstrap.Status.serving_git a reading of a SOCKET rather than of a boot latch. Per-plane and not a static: two servers in one test process are two listeners, and a process-wide slot would let the second answer for the first.

Source

pub fn release_stale_purge_leases(stores: &ServerStores) -> u64

Release every purge lease this box is still holding.

Called once at start-up, and it is the fast half of the crash-recovery story. A purge that was interrupted by a SIGKILL or a power cut left its lease in the identity table; nothing is running any more, so every one of them is stale by construction — this process has just started and has enqueued nothing.

Without it those accounts stay shut until their leases expire, which is up to gunnar_core::PURGE_LEASE_MS of a paying customer being told their account is locked by a purge that is not happening. The lease is still there as the backstop for the case this cannot cover — a box that never comes back up.

Administrative gates are left alone. They carry no deadline and were set by a person; a restart must not un-suspend an account.

Returns how many were released. Failures are logged and not fatal: a server that refused to start because it could not tidy a lease would turn a recoverable state into an outage.

Source

pub fn resume_interrupted_mirrors(&self) -> usize

Settle every sync a stopped process left in flight, and resume it (import-big, 2026-10-26).

The job queue is memory: a restart in the middle of an import (an out-of-memory kill, an update, a power cut) forgot the job, and what was left was a repository with nothing in it and a registration with no error — a console that showed no failure for an import that never landed. The registration now carries an in-flight mark (MirrorRecord::sync_started_at_unix_ms); here, once at start, each marked sync is settled as interrupted (its last_error says so, and the account’s audit log has a line) and enqueued again, up to gunnar_core::MAX_RESUMES times in a row. Past that it stays FAILED with the reason, for a person to press Sync. Returns how many were resumed.

Source

pub fn with_headroom(self, gate: Arc<DiskHeadroomGate>) -> Self

Hand the disk-headroom gate in. From here on Repos.Create refuses under the floor with RESOURCE_EXHAUSTED and the gate’s own line — the one a refused git push prints, because it is the same function.

Source

pub fn with_grow(self, grow: Arc<GrowMachine>) -> Self

Hand this plane the runtime growth (crate::grow). Called by serve once the admission gate and the stores exist.

Source

pub fn with_admission(self, admission: Arc<Admission>) -> Self

Hand this plane the admission gate the git fronts share, for the in-flight count Mirror.FlushReplication reports.

Source

pub fn replication_view(&self) -> ReplicationView

The slot serve drops the replicator’s counters into, so Admin.Stats can report them.

Without it the counters exist and nothing reads them, which is exactly the state that let every configured standby receive NOTHING from e31ab8f4 to 2026-08-30 while five log lines out of five said so into a file nobody was tailing.

Source

pub fn with_max_object_bytes(self, bytes: u64) -> Self

Set the deployment’s object ceiling for Refs.ReadObject.

0 disables the ceiling, which restores the unbounded behaviour: the server will inflate an object of any size to answer a request. That is a real choice for a single-tenant box with a trusted control port and a repository full of large blobs, and it is spelled explicitly rather than being what you get by forgetting to configure anything.

This exists as much for the tests as for the operator: a ceiling that can only be a compile-time constant can only be exercised by writing an object bigger than it, which for the real default means a 64 MiB fixture. Being able to set it small is what lets the refusal be seen red cheaply — and it proves the configuration is wired, which a test against the constant could never show.

Source§

impl CoreControlPlane

Source

pub fn claim_operator_named( &self, fed: Option<&str>, asked: &str, ) -> ControlResult<OperatorClaim>

★ The estate’s operator, made on its first claim — the one writer behind Accounts.ClaimOperator and behind the primary’s own claim when the installer feeds operator_name ([crate::fed_principals::install]).

fed is the installer’s name for the operator and wins; asked is the caller’s, used only when nothing was fed (gunnar_grpc::seam::operator_name).

  • A PERSON already under that id or name is the answer, unchanged but for two upserts that make it what an operator is: its account namespace exists, and it is gunnar-admin.
  • Anything else under it — a machine, a group — is refused by name: an estate’s parts are never its operator.
  • Otherwise the principal is created, KEYLESS (the console’s credential for the person is its first key, which owes nobody an attestation; the person’s own SSH keys come after and are vouched for by it), with its account namespace — and a namespace of that name that already exists with no owner is ADOPTED rather than refused. MEASURED on test2 2026-10-04: a first push with the installer’s admin key (rickard/t2-scratch) conjured rickard, and AddPrincipal refused the operator’s own account from then on — the first admin’s passkey bound to nothing. Adoption is safe exactly here: the namespace has no principal of its id (this call is creating it), only a server Admin can ask, and that Admin could already delete the namespace or grant it.

Trait Implementations§

Source§

impl ClanPlane for CoreControlPlane

Source§

fn clan_dissolve(&self, clan: &str, by: &str) -> ControlResult<Clan>

The admin’s, and only of an EMPTY clan: no repository in its namespace, live or deleted-and-still-held. The namespace goes first — the catalog refuses a namespace that holds a repository, in its own write, so a repository made a moment ago keeps the clan — and the clan row after it.

Source§

fn clan_take_offer( &self, clan: &str, repo: &str, to: &str, by: &str, ) -> ControlResult<(RepoOffer, Repo)>

The move, then the offer closed. A move that fails leaves the offer open (and the repository where it was) for another try or a decline.

Source§

fn clan_create( &self, name: &str, creator: &str, contribution: u64, room: ClanRoom, ) -> ControlResult<Clan>

Make clan name (and its namespace, private), creator its admin, giving contribution bytes. AlreadyExists for a taken name.
Source§

fn clan_get(&self, name: &str) -> ControlResult<Option<Clan>>

One clan, if there is one.
Source§

fn clans_of(&self, principal: &str) -> ControlResult<Vec<Clan>>

The clans principal is in or invited to.
Source§

fn clan_invite( &self, clan: &str, by: &str, principal: &str, ) -> ControlResult<Clan>

An admin (by) invites principal.
Source§

fn clan_answer( &self, clan: &str, principal: &str, accept: bool, contribution: u64, room: ClanRoom, ) -> ControlResult<Clan>

principal answers its invitation; joining gives contribution.
Source§

fn clan_withdraw( &self, clan: &str, by: &str, principal: &str, ) -> ControlResult<Clan>

An admin withdraws principal’s invitation.
Source§

fn clan_set_contribution( &self, clan: &str, member: &str, bytes: u64, room: ClanRoom, used: Option<u64>, ) -> ControlResult<Clan>

member sets its own contribution; used: what the clan stores (None: not measured, and a lowering is refused).
Source§

fn clan_leave( &self, clan: &str, member: &str, used: Option<u64>, ) -> ControlResult<Clan>

member leaves.
Source§

fn clan_remove( &self, clan: &str, by: &str, member: &str, used: Option<u64>, ) -> ControlResult<Clan>

An admin removes member.
Source§

fn clan_set_admin( &self, clan: &str, by: &str, member: &str, admin: bool, ) -> ControlResult<Clan>

An admin makes member admin, or not.
Source§

fn clan_room_left( &self, account: &str, room: ClanRoom, ) -> ControlResult<Option<u64>>

What account could still give a clan (None: uncapped).
Source§

fn clan_given(&self, account: &str) -> ControlResult<u64>

What account gives all its clans, bytes — its own room is its cap less this (Usage.Read’s allowance, the door’s ceiling).
Source§

fn clan_offer_repo( &self, clan: &str, by: &str, repo: &str, to: &str, ) -> ControlResult<Clan>

The clan’s admin by offers clan/repo to account to. NotFound for a repository the clan does not hold.
Source§

fn clan_repo_offer( &self, clan: &str, repo: &str, ) -> ControlResult<Option<RepoOffer>>

The open offer of clan/repo, with what the repository stores.
Source§

fn clan_offers_to(&self, to: &str) -> ControlResult<Vec<RepoOffer>>

Every open offer to account to.
Source§

fn clan_withdraw_offer( &self, clan: &str, by: &str, repo: &str, ) -> ControlResult<Clan>

The clan’s admin withdraws the offer of clan/repo.
Source§

fn clan_decline_offer( &self, clan: &str, repo: &str, to: &str, ) -> ControlResult<RepoOffer>

Account to declines the offer of clan/repo.
Source§

fn clan_role( &self, principal: &str, namespace: &str, ) -> ControlResult<Option<ClanRole>>

Where principal stands in the clan whose namespace is namespace (None: not a clan, or not in it) — what Repos.Create asks.
Source§

impl ControlPlane for CoreControlPlane

Source§

fn namespace(&self, name: &str) -> ControlResult<Option<Namespace>>

One catalog probe, not the listing the trait’s default walks: this is asked by the account settings and the outsider’s view of a namespace.

Source§

fn create_room(&self, namespace: &str, machine: bool) -> ControlResult<()>

The front doors’ plan chain (ServerStores::plan_gates), asked as a push-to-create asks it: the verdict on a write (a suspended or retained plan: FailedPrecondition), then the cap (ResourceExhausted). A gate that does not bind machines is not asked for one.

Source§

fn delete_repo( &self, path: &RepoPath, options: DeleteOptions, ) -> ControlResult<DeleteOutcome>

Delete: a dated tombstone by default, a reclaim when asked.

Two facts had to be held at once here, and they pull in opposite directions:

  • The reclaim: repo delete must reclaim the disk. Before it, the catalog row went and the objects stayed — 25 GB of a kernel push left on the appliance with its indexer still running, invisible because nothing listed it.
  • Measured: the bytes are gone in ~2 seconds (181 GiB to 33 MiB measured) and a mirror does not help, because the delete replicates. The failure a paying customer is most likely to cause is deleting their own repository.

A tombstone answers both, and only because it is dated and listed: the bytes are held on purpose with a deadline written at the instant of the delete, ServerStores::sweep_expired_tombstones reclaims them when it passes, and Repos.ListDeleted / Admin.StorageInfo report them so they are never the invisible 25 GB again. They still count against the account — see Self::storage_info.

Source§

fn undelete_repo(&self, path: &RepoPath) -> ControlResult<Repo>

Undelete: the row back first, the tombstone off second.

That order is not tidiness. A crash between the two leaves a repository that is catalogued and still fenced off, which an operator can fix with a second undelete; the other order leaves a name with no tombstone and no row, which the next repo create would adopt — and adopting a store a create did not make is exactly the 2026-08-30 resurrection.

Source§

fn gc_repo(&self, path: &RepoPath) -> ControlResult<GcJob>

Repos.Gc — the console’s door onto the maintenance the CLI has always had.

The whole body is a resolve and a call: the maintenance itself is crate::admin::gc_one, the same function gunnar gc runs, so there is one writer and not a second implementation to drift (one implementation).

Unlike Self::purge_repo this one demands the repository be live. A tombstoned store is on its way out and sealing it would spend the I/O of a compaction on bytes a sweep is about to unlink; that is Purge’s job, and saying so by name is better than maintaining a corpse.

Source§

fn purge_account( &self, account: &str, options: PurgeAccountOptions, ) -> ControlResult<(PurgeJob, AccountGate)>

Gate first, enqueue second — and read the gate BACK.

The order is the whole guarantee: by the time this returns the account is shut, so there is no window in which a purge has been accepted and the user can still push into the account it is about to walk.

The gate is then read back out of the table rather than the one just written being returned, and that is not belt-and-braces — it is the only correct answer in two cases this call cannot distinguish up front:

  • the account was already under an administrative gate. The lease is not written over it (gate_for_purge refuses to downgrade), so the gate in force is somebody else’s and the job will not lift it.
  • the account already had a purge queued or running. The queue returns that job instead of this one, so the lease that will be released is the other job’s, and returning this call’s freshly minted id would name a lockout nothing is going to lift.
Source§

fn list_orphans(&self, namespace: Option<&str>) -> ControlResult<Vec<OrphanRow>>

The store directories with no catalog row and no tombstone.

It walks the disk rather than asking the catalog — the same reason Self::list_deleted does, one step further: a tombstone at least exists as a file, and an orphan is defined by the absence of every record. On the appliance 2026-08-30 that absence made ~25-39 GB at alice/linux unreachable by delete, purge, undelete, the sweep AND list-deleted, while create refused the name because the bytes were there.

Source§

fn reclaim_orphan(&self, path: &RepoPath) -> ControlResult<u64>

Reclaim an orphan. FailedPrecondition when it is not one, because “this is a live repository” is a fact the caller can act on and not an internal fault.

Source§

fn recipient_keys(&self, principal_id: &str) -> ControlResult<Vec<RecipientKey>>

A member’s ONE encryption key (CRYPT-YUBI 2026-10-26): a repository’s ring is wrapped to the person’s encryption public key, never to an SSH key. Empty when they have none yet.

Source§

fn archive_out( &self, path: &RepoPath, from_generation: u64, sink: &mut dyn Write, ) -> ControlResult<ArchiveCut>

Repos.ArchiveOut: the same bytes gunnar repo archive-out writes, into a sink the control plane owns — so a cron on another machine can pull a backup off a box that has no shell.

It does not go through Self::open. An archive is three files read out of a directory; taking the handle cache and the store’s lock for it would make a backup contend with serving, and the whole reason there is no freeze verb is that it does not have to. The pack is append-only and fsynced below the cut, so what is read is durable and what arrives after the cut belongs to the next one.

Source§

fn log_all_refs( &self, path: &RepoPath, limit: u32, touching: &str, ) -> ControlResult<Vec<Commit>>

Every branch and tag tip, and HEAD, walked as one history: the History page’s “all branches” view (owner order 2026-10-18), so a commit that lives only on a side branch is drawn too.

Source§

fn merge_pull( &self, path: &RepoPath, number: u64, principal_id: &str, ) -> ControlResult<(PullRequest, MergedRef)>

FAST-FORWARD ONLY, and every step is ordered so a failure leaves the truth on disk: the ancestry is decided by the same gunnar_policy::ancestry::is_descendant_of branch protection trusts (fail-closed: inconclusive is a refusal, never an allow), the ref moves through the store’s own compare-and-swap — the primitive receive-pack uses, so this is not a second ref-write path — and the record is marked merged only AFTER the ref has moved.

Source§

fn read_lfs_object( &self, path: &RepoPath, oid: &str, max_bytes: u64, range: Option<(u64, u64)>, ) -> ControlResult<ReadLfsObjectResponse>

Refs.ReadLfsObject, served out of the repository’s own LFS store.

The same store the SSH adapter and the HTTP batch API serve (gunnar_lfs::FsLfsStore under <git_dir>/lfs), opened the same way transport::open_lfs_store opens it, so there is one place an object lives and one answer to where it is.

§Whole object or nothing, and the ceiling that decides

read_object truncates a git blob past max_bytes, because a prefix of a file is still a file’s beginning. This does not: the caller asked for a PDF, and half a PDF is not a smaller PDF. So the SIZE decides before a byte is read — size_of costs a stat, not an open — and an object over the ceiling is refused with its size in the message, which is what lets the caller go to git lfs instead.

The ceiling is the caller’s max_bytes, lowered to [MAX_RESPONSE_BYTES] because that is what a gRPC client can receive by default. Nothing here allocates from a number the client sent: the allocation is the object’s own size, after that size has been refused or accepted.

§A ranged read (2026-10-15)

With range, the same ceiling bounds a RANGE instead of the object, so a caller with no git lfs — a console handing a browser a 6 MB README picture — reads any object a range at a time, and one that needs only a picture’s header reads only that. The store seeks (open_at), so a range deep into a large object does not read what lies before it. Still never a silent prefix: a range ends early only at the object’s end, which offset + the length against size says.

Source§

fn diff( &self, path: &RepoPath, from: &str, to: &str, path_prefixes: &[String], ) -> ControlResult<Vec<FileChange>>

Refs.Diff, served (crate::diff).

It used to answer Unimplemented — “the server has no tree-diff engine; gix’s blob-diff is client-side porcelain and an S crate must not link it”. The constraint was right and the conclusion was not: a tree diff needs GitServe::read and nothing else, so it costs no crate and the appliance can serve one. crate::diff says what it computes exactly and what it deliberately does not (rename detection).

Source§

fn operator_bootstrap_invite( &self, operator: &str, secret_sha256: &str, now_unix_ms: i64, ttl_ms: i64, ) -> ControlResult<BootstrapInvite>

The fed name is read where the installer left it — the last join reply, <data>/pairing/joined.toml — so it holds across restarts and needs no second record. A build without the pairing loop was fed nothing. ★ The operator’s first invite, kept in the identity table (the twin replicates it): the break-glass token’s hash as operator-bootstrap for the operator’s first passkey, no server grant. The identity core reads it as a fed invite of its own and spends it there (IDENTITY-HOST). Spent — and a leftover one withdrawn — once the operator holds a passkey; otherwise recorded, or renewed for another OPERATOR_INVITE_TTL_MS when the same hash is pending: one update.

Source§

fn set_protection( &self, path: &RepoPath, protection: BranchProtection, ) -> ControlResult<BranchProtection>

Write one protection rule, replacing any rule with the same pattern.

Two things this deliberately does not do, both of them places where the proto and gunnar_policy::ProtectionRule do not line up. Getting either wrong silently is how an operator ends up with a policy they did not write:

  • The per-pattern byte and object limits are refused, not dropped. BranchProtection carries max_object_bytes and max_objects_per_push per rule; gunnar-policy’s LimitPolicy is repository-wide. Storing a per-rule number into a repository-wide limit would apply it to references the caller never named, and ignoring it would report success for a limit that is not enforced. Both are worse than saying so.
  • allow_create and ancestry_budget survive a round trip. They have no proto field, so a rule written over gRPC would otherwise reset whatever a gunnar-policy.json had set — an edit through the console quietly widening a rule it did not display. The existing rule is read first and only the fields the proto carries are overwritten.
Source§

fn stats_extras(&self) -> ControlResult<StatsExtras>

The two numbers an operator is surprised by if nobody reports them.

Tombstoned bytes, because a customer who deletes a repository and sees no change in their usage will call the bill wrong; and replication, because every configured standby on this project received NOTHING from e31ab8f4 until 2026-08-30 and the only thing that said so was a log line nobody was tailing.

Source§

fn issuer_info(&self) -> ControlResult<IssuerInfoResponse>

The PKI document, read (Admin.IssuerInfo) — the ONE implementation, and the same document gunnar issuer show prints.

Through ServerStores::pki_readonly and never pki(): the handle this opens installs a secret store that refuses every call, so the capability to sign is absent from the value rather than absent from whoever remembered not to call create on it. A future edit of this body cannot mint with it.

An unreadable document is ControlError::Unavailable and not an empty answer, for the reason that error exists: “this box has no issuer” and “I could not find out” are different facts, and the first one invites an operator to mint over a CA their whole fleet pins.

Source§

fn audit_trail(&self, at_seq: u64) -> ControlResult<AuditTrailResponse>

The audit chain, read as a shape (Admin.AuditTrail, lane T11).

Opened fresh from ServerStores::audit_path rather than reaching for the live AuditChain the gate holds: the gate’s copy carries an in-memory tip and would answer from it, and the question being asked is about the FILE — whether it still holds what gunnar wrote. So this re-reads and re-verifies, which is also what makes verified: false mean something.

A chain that will not open is not an error: it is present: false with why, because “this box has no durable trail” is exactly the answer an operator needs from a box whose disk filled or whose trail somebody has edited, and turning it into a failed RPC would hide it. The one thing never done here is to answer an empty chain, which would read as “nothing has happened” instead of “I hold no record”.

Source§

fn capacity(&self, request: &CapacityRequest) -> ControlResult<CapacityResponse>

“If a tenant’s cap rose to N bytes, could you serve it?” — Admin.Capacity, the question monetize must ask before it buys a disk.

Three measurements, none of them new (one implementation):

  • the bytes are the disk gate’s own statvfs — the SAME one a push is refused under, so the capacity a seller is quoted and the floor a push dies at cannot disagree;
  • what the tenant already holds is Self::usage’s pack_bytes — the meter caps["pack_bytes"] caps, so the delta is computed against the number the quota is actually about;
  • whether the set can grow is the header band’s /proc/mounts + md sysfs read;
  • the box’s cores and RAM — product scope, the ceilings a seller may sell up to — are crate::capacity::MachineFacts::measure: the available_parallelism the budgets use and the band’s /proc/meminfo, absent (never 0) when unreadable.

crate::capacity::answer decides over them, and is pure so that the SEALED arm can be driven on a developer box where gunnar is never PID 1.

No gate wired is Unimplemented, not a default floor. A plane built without Self::with_headroom does not know the --disk-floor in force, and quoting a seller against a floor this server is not enforcing is worse than declining to answer.

Source§

fn usage(&self, namespace: Option<&str>) -> ControlResult<Vec<NamespaceUsage>>

The meters, per namespace. See usage.proto for what each counts and repo_bytes for the one walk that attributes a directory.

Reads the DISK, not the open handles: Admin.StorageInfo opens every repository it reports (repo_store) to ask for its ref count, which on a server with a thousand repositories is a thousand store opens for one bill. A bill needs bytes, and bytes are on the disk whether or not the store is open — so this walks repo_dir and opens nothing. The one meter that IS about open handles (objects) reads the engine’s live registry, which is a few len() calls and no disk.

Tombstones are charged to the namespace the tombstone names, off the same walk Admin.StorageInfo and Admin.Stats use — a deleted repository’s bytes are on the account until the purge, and a bill that forgot them would be the one an operator cannot explain.

Source§

fn repo_usage(&self, namespace: &str) -> ControlResult<Vec<RepoStored>>

One namespace’s repositories with what each stores — the same per-repository walk Self::usage sums, row by row (see the seam).

Source§

fn usage_page( &self, page_token: &str, page_size: usize, changed_since: &str, ) -> ControlResult<ReadManyUsageResponse>

One page of every namespace’s meters, out of the usage ledger (UsageBatch.ReadMany, 2026-10-06) — re-measuring only what changed since the ledger last looked, and with a cursor answering only the namespaces whose row moved. See crate::usage_ledger.

Source§

fn activity( &self, namespace: &str, since_unix_ms: i64, until_unix_ms: i64, ) -> ControlResult<(Vec<DayActivity>, i64)>

The flow series (Usage.Activity) — see the seam for the contract.

The window is resolved HERE and not in the handler, because this is the half that knows what this server keeps: 0 for since means the whole kept window, 0 for until means now, and both ends are clamped to whole UTC days. A window that runs backwards returns nothing rather than an error — it is an empty question, and an empty answer is what it deserves.

A named namespace must EXIST, for ControlPlane::usage’s reason: answering “no activity” for a namespace that is not in the catalog would let a typo read as a silent tenant, which is the one conclusion this whole ledger exists to make reliable.

Today is flushed before it is read. The live slot holds today, the store holds a copy taken at the last tick, and a series that read only the store would end a minute in the past. Rather than fold the live slot into the answer — a second arithmetic on top of the flush’s — the flush is simply run, which is a copy and cannot double a count.

Source§

fn admit_plan(&self, namespace: &str) -> ControlResult<AdmitPlanResponse>

The admission plan (Usage.AdmitPlan) — see the seam for the contract. A read for the admin view and the tenant, never on the admission path: the policy is asked exactly as the gate asks it, with the usage the gate would plan with, once per kind of work this server admits (request stands for ls-remote and gc).

Source§

fn mirror_add( &self, path: &RepoPath, remote_url: &str, prune: bool, create_repo: bool, ) -> ControlResult<MirrorConfig>

Register an upstream. Bookkeeping only — no fetch happens here.

create_repo is honoured before the registration, because a mirror of a repository that does not exist is a registration nothing can ever act on. Without the flag the repository must already be there, and a missing one is NotFound rather than a silently created empty one.

Source§

fn mirror_sync(&self, path: &RepoPath) -> ControlResult<MirrorConfig>

The one verb that genuinely needs the client half, and the only one still refused.

The original refusal covered all four: “mirroring runs over gunnar-client (nornir-git ls-remote + fetch), which an S crate must never link (the dependency budget)”. That is true of a sync and of nothing else — adding, listing and removing a registration are a document write, a document read and a document delete. Narrowing the gap to the verb that has it is the difference between “mirroring is unimplemented” and “this build cannot fetch”. Enqueue one sync, and return without syncing.

The refusal this replaced covered all four verbs and then just this one: “mirroring runs over gunnar-client (nornir-git ls-remote + fetch), which an S crate must never link (the dependency budget)”. That was true while the only fetch client in the constellation needed gix, a .git directory and a worktree — none of which exist on an appliance running as PID 1 over znippy. gunnar_wire::fetch_pack is a fetch client with none of those requirements, over the same BufRead/Write pair upload_pack already answers on, so the gap is closed rather than narrowed again.

Registration first, so an unregistered repository is NotFound by name and not a job that fails four minutes later — two very different things to tell an operator, and only one of them is their mistake.

The upstream and prune are read HERE and carried on the order, because they are what the caller asked about; the credential is read on the worker (see CoreControlPlane::mirror_queue) because it is a secret and an order is Debug.

Source§

fn mirror_set_credential( &self, path: &RepoPath, kind: MirrorCredentialKind, secret: &str, username: &str, ) -> ControlResult<MirrorConfig>

Seal the credential first, then record its kind.

That order is the correctness argument. A document that claims a credential the secret store does not hold makes every later sync fail with “no credential stored” against an operator who can see one listed; a sealed value with no document pointing at it is unreachable and harmless. So the value is written before the fact, and forgotten after it.

The value never comes back out of this function. mirror_out has no field for it, MirrorConfig has no field for it, and the two error paths below quote the repository and the kind and nothing else.

Source§

fn mirror_remove(&self, path: &RepoPath) -> ControlResult<()>

Un-register an upstream, and forget its credential with it.

The registration goes first and the sealed value second, and the order is the correctness argument — the mirror image of Self::mirror_set_credential’s. That function writes the value before the document so a document can never claim a credential the store does not hold; this one removes the document before the value for the same reason read backwards. A sealed secret with no document pointing at it is unreachable and harmless; a document with no secret makes every later sync fail with “no credential stored” against an operator who can see one listed. So the failure mode of a crash between the two lines is the harmless one, and the NotFound path deletes nothing.

Before 2026-09-11 the value was simply left behind: Mirror.Remove took the document away and the ssh private key or forge token stayed in the sealed store for the life of the box, with nothing on the control plane naming it or able to name it — un-listable, un-rotatable, and still decryptable by anything that reaches the KEK. An operator who removes a mirror because its credential leaked has been told the credential is gone.

Source§

fn restore_keys(&self) -> ControlResult<Vec<String>>

The keys handed in by serve (--restore-pubkey), then the admin-grade keys the installer’s door fed this box ([crate::fed_principals::restore_keys_at]). Read on every call: a feed that arrives while the box runs counts from the next stream on.

Source§

fn restore_intake<'a>( &'a self, owner: &str, ) -> ControlResult<Option<Box<dyn RestoreIntake + 'a>>>

The server’s intake (restore-large-entry, 2026-10-07): an entry larger than one piece is staged on this box’s disk as it arrives and survives the stream ending inside it (crate::twin::intake).

Source§

fn snapshot_stream_from( &self, from_item: u64, from_offset: u64, expect_id: &str, sink: &mut dyn FnMut(LogChunk) -> ControlResult<()>, ) -> ControlResult<u64>

A snapshot from item from_item, byte from_offset on (restore-after-snapshot, 2026-10-07): a twin’s kept base, every piece with its item, offset, the item’s crc on its last and the base’s id (crate::twin::snapshot::serve_from); a primary’s own snapshot, a new cut each time, with none. A base that changed is [snapshot-changed].

Source§

fn log_fetch_stream( &self, from: u64, to: u64, max_bytes: u64, ) -> ControlResult<LogFetchProducer>

Mirror.LogFetch off the segments, a piece at a time (twin-streaming, 2026-10-06): the refusals now, as the call’s status; then crate::twin::stream_range — the payload crc of every entry checked before its last piece leaves — through [ChunkOut] onto the wire. What this box holds of an entry is one piece, whatever its size.

Source§

fn log_fetch_stream_at( &self, from: u64, from_offset: u64, to: u64, max_bytes: u64, ) -> ControlResult<LogFetchProducer>

Self::log_fetch_stream with the first entry from byte from_offset on (restore-large-entry, 2026-10-07): crate::twin::stream_range_at.

Source§

fn enterprise(&self) -> Option<&dyn EnterprisePlane>

The owner’s enterprise list (2026-10-05): the account audit log, the operator’s aggregates and look, the name filter (crate::enterprise::EnterprisePlane), when this plane has the stores behind it. None: nothing is recorded, nothing is shielded, every name is clean, and every Enterprise verb answers UNIMPLEMENTED.
Source§

fn create_namespace( &self, name: &str, _description: &str, ) -> ControlResult<Namespace>

Source§

fn list_namespaces(&self) -> ControlResult<Vec<Namespace>>

Source§

fn namespace_for_outsider( &self, principal_id: &str, name: &str, ) -> ControlResult<Option<Namespace>>

name as an outsider may be shown it — the namespace whose public / events_public its account admin opened, read for a caller who is NOT inside it. None when it does not exist, and also while the account or the caller is gated: the gate outranks the admin’s setting exactly as it outranks Visibility::Public in AuthTable::may, so a public account that is being emptied is not listed and its events are not shown, and a locked-out principal is shown no public account at all. Read more
Source§

fn set_namespace_settings( &self, name: &str, public: Option<bool>, events_public: Option<bool>, ) -> ControlResult<Namespace>

The account admin’s two settings (owner, 2026-10-03: namespaces and emitted events are not public by default, “such things has to be set”): is the namespace public, are its events public. None leaves a setting as it is. Who may (the account admin) is the service’s question.
Source§

fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32>

Returns how many repos went with it. Refuses a non-empty namespace unless delete_repos.
Source§

fn create_repo(&self, spec: ProtoNewRepo) -> ControlResult<Repo>

Source§

fn write_first_commit( &self, path: &RepoPath, readme: &str, ) -> ControlResult<String>

A new repository’s first commit, written by the server (CreateRepoRequest.readme): README.md holding readme on the default branch. Answers the commit id (hex). A control plane that keeps no git store refuses by name.
Source§

fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo>

Source§

fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>>

Source§

fn purge_repo(&self, path: &RepoPath) -> ControlResult<u64>

Reclaim a soft-deleted repository’s bytes now. Returns the bytes.
Source§

fn gc_status(&self, path: &RepoPath) -> ControlResult<Option<GcJob>>

How a repository’s gc is going (Repos.GcStatus). Read more
Source§

fn gate_account( &self, account: &str, actor: &str, note: &str, ) -> ControlResult<(AccountGate, bool)>

Lock account out, completely (Accounts.Gate). Read more
Source§

fn ungate_account(&self, account: &str) -> ControlResult<Option<AccountGate>>

Let account back in (Accounts.Ungate). Returns what was lifted, or None if it was not gated. Read more
Source§

fn account_gates(&self) -> ControlResult<Vec<AccountGate>>

Every account locked out right now (Accounts.ListAccountGates). Read more
Source§

fn purge_account_status( &self, account: &str, ) -> ControlResult<(Option<PurgeJob>, Option<AccountGate>)>

How an account’s purge is going (Accounts.PurgeStatus), and the lockout as it stands. Read more
Source§

fn default_delete_retention_days(&self) -> u32

How many days a soft delete that names no window keeps the bytes (ListDeletedResponse.default_retention_days, 2026-10-28). 0 = this plane does not say; a client then speaks of “the retention window” without a number.
Source§

fn list_deleted( &self, namespace: Option<&str>, ) -> ControlResult<Vec<DeletedRepoRow>>

What is deleted and not yet purged. Empty is a legitimate answer here — unlike a ref list, “nothing is deleted” is a fact a caller can act on — but the default is still Unimplemented, because a control plane that cannot see tombstones must not report that there are none.
Source§

fn index_progress(&self) -> ControlResult<Vec<IndexProgressRow>>

Index work in flight. Default: none — an engine without a background indexer has nothing to report, and that is not an error.
Source§

fn set_default_branch( &self, path: &RepoPath, default_branch: &str, ) -> ControlResult<Repo>

Source§

fn set_visibility( &self, path: &RepoPath, visibility: Visibility, ) -> ControlResult<Repo>

Source§

fn encryption_key( &self, principal_id: &str, ) -> ControlResult<GetEncryptionKeyResponse>

A person’s encryption key and its unlock methods (Access.GetEncryptionKey, CRYPT-YUBI). exists: false when they have none. The caller is the person.
Source§

fn create_encryption_key( &self, principal_id: &str, public_key: &str, first: UnlockMethod, ) -> ControlResult<GetEncryptionKeyResponse>

The first time (Access.CreateEncryptionKey); AlreadyExists when one is held.
Source§

fn add_unlock_method( &self, principal_id: &str, method: UnlockMethod, ) -> ControlResult<GetEncryptionKeyResponse>

One more unlock method (Access.AddUnlockMethod).
Source§

fn remove_unlock_method( &self, principal_id: &str, id: &str, ) -> ControlResult<GetEncryptionKeyResponse>

Delete one method’s wrap (Access.RemoveUnlockMethod); the last is FailedPrecondition.
Source§

fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo>

Turn a repository’s “encrypted” flag on or off (Repos.SetEncrypted). The caller has already checked that the account admin asks.
Source§

fn list_refs( &self, path: &RepoPath, prefix: &str, peel_them: bool, ) -> ControlResult<Vec<RefTarget>>

Source§

fn get_ref( &self, path: &RepoPath, ref_name: &str, peel_them: bool, ) -> ControlResult<RefTarget>

Source§

fn log( &self, path: &RepoPath, start: &str, limit: u32, touching: &str, ) -> ControlResult<Vec<Commit>>

Source§

fn create_pull( &self, path: &RepoPath, author: &str, title: &str, description: &str, source_ref: &str, target_ref: &str, ) -> ControlResult<PullRequest>

Open a pull request. author is the authenticated principal.
Source§

fn list_pulls( &self, path: &RepoPath, state: i32, ) -> ControlResult<Vec<PullRequest>>

Every pull in one repository; state zero means all.
Source§

fn get_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>

Source§

fn close_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>

Source§

fn read_object( &self, path: &RepoPath, oid: &str, max_bytes: u64, ) -> ControlResult<ReadObjectResponse>

Source§

fn add_principal( &self, id: &str, name: &str, kind: PrincipalKind, ) -> ControlResult<Principal>

Create a principal with id as its identity (and account) and name as its display name. The service resolves an empty wire id to the name before calling, so this never sees an empty id.
Source§

fn enroll_employee( &self, account: &str, ask: &EmployeeAsk, ) -> ControlResult<EmployeeAnswer>

Enroll an employee: a NEW user created by and bound to account (Accounts.EnrollEmployee). AlreadyExists for an id or name that is taken — an existing principal never becomes an employee — and for an id that names an existing namespace (its owner-by-name would be the employee). NotFound when account is not a namespace. The employee gets no namespace of its own and Read on the account’s. Read more
Source§

fn seats(&self, account: &str) -> ControlResult<(u32, u32)>

The account’s seats: every seat granted, and the open ones.
Source§

fn clans(&self) -> Option<&dyn ClanPlane>

Clans (2026-10-05, crate::clans::ClanPlane), when this plane keeps an identity table that holds them. None: every Clans verb is UNIMPLEMENTED and no namespace is a clan’s.
Source§

fn has_employees(&self, account: &str) -> ControlResult<bool>

Has the account enrolled anyone? What Access.WhoAmI asks on every call; a plane that can answer without listing (CoreControlPlane) says so.
Source§

fn employees(&self, account: &str) -> ControlResult<Vec<Principal>>

The account’s employees, by id.
Source§

fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>>

One principal by its display name, or None.
Source§

fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim>

The estate’s operator, created on first claim (Accounts.ClaimOperator, 2026-10-04). asked is the caller’s name for it, used only when nothing was fed (operator_name). Idempotent; an ownerless namespace of that name is adopted; a machine under the name is FAILED_PRECONDITION. The principal is created keyless and gunnar-admin. The default is the honest answer of a plane that keeps no operator.
Source§

fn rename_principal( &self, principal_id: &str, name: &str, ) -> ControlResult<Principal>

Change the display name and nothing else. AlreadyExists when another principal carries the name (as name or id); NotFound for an unknown id.
Source§

fn list_principals(&self) -> ControlResult<Vec<Principal>>

Source§

fn principal_activity(&self) -> ControlResult<BTreeMap<String, PrincipalSeen>>

When each principal last signed in and last changed something (2026-10-21), by principal id; a principal with no entry was never seen. The default knows nothing — a plane that keeps no stamps answers “never” for everybody, never an error.
Source§

fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool>

Does principal_id hold a SERVER-scope grant (the seller’s mark, Namespace.owner_server_grant)? The default: no grant is known here.
Source§

fn add_credential( &self, principal_id: &str, credential_id: &[u8], public_key: &[u8], label: &str, sign_count: u32, ) -> ControlResult<WebCredential>

Register a web credential on a principal (Access.AddCredential, Stage L). NotFound for an unknown principal; FailedPrecondition for one that is not a USER (no human credential on a machine principal); AlreadyExists for a credential id already registered. The public key is validated by the service before it gets here. Removed with the principal. Defaults to Unimplemented: a plane that keeps no identity table keeps no passkeys either.
Source§

fn credentials( &self, principal_id: Option<&str>, ) -> ControlResult<Vec<WebCredential>>

One principal’s web credentials, or every one (None).
Source§

fn credential( &self, credential_id: &[u8], ) -> ControlResult<Option<StoredCredential>>

One web credential with its public key, or None.
Source§

fn record_passkey_use( &self, credential_id: &[u8], sign_count: u32, now_unix_ms: i64, ) -> ControlResult<WebCredential>

A passkey was used (the identity core’s login and step-up, IDENTITY-HOST 2026-10-27): its counter and last use, in one update. The core checked the counter rule; one that went back is still refused here (PermissionDenied). NotFound for a credential that is gone.
Source§

fn revoke_credential( &self, credential_id: &[u8], ) -> ControlResult<Option<WebCredential>>

Remove a web credential; the row it was, or None if there was none.
Source§

fn rename_credential( &self, credential_id: &[u8], label: &str, ) -> ControlResult<Option<WebCredential>>

Change a web credential’s label; the updated row, or None if there is no such credential.
Source§

fn first_invite(&self) -> ControlResult<Option<FirstInvite>>

The operator’s first invite, if one is pending (IDENTITY-HOST): the record ControlPlane::operator_bootstrap_invite made, which the identity core reads as a fed invite of its own. The default: none.
Source§

fn drop_first_invite(&self) -> ControlResult<()>

The operator’s first invite is spent or withdrawn (the identity core redeemed it, or its person holds a passkey): the record goes.
Source§

fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>>

The account that enrolled principal_id as an employee, if any.
Source§

fn sso(&self) -> Option<&dyn SsoPlane>

Company SSO’s storage (crate::sso::plane::SsoPlane), when this plane has an identity table that holds it. None: every Sso verb answers UNIMPLEMENTED, and Access.WebLogin knows of no require_sso.
Source§

fn sso_import(&self, order: ImportOrder) -> ControlResult<ImportJob>

Start a directory import as a background job (Sso.ImportUsers, A34-17): one per account at a time on the server’s jera lane, the whole file in one write. Returns the job as it stands; an account with an import still queued or running is refused by name.
Source§

fn sso_import_status(&self, account: &str) -> ControlResult<Option<ImportJob>>

The account’s latest import job (Sso.ImportStatus), if the server still holds it.
Source§

fn approve_machine_enrolment( &self, approval: MachineApproval, ) -> ControlResult<MachineApproved>

Commit an approved machine enrolment (2026-10-05): see MachineApproval. The ONE way a web credential lands on a MACHINE principal — add_credential and the identity core still refuse one. A step-up counter that did not increase is PermissionDenied. The operator’s first invite is never touched: it is the owner’s.
Source§

fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)>

Returns (keys revoked, grants revoked).
Source§

fn add_key( &self, principal_id: &str, openssh_public_key: &str, _comment: &str, expires_at_unix_ms: Option<i64>, caller: &str, attestation: Option<SignedKeyAttestation>, ) -> ControlResult<PublicKey>

Register a key on principal_id. Read more
Source§

fn revoke_key(&self, fingerprint: &str) -> ControlResult<()>

Source§

fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>>

Source§

fn grant( &self, principal_id: &str, namespace: &str, repo: &str, level: GrantLevel, ) -> ControlResult<Grant>

Source§

fn revoke( &self, principal_id: &str, namespace: &str, repo: &str, ) -> ControlResult<()>

Source§

fn list_grants( &self, principal_id: Option<&str>, namespace: Option<&str>, repo: Option<&str>, ) -> ControlResult<Vec<Grant>>

Source§

fn add_group_member( &self, group_id: &str, member_id: &str, signed: SignedMembershipEvent, ) -> ControlResult<u64>

Verify the acting admin’s signed event, make member_id a member of group_id, and append the event to the membership log.
Source§

fn remove_group_member( &self, group_id: &str, member_id: &str, signed: SignedMembershipEvent, ) -> ControlResult<(bool, u64)>

Verify the signed event, remove member_id from group_id, and append it. Returns whether it was a member and the log index.
Source§

fn membership_events( &self, group_id: Option<&str>, ) -> ControlResult<Vec<SignedMembershipEvent>>

The signed membership log, optionally filtered to one group, in append order — the evidence behind the live table.
Source§

fn list_group_members(&self, group_id: &str) -> ControlResult<Vec<Principal>>

The direct members of a group.
Source§

fn get_protection( &self, path: &RepoPath, ref_pattern: Option<&str>, ) -> ControlResult<Vec<BranchProtection>>

Source§

fn delete_protection( &self, path: &RepoPath, ref_pattern: &str, ) -> ControlResult<()>

Source§

fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy>

Source§

fn set_ref_name_policy( &self, path: &RepoPath, policy: RefNamePolicy, ) -> ControlResult<RefNamePolicy>

Source§

fn forge_append(&self, event: SignedForgeEvent) -> ControlResult<u64>

Verify a signed review event against the registered keys and append it. Read more
Source§

fn forge_append_attested( &self, event: SignedForgeEvent, session_stepped_up_at_unix_ms: i64, ) -> ControlResult<(u64, String)>

Append an event gunnar attests (GHUNT-19): event arrives UNSIGNED from a person’s identity-core session that stepped up — which holds no key of theirs — and the plane signs it with gunnar’s own forge-attestation key, the actor named and session_stepped_up_at_unix_ms inside the signature. The caller (the service) has already checked that the session stepped up and that the actor is its person; the subject is checked here exactly as for a signed event. Returns the index it landed at and the attestation in words (attested for session step-up at <time>), for the audit row.
Source§

fn forge_review( &self, repo: &str, pull: u64, ) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)>

One pull request’s review, folded, with every approval re-checked now.
Source§

fn forge_reviews(&self, repo: &str) -> ControlResult<Vec<ReviewState>>

The review on every pull request in repo that has one.
Source§

fn forge_issue( &self, repo: &str, number: u64, ) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>>

One issue, folded from its events, or None if nothing opened that number. The whole thread is returned alongside — the opening, every comment, and the close if there is one.
Source§

fn forge_issues(&self, repo: &str) -> ControlResult<Vec<IssueState>>

Every issue in repo, folded, in the order they were opened.
Source§

fn counts(&self) -> ControlResult<ControlCounts>

Source§

fn verify_chain( &self, request: &VerifyChainRequest, ) -> ControlResult<VerifyChainResponse>

Walk the provenance chain (Admin.VerifyChain, roadmap phase 2). Read more
Source§

fn blame( &self, path: &RepoPath, rev: &str, file: &str, ) -> ControlResult<BlameResponse>

Blame a file at a revision (Refs.Blame). Read more
Source§

fn search( &self, path: &RepoPath, rev: &str, pattern: &str, max_file_bytes: u64, max_matches: u32, ) -> ControlResult<SearchResponse>

Search one revision’s files for a literal string (Refs.Search). Read more
Source§

fn setting(&self, name: &str) -> ControlResult<Option<Vec<u8>>>

A generic server setting (Admin.GetSetting, owner decision 2026-10-26): the bytes stored under name, None where nothing is. The server knows no setting name: the client names them (gunnar-core settings).
Source§

fn set_setting( &self, name: &str, value: &[u8], ) -> ControlResult<Option<Vec<u8>>>

Store a generic server setting (Admin.SetSetting): value under name; the value it replaced, None for a new name. Bounded (gunnar-core settings), and a refusal names the bound.
Source§

fn storage_info( &self, path: Option<&RepoPath>, ) -> ControlResult<StorageInfoResponse>

Source§

fn grow_start( &self, request: &GrowStartRequest, ) -> ControlResult<GrowStatusResponse>

Begin a runtime growth: drain, unmount, wait, grow, mount, reopen.
Source§

fn grow_status(&self) -> ControlResult<GrowStatusResponse>

The growth’s phase, members and journal. Answered with and without a credential (Admin.GrowStatus, Bootstrap.GrowStatus).
Source§

fn grow_resume(&self, give_up: bool) -> ControlResult<GrowStatusResponse>

The outside actor says “look again” (or the operator gives up).
Source§

fn mirror_status( &self, namespace: Option<&str>, path: Option<&RepoPath>, ) -> ControlResult<Vec<MirrorConfig>>

Source§

fn is_group_member( &self, principal_id: &str, group: &str, ) -> ControlResult<bool>

Is principal_id a member of group? The twin’s log gate asks it (Mirror.LogHead / LogFetch for a member of group twin). Default false: a plane with no groups has no twin members.
Source§

fn become_twin(&self, primary: &str) -> ControlResult<String>

Bootstrap.BecomeTwin: a fresh box becomes a twin of primary; answers the twin’s public key line.
Source§

fn twin_csr(&self) -> ControlResult<String>

Bootstrap.TwinCsr: this twin’s replication certificate request, minting its TLS key on the box on first call (lane T9).
Source§

fn install_twin_certificate( &self, cert_pem: &str, ca_pem: &str, replication_port: u16, replication_host: &str, ) -> ControlResult<i64>

Bootstrap.InstallTwinCertificate: take the primary-signed certificate and pin its issuer; answers whole days left.
Source§

fn issue_twin_certificate( &self, principal: &str, csr_pem: &str, ) -> ControlResult<(String, String, u16, String)>

Admin.IssueTwinCertificate, on the primary: (cert, issuer, port, host) — host is the address the listener is bound on when that is not the joined one (its tunnel address), empty otherwise.
Source§

fn twin_git_listener_needs_restart(&self) -> bool

Is this box’s git listener still bound where a primary’s was? Read more
Source§

fn restore_begin(&self, owner: &str) -> ControlResult<u64>

Bootstrap.Restore, start: the next sequence this box takes. owner is who the stream’s credential proved the caller to be (crate::auth::authenticate_restore): a box already restoring resumes only for the owner that started it, and refuses anybody else PERMISSION_DENIED (h-v1-6a, 2026-09-28).
Source§

fn restore_room(&self, announced_bytes: u64) -> ControlResult<()>

Bootstrap.Restore, the room check: has this box’s data set space for a stream of announced_bytes of log? Asked ONCE, on the first chunk, before anything of the restore is written — the restore-onto-different-hardware case. 0 = the sender did not say, and no guess is made. A default Ok(()): a plane with no data set of its own cannot run out.
Source§

fn restore_apply(&self, entry: LogEntry) -> ControlResult<bool>

Bootstrap.Restore, one whole entry: true when a replayed CAS lost.
Source§

fn restore_finish( &self, announced_head: u64, log_id_of_source: &str, ) -> ControlResult<u64>

Bootstrap.Restore, end: the head reached. announced_head is the largest LogChunk.restore_head the stream carried (0 = none): a box whose head is below it refuses to finish and stays restoring.
Source§

fn snapshot_stream( &self, sink: &mut dyn FnMut(LogChunk) -> ControlResult<()>, ) -> ControlResult<u64>

Mirror.SnapshotFetch: a consistent snapshot streamed into sink as LogChunk pieces — items with seq 0, at most 1 MiB of blob a piece, blob_len the item’s total on its last piece and u64::MAX before it, the SnapshotAt terminator last. Never gathered: the sink’s backpressure paces the producer. Answers the sequence it was taken at.
Source§

fn log_retain(&self, from: u64) -> ControlResult<LogHeadResponse>

Mirror.LogRetain: drop every whole segment below from (0 = head + 1); answers the head as LogHead does. Refused on a twin.
Source§

fn log_layout(&self) -> ControlResult<Vec<(u64, u64, u64)>>

★ The log’s segments, for a byte-bounded retention (cov-13, 2026-10-06): (first, last, bytes), oldest first, the open one last (last = first - 1 while it holds nothing). Only whole segments are dropped, so this is what a byte budget is decided over. Default Unimplemented: a plane with no log.
Source§

fn data_volume(&self) -> Option<(u64, u64)>

★ The data volume the log lives on: (total, used) bytes, statvfs — what the retention’s byte budget is a share of, and what a twin silent through main’s growth is owed (cov-13). None when unknown, and while the set is released for a growth (statvfs would read the box’s root, not the set).
Source§

fn twin_owed_read(&self) -> ControlResult<Vec<u8>>

★ twin-owed.json (cov-13): what each twin is owed, as last written; empty = never. Default Unimplemented: the book then lives in memory only.
Source§

fn twin_owed_write(&self, bytes: &[u8]) -> ControlResult<()>

★ Write twin-owed.json (replace, crash-safe).
Source§

fn restore_snapshot_chunk(&self, chunk: &LogChunk) -> ControlResult<Option<u64>>

Bootstrap.Restore, one snapshot piece (seq 0), staged on disk: Some(at) once the terminator installed the snapshot, so the restore continues from at + 1.
Source§

fn health_alarms(&self) -> Vec<HealthAlarm>

States this box is in that an operator must act on and no console reaches (Admin.Health.alarms). Default: none.
Source§

fn is_twin(&self) -> bool

Is this box a twin right now? Default false. Read by the browse refusal: a twin is frozen like a browse replica, yet it must take its OWN data-set growth exactly as main does (owner ruling 2026-09-22, T21), so the grow verbs pass on a twin and on no browse box.
Source§

fn frozen(&self) -> Option<String>

Why this box takes no control-plane write right now — a twin, a browse replica — or None. Read by the paths outside the gate that write anyway (the identity core, RA5-10), so they refuse as the gate’s browse refusal does. Default: None (a plane with no replication).
Source§

fn log_follows(&self) -> bool

Does this box’s transaction log FOLLOW another’s (a twin, or a main being restored)? It originates no transaction until it opens as the primary. Default false (wiring tracer B 2026-10-08: Entitlement.Set answers such a box’s “try again” before it asks whether a namespace exists — a namespace made after the twin’s snapshot is not replayed yet).
Source§

fn topology_facts(&self) -> Facts

What Admin.Topology needs from the plane (2026-10-04): the pairing feed’s peers, which principals run on the front, the plugin registration, this box’s own address and data volume. Default: a box that knows only itself — which is the truthful answer for a plane with no pairing (a solo gunnar, a test).
Source§

fn maintenance_reasons(&self) -> Vec<String>

Why this box reports maintenance (Admin.Health.status), one reason each; empty is serving. It still serves either way. Default: none.
Source§

fn primary_heartbeat(&self, silent_after_ms: u64) -> Option<PrimaryHeartbeat>

On a twin: its primary’s heartbeat (when the primary last answered), with the silence bound silent_after_ms. Default None: not a twin.
Source§

fn log_head(&self) -> ControlResult<LogHeadResponse>

The log’s head — head_seq, first_seq, bytes, and on a twin what it has applied. Default Unimplemented for a plane with no log.
Source§

fn log_head_signed(&self, nonce: &str) -> ControlResult<LogHeadResponse>

ControlPlane::log_head, signed over the caller’s nonce — what a twin’s poll actually calls (2026-09-21, lane T4). The default ignores the nonce and answers unsigned, which is the honest answer for a plane with no key of its own; a twin that has pinned a key refuses it.
Source§

fn fence_by_generation(&self, proof: &GenerationProof) -> ControlResult<()>

A twin’s proof that a NEWER generation of this box’s log exists (LogHeadRequest.generation_proof, stinky row 18, the main’s half). Called before the answer is made. A box that finds the proof genuine — its own log, its own host key, a generation above its own — fences itself and originates nothing from then on; anything else is ignored. The default keeps no log and has nothing to fence.
Source§

fn log_fetch( &self, from: u64, to: u64, max_bytes: u64, ) -> ControlResult<Vec<LogEntry>>

Entries from..=to, whole, at most max_bytes of blob in total but never fewer than one entry. from below the oldest retained sequence is FailedPrecondition naming it.
Source§

fn principal(&self, id: &str) -> Result<Option<Principal>, ControlError>

One principal by its ID, or None (Access.WhoAmI). The default walks ControlPlane::list_principals; a plane with an id index may answer it directly.
Source§

fn set_server_level( &self, principal_id: &str, level: GrantLevel, ) -> Result<(), ControlError>

Set a principal’s server-wide level (the identity core’s first admin, IDENTITY-HOST): the scope-less grant.
Source§

fn replication_set( &self, _target: ReplicationTarget, ) -> Result<ReplicationStatusResponse, ControlError>

Add or replace one standby by name, persist it, and start replicating.
Source§

fn replication_remove( &self, _name: &str, ) -> Result<ReplicationStatusResponse, ControlError>

Drop one standby by name, persist that, and stop replicating to it.
Source§

fn replication_demote(&self) -> Result<ReplicationStatusResponse, ControlError>

Drop EVERY standby at once — the failover demote. Persist the empty roster and stop replicating anywhere.
Source§

fn replication_set_role( &self, _role: ReplicationRole, ) -> Result<ReplicationStatusResponse, ControlError>

Set this box’s role: STANDBY freezes client writes (a read-only standby); PRIMARY is a writable primary — the takeover flip; BROWSE is a frozen standby that serves anonymous reads and can never be promoted (the browse replica) — an implementation refuses PRIMARY on a browse box with FailedPrecondition. Persist it and flip the shared write-gate the front doors read.
Source§

fn replication_abdicate( &self, ) -> Result<ReplicationStatusResponse, ControlError>

Abdicate: freeze client writes, flush pending to the standbys, then demote — the master steps down cleanly, in that order.
Source§

fn replication_status( &self, _include_refs: bool, ) -> Result<ReplicationStatusResponse, ControlError>

What is configured, and what each running standby has done. Read more
Source§

fn replication_resync( &self, _name: &str, _repo: Option<&RepoPath>, _except: &[RepoPath], ) -> Result<ResyncReplicationResponse, ControlError>

Backfill: queue every repository one standby covers, or one named repository, so a standby added AFTER the repositories were written gets them. Adding a standby enqueues nothing.
Source§

fn replication_flush( &self, _request: &FlushReplicationRequest, ) -> Result<FlushReplicationResponse, ControlError>

The replication watermark (Mirror.FlushReplication, DATA-SET-GROWTH-FLOW.md G2/T11): push everything pending to every standby, wait — bounded — and report the tuple that proves (or does not) that the twin is caught up. Changes no role and no roster.
Source§

fn assign_tunnel_address( &self, _address: &str, ) -> Result<AssignTunnelAddressResponse, ControlError>

Bootstrap.AssignTunnelAddress (2026-09-21, T17): address empty = answer only; otherwise record it as this box’s WireGuard tunnel address and raise wg0. Default: a plane with no tunnel has no address to take.
Source§

fn set_tunnel_address( &self, _address: &str, ) -> Result<AssignTunnelAddressResponse, ControlError>

Admin.SetTunnelAddress (2026-09-21, T17): the SAME answer as ControlPlane::assign_tunnel_address for a box past its bootstrap window. The caller has already been held to server-wide Admin by the service; the plane does not re-check the window.
Source§

fn webhook_add( &self, _request: AddWebhookRequest, ) -> Result<WebhookListResponse, ControlError>

Add or replace one endpoint by name, persist it, and start delivering.
Source§

fn webhook_list(&self) -> Result<WebhookListResponse, ControlError>

What is configured, plus the signing public key a receiver verifies with.
Source§

fn webhook_remove( &self, _name: &str, ) -> Result<WebhookListResponse, ControlError>

Drop one endpoint by name, persist that, and stop delivering to it.
Source§

impl EnterprisePlane for CoreControlPlane

Source§

fn account_of_namespace(&self, namespace: &str) -> ControlResult<Option<String>>

The account namespace belongs to: a person’s, read off its first component. None for a namespace no person owns.
Source§

fn account_of_principal( &self, principal_id: &str, ) -> ControlResult<Option<String>>

The account principal_id belongs to: its employer, or its own.
Source§

fn is_account_admin( &self, principal_id: &str, account: &str, ) -> ControlResult<bool>

Is principal_id an admin of account? Its own person, or one its admin made Admin over the namespace — never a server-scope grant.
Source§

fn shielded(&self, principal_id: &str, namespace: &str) -> ControlResult<bool>

Is namespace closed to principal_id’s server-scope grants? A customer’s account the principal is not inside and holds no live look into. What a server-wide listing asks per row.
Source§

fn account_audit_record( &self, account: &str, event: &AccountAuditEvent, ) -> ControlResult<()>

Append one event to account’s audit log; the plane says what the actor is to the account.
Source§

fn account_audit_log( &self, account: &str, query: &AccountAuditQuery, ) -> ControlResult<AuditLogResponse>

One page of account’s log, its chain’s state with it.
Source§

fn account_audit_export( &self, account: &str, query: &AccountAuditQuery, format: AuditExportFormat, ) -> ControlResult<ExportAuditLogResponse>

Every entry the query matches, as a file.
Source§

fn aggregates(&self) -> ControlResult<AggregatesResponse>

The operator’s totals: no name of anything inside an account.
Source§

fn operator_access_request( &self, account: &str, operator: &str, reason: &str, minutes: u32, ) -> ControlResult<OperatorAccess>

The operator asks to look inside account.
Source§

fn operator_access_list( &self, account: Option<&str>, ) -> ControlResult<Vec<OperatorAccess>>

The looks into account (None: every account), newest first.
Source§

fn operator_access_get( &self, request_id: &str, ) -> ControlResult<Option<OperatorAccess>>

One look, by its request id.
Source§

fn operator_access_decide( &self, decision: &OperatorAccessDecision, ) -> ControlResult<OperatorAccess>

The account admin’s decision, committed with the step-up’s counter.
Source§

fn operator_access_revoke( &self, request_id: &str, by: &str, ) -> ControlResult<OperatorAccess>

End a look now.
Source§

fn name_check( &self, kind: &str, name: &str, account: &str, ) -> ControlResult<NameVerdict>

What the name filter says about name as a kind (account, member, namespace, repo) in account (empty: none).
Source§

fn name_review_add( &self, kind: &str, name: &str, account: &str, actor: &str, hold: bool, ) -> ControlResult<NameReview>

Hold a borderline name for account’s admin (hold), or note a refused one for them (!hold).
Source§

fn name_lists(&self) -> ControlResult<NameLists>

The lists in force.
Source§

fn set_name_lists(&self, lists: NameLists) -> ControlResult<NameLists>

Replace the lists.
Source§

fn name_reviews(&self, account: &str) -> ControlResult<Vec<NameReview>>

account’s queue, newest first.
Source§

fn name_review_decide( &self, account: &str, id: &str, allow: bool, by: &str, ) -> ControlResult<NameReview>

The account admin decides a held name.
Source§

impl FirstBoot for CoreControlPlane

First boot, delegated straight to gunnar_core::AuthTable.

Nothing is decided here. bootstrap_state reads the one predicate the writer also reads, and enroll_first_identity is a call into the one compare-and-swap that can produce a BootstrapRecord. A second opinion in this adapter — “the table looks empty, so the window is open” — would be a status that could disagree with the gate, and the permissive one would win silently (one implementation).

Source§

fn bootstrap_state(&self) -> ControlResult<BootstrapState>

The enrolment state, as the server holds it. Read more
Source§

fn enroll_first_identity( &self, name: &str, openssh_public_key: &str, ) -> ControlResult<Enrolled>

Create the first identity and shut the window, atomically. Read more
Source§

impl Identity for CoreControlPlane

The one place gRPC identity is decided, and it decides nothing itself.

gunnar-grpc closed the allow-all authentication hole with SSH-key-derived identity: a caller signs a single-use challenge with a key that is already in the ACL key table, and the gate asks the control plane two questions: whose key is this, and may they. Both are delegated here, and both land on [AuthTable]:

questionanswered by
whose key is this[AuthTable::principal_by_fingerprint] (expiry included)
may they[AuthTable::may_server] / [AuthTable::may_namespace] / [AuthTable::may]

So gRPC and SSH share one identity table and one ACL. Revoking a key over gRPC revokes it for git in the same instant, and there is no second grant decision that could drift from the first — one implementation, because two disagreeing security checks have cost this design once already.

Source§

fn awaiting_enrolment(&self, fingerprint: &str) -> Option<String>

The installer’s feed, as crate::fed_principals::note_fed left it: the keys this primary will enrol and has not yet.

Source§

fn registered_certificate( &self, der: &[u8], ) -> ControlResult<Option<CertificateIdentity>>

The X.509 half of the same table registered_key reads.

gunnar control bootstrap has minted client certificates and registered them under algo: "x509" since the certificate-identity plan’s first-boot half landed; until the listener grew mTLS there was nothing that could look one up, so those registrations were inert. This is the lookup, and it is deliberately the same three steps registered_key takes — hash, principal, key row — so that revoking a certificate over gRPC revokes it for the handshake in the same instant, out of one table (one implementation).

No openssh line comes back and none is wanted: the handshake already proved possession, so there is no second signature to check. See gunnar_grpc::CertificateIdentity.

Source§

fn narrowed_methods( &self, principal_id: &str, ) -> ControlResult<Option<Vec<String>>>

f-principal-reach: what fed_principals::install narrowed a billing principal to, from the data root.

Source§

fn may_each_repo( &self, principal_id: &str, paths: &[RepoPath], level: GrantLevel, ) -> ControlResult<Vec<bool>>

One read of the catalog and one of the identity table for the whole listing (round 5.3.1); a row that does not exist is false, as may.

Source§

fn registered_key( &self, fingerprint: &str, ) -> ControlResult<Option<RegisteredKey>>

The registered key for a SHA256:… fingerprint, or None if there is no such key, the key has expired, or its principal is gone. Read more
Source§

fn may( &self, principal_id: &str, access: &Access, level: GrantLevel, ) -> ControlResult<bool>

May this principal do this, at this level? gunnar_core::AuthTable::may (the ACL model) seen through the seam.
Source§

impl SsoPlane for CoreControlPlane

Source§

fn provider(&self, account: &str) -> ControlResult<Option<SsoProvider>>

An account’s provider, has_client_secret filled, no secret.
Source§

fn any_provider(&self) -> ControlResult<bool>

Whether any account has a provider configured — an existence bit for the login page’s company field (DiscoverResponse.any_provider).
Source§

fn set_provider( &self, provider: SsoProvider, secret: SecretChange, actor: &str, ) -> ControlResult<SsoProvider>

Store a provider (validated by the service) and change the secret.
Source§

fn remove_provider(&self, account: &str) -> ControlResult<bool>

Source§

fn account_for_domain(&self, domain: &str) -> ControlResult<Option<String>>

Source§

fn client_secret(&self, account: &str) -> ControlResult<Option<String>>

The unsealed OIDC client secret, for the token request only.
Source§

fn login(&self, ask: SsoLoginAsk) -> ControlResult<SsoLoginOutcome>

Source§

fn directory(&self, account: &str) -> ControlResult<Vec<DirUser>>

The account’s directory, deleted rows included.
Source§

fn provision(&self, ask: ProvisionAsk) -> ControlResult<DirUser>

Source§

fn import( &self, account: &str, rows: &[(String, ImportRow)], actor: &str, now_unix_ms: i64, commit: bool, ) -> ControlResult<()>

A whole directory import in ONE write (A34-17): every row or none; commit false checks the same rows (room, names, ids) and writes nothing. A refusal names the row (line 7: bob).
Source§

fn update_user( &self, account: &str, principal_id: &str, patch: UserPatch, actor: &str, ) -> ControlResult<(DirUser, bool)>

Returns the row and whether active changed.
Source§

fn delete_user( &self, account: &str, principal_id: &str, actor: &str, ) -> ControlResult<DirUser>

Source§

fn create_scim_token( &self, account: &str, id: &str, label: &str, secret_hash: &str, created_by: &str, expires_at_unix_ms: i64, read_only: bool, ) -> ControlResult<ScimToken>

A new SCIM bearer by its hash; expires_at_unix_ms 0 never expires, read_only may only read (A34-27).
Source§

fn scim_tokens(&self, account: &str) -> ControlResult<Vec<ScimToken>>

Source§

fn revoke_scim_token(&self, account: &str, id: &str) -> ControlResult<bool>

Source§

fn scim_account( &self, secret_hash: &str, ) -> ControlResult<Option<(String, Bearer)>>

The account a bearer’s hash provisions and the bearer’s own rules (expiry, scope: A34-27), and its use noted.
Source§

fn groups(&self, account: &str) -> ControlResult<Vec<GroupRow>>

Source§

fn put_group(&self, group: GroupRow) -> ControlResult<GroupRow>

Source§

fn remove_group(&self, account: &str, id: &str) -> ControlResult<bool>

Source§

fn passkey_login_refused( &self, principal_id: &str, ) -> ControlResult<Option<String>>

require_sso: the sentence a passkey-only login of this principal is refused with, or None (not an employee, or its account does not require SSO). The account admin is never an employee.
Source§

fn passkeys_of(&self, principal_id: &str) -> ControlResult<Vec<Vec<u8>>>

The principal’s passkeys (ids), for a second factor’s allowCredentials.
Source§

fn verify_domain( &self, account: &str, domain: &str, ) -> ControlResult<SsoProvider>

domain is proven for account (A34-5), after the service read its DNS proof: it now leads sign-ins there and counts for claims and JIT. Refused (PermissionDenied, by code) when it is not one of the provider’s domains or another account proved it first.
Source§

fn rebind_users( &self, account: &str, principal_ids: &[String], external_id: &str, ) -> ControlResult<Vec<DirUser>>

Open a rebind (A34-11): the named employees (every bound one when none is named) are unbound from their old IdP subject and bind afresh on their next sign-in; external_id (one employee only) is its id at the new IdP.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more