pub struct CoreControlPlane { /* private fields */ }Expand description
The control plane, backed by the real catalog and identity table.
Implementations§
Source§impl CoreControlPlane
impl CoreControlPlane
Sourcepub fn new(stores: Arc<ServerStores>) -> Self
pub fn new(stores: Arc<ServerStores>) -> Self
Serve out of stores, with the default object ceiling.
Sourcepub fn with_restore_keys(self, keys: Vec<String>) -> Self
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.
Sourcepub fn with_bus(self, bus: Arc<EventBus>) -> Self
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.
Sourcepub fn with_twin_role(self, twin_role: bool) -> Self
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.
Sourcepub fn with_git_listener(self, listener: GitListener) -> Self
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.
Sourcepub fn release_stale_purge_leases(stores: &ServerStores) -> u64
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.
Sourcepub fn resume_interrupted_mirrors(&self) -> usize
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.
Sourcepub fn with_headroom(self, gate: Arc<DiskHeadroomGate>) -> Self
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.
Sourcepub fn with_grow(self, grow: Arc<GrowMachine>) -> Self
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.
Sourcepub fn with_admission(self, admission: Arc<Admission>) -> Self
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.
Sourcepub fn replication_view(&self) -> ReplicationView
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.
Sourcepub fn with_max_object_bytes(self, bytes: u64) -> Self
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
impl CoreControlPlane
Sourcepub fn claim_operator_named(
&self,
fed: Option<&str>,
asked: &str,
) -> ControlResult<OperatorClaim>
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) conjuredrickard, andAddPrincipalrefused 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
impl ClanPlane for CoreControlPlane
Source§fn clan_dissolve(&self, clan: &str, by: &str) -> ControlResult<Clan>
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)>
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>
fn clan_create( &self, name: &str, creator: &str, contribution: u64, room: ClanRoom, ) -> ControlResult<Clan>
name (and its namespace, private), creator its admin,
giving contribution bytes. AlreadyExists for a taken name.Source§fn clans_of(&self, principal: &str) -> ControlResult<Vec<Clan>>
fn clans_of(&self, principal: &str) -> ControlResult<Vec<Clan>>
principal is in or invited to.Source§fn clan_invite(
&self,
clan: &str,
by: &str,
principal: &str,
) -> ControlResult<Clan>
fn clan_invite( &self, clan: &str, by: &str, principal: &str, ) -> ControlResult<Clan>
by) invites principal.Source§fn clan_answer(
&self,
clan: &str,
principal: &str,
accept: bool,
contribution: u64,
room: ClanRoom,
) -> ControlResult<Clan>
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>
fn clan_withdraw( &self, clan: &str, by: &str, principal: &str, ) -> ControlResult<Clan>
principal’s invitation.Source§fn clan_set_contribution(
&self,
clan: &str,
member: &str,
bytes: u64,
room: ClanRoom,
used: Option<u64>,
) -> ControlResult<Clan>
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>
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>
fn clan_remove( &self, clan: &str, by: &str, member: &str, used: Option<u64>, ) -> ControlResult<Clan>
member.Source§fn clan_set_admin(
&self,
clan: &str,
by: &str,
member: &str,
admin: bool,
) -> ControlResult<Clan>
fn clan_set_admin( &self, clan: &str, by: &str, member: &str, admin: bool, ) -> ControlResult<Clan>
member admin, or not.Source§fn clan_room_left(
&self,
account: &str,
room: ClanRoom,
) -> ControlResult<Option<u64>>
fn clan_room_left( &self, account: &str, room: ClanRoom, ) -> ControlResult<Option<u64>>
account could still give a clan (None: uncapped).Source§fn clan_given(&self, account: &str) -> ControlResult<u64>
fn clan_given(&self, account: &str) -> ControlResult<u64>
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>
fn clan_offer_repo( &self, clan: &str, by: &str, repo: &str, to: &str, ) -> ControlResult<Clan>
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>>
fn clan_repo_offer( &self, clan: &str, repo: &str, ) -> ControlResult<Option<RepoOffer>>
clan/repo, with what the repository stores.Source§fn clan_offers_to(&self, to: &str) -> ControlResult<Vec<RepoOffer>>
fn clan_offers_to(&self, to: &str) -> ControlResult<Vec<RepoOffer>>
to.Source§fn clan_withdraw_offer(
&self,
clan: &str,
by: &str,
repo: &str,
) -> ControlResult<Clan>
fn clan_withdraw_offer( &self, clan: &str, by: &str, repo: &str, ) -> ControlResult<Clan>
clan/repo.Source§fn clan_decline_offer(
&self,
clan: &str,
repo: &str,
to: &str,
) -> ControlResult<RepoOffer>
fn clan_decline_offer( &self, clan: &str, repo: &str, to: &str, ) -> ControlResult<RepoOffer>
to declines the offer of clan/repo.Source§impl ControlPlane for CoreControlPlane
impl ControlPlane for CoreControlPlane
Source§fn namespace(&self, name: &str) -> ControlResult<Option<Namespace>>
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<()>
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>
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 deletemust 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>
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>
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)>
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_purgerefuses 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>>
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>
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>>
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>
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>>
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)>
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>
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>>
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>
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>
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.
BranchProtectioncarriesmax_object_bytesandmax_objects_per_pushper rule;gunnar-policy’sLimitPolicyis 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_createandancestry_budgetsurvive a round trip. They have no proto field, so a rule written over gRPC would otherwise reset whatever agunnar-policy.jsonhad 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>
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>
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>
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>
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’spack_bytes— the metercaps["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: theavailable_parallelismthe 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>>
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>>
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>
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)>
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>
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>
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>
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>
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<()>
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>>
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>>>
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>
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>
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>
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>
fn enterprise(&self) -> Option<&dyn EnterprisePlane>
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.fn create_namespace( &self, name: &str, _description: &str, ) -> ControlResult<Namespace>
fn list_namespaces(&self) -> ControlResult<Vec<Namespace>>
Source§fn namespace_for_outsider(
&self,
principal_id: &str,
name: &str,
) -> ControlResult<Option<Namespace>>
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 moreSource§fn set_namespace_settings(
&self,
name: &str,
public: Option<bool>,
events_public: Option<bool>,
) -> ControlResult<Namespace>
fn set_namespace_settings( &self, name: &str, public: Option<bool>, events_public: Option<bool>, ) -> ControlResult<Namespace>
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>
fn delete_namespace(&self, name: &str, delete_repos: bool) -> ControlResult<u32>
delete_repos.fn create_repo(&self, spec: ProtoNewRepo) -> ControlResult<Repo>
Source§fn write_first_commit(
&self,
path: &RepoPath,
readme: &str,
) -> ControlResult<String>
fn write_first_commit( &self, path: &RepoPath, readme: &str, ) -> ControlResult<String>
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.fn get_repo(&self, path: &RepoPath) -> ControlResult<Repo>
fn list_repos(&self, namespace: Option<&str>) -> ControlResult<Vec<Repo>>
Source§fn purge_repo(&self, path: &RepoPath) -> ControlResult<u64>
fn purge_repo(&self, path: &RepoPath) -> ControlResult<u64>
Source§fn gc_status(&self, path: &RepoPath) -> ControlResult<Option<GcJob>>
fn gc_status(&self, path: &RepoPath) -> ControlResult<Option<GcJob>>
Repos.GcStatus). Read moreSource§fn gate_account(
&self,
account: &str,
actor: &str,
note: &str,
) -> ControlResult<(AccountGate, bool)>
fn gate_account( &self, account: &str, actor: &str, note: &str, ) -> ControlResult<(AccountGate, bool)>
Source§fn ungate_account(&self, account: &str) -> ControlResult<Option<AccountGate>>
fn ungate_account(&self, account: &str) -> ControlResult<Option<AccountGate>>
account back in (Accounts.Ungate). Returns what was lifted,
or None if it was not gated. Read moreSource§fn account_gates(&self) -> ControlResult<Vec<AccountGate>>
fn account_gates(&self) -> ControlResult<Vec<AccountGate>>
Accounts.ListAccountGates). Read moreSource§fn purge_account_status(
&self,
account: &str,
) -> ControlResult<(Option<PurgeJob>, Option<AccountGate>)>
fn purge_account_status( &self, account: &str, ) -> ControlResult<(Option<PurgeJob>, Option<AccountGate>)>
Accounts.PurgeStatus), and the
lockout as it stands. Read moreSource§fn default_delete_retention_days(&self) -> u32
fn default_delete_retention_days(&self) -> u32
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>>
fn list_deleted( &self, namespace: Option<&str>, ) -> ControlResult<Vec<DeletedRepoRow>>
Unimplemented, because a control plane that
cannot see tombstones must not report that there are none.Source§fn index_progress(&self) -> ControlResult<Vec<IndexProgressRow>>
fn index_progress(&self) -> ControlResult<Vec<IndexProgressRow>>
fn set_default_branch( &self, path: &RepoPath, default_branch: &str, ) -> ControlResult<Repo>
fn set_visibility( &self, path: &RepoPath, visibility: Visibility, ) -> ControlResult<Repo>
Source§fn encryption_key(
&self,
principal_id: &str,
) -> ControlResult<GetEncryptionKeyResponse>
fn encryption_key( &self, principal_id: &str, ) -> ControlResult<GetEncryptionKeyResponse>
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>
fn create_encryption_key( &self, principal_id: &str, public_key: &str, first: UnlockMethod, ) -> ControlResult<GetEncryptionKeyResponse>
Access.CreateEncryptionKey); AlreadyExists when one is held.Source§fn add_unlock_method(
&self,
principal_id: &str,
method: UnlockMethod,
) -> ControlResult<GetEncryptionKeyResponse>
fn add_unlock_method( &self, principal_id: &str, method: UnlockMethod, ) -> ControlResult<GetEncryptionKeyResponse>
Access.AddUnlockMethod).Source§fn remove_unlock_method(
&self,
principal_id: &str,
id: &str,
) -> ControlResult<GetEncryptionKeyResponse>
fn remove_unlock_method( &self, principal_id: &str, id: &str, ) -> ControlResult<GetEncryptionKeyResponse>
Access.RemoveUnlockMethod); the last is FailedPrecondition.Source§fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo>
fn set_encrypted(&self, path: &RepoPath, encrypted: bool) -> ControlResult<Repo>
Repos.SetEncrypted).
The caller has already checked that the account admin asks.fn list_refs( &self, path: &RepoPath, prefix: &str, peel_them: bool, ) -> ControlResult<Vec<RefTarget>>
fn get_ref( &self, path: &RepoPath, ref_name: &str, peel_them: bool, ) -> ControlResult<RefTarget>
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>
fn create_pull( &self, path: &RepoPath, author: &str, title: &str, description: &str, source_ref: &str, target_ref: &str, ) -> ControlResult<PullRequest>
author is the authenticated principal.Source§fn list_pulls(
&self,
path: &RepoPath,
state: i32,
) -> ControlResult<Vec<PullRequest>>
fn list_pulls( &self, path: &RepoPath, state: i32, ) -> ControlResult<Vec<PullRequest>>
state zero means all.fn get_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>
fn close_pull(&self, path: &RepoPath, number: u64) -> ControlResult<PullRequest>
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>
fn add_principal( &self, id: &str, name: &str, kind: PrincipalKind, ) -> ControlResult<Principal>
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>
fn enroll_employee( &self, account: &str, ask: &EmployeeAsk, ) -> ControlResult<EmployeeAnswer>
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 moreSource§fn seats(&self, account: &str) -> ControlResult<(u32, u32)>
fn seats(&self, account: &str) -> ControlResult<(u32, u32)>
Source§fn clans(&self) -> Option<&dyn ClanPlane>
fn clans(&self) -> Option<&dyn ClanPlane>
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>
fn has_employees(&self, account: &str) -> ControlResult<bool>
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>>
fn employees(&self, account: &str) -> ControlResult<Vec<Principal>>
Source§fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>>
fn principal_by_name(&self, name: &str) -> ControlResult<Option<Principal>>
None.Source§fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim>
fn claim_operator(&self, asked: &str) -> ControlResult<OperatorClaim>
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>
fn rename_principal( &self, principal_id: &str, name: &str, ) -> ControlResult<Principal>
AlreadyExists when another
principal carries the name (as name or id); NotFound for an unknown id.fn list_principals(&self) -> ControlResult<Vec<Principal>>
Source§fn principal_activity(&self) -> ControlResult<BTreeMap<String, PrincipalSeen>>
fn principal_activity(&self) -> ControlResult<BTreeMap<String, PrincipalSeen>>
Source§fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool>
fn holds_server_grant(&self, principal_id: &str) -> ControlResult<bool>
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>
fn add_credential( &self, principal_id: &str, credential_id: &[u8], public_key: &[u8], label: &str, sign_count: u32, ) -> ControlResult<WebCredential>
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>>
fn credentials( &self, principal_id: Option<&str>, ) -> ControlResult<Vec<WebCredential>>
None).Source§fn credential(
&self,
credential_id: &[u8],
) -> ControlResult<Option<StoredCredential>>
fn credential( &self, credential_id: &[u8], ) -> ControlResult<Option<StoredCredential>>
None.Source§fn record_passkey_use(
&self,
credential_id: &[u8],
sign_count: u32,
now_unix_ms: i64,
) -> ControlResult<WebCredential>
fn record_passkey_use( &self, credential_id: &[u8], sign_count: u32, now_unix_ms: i64, ) -> ControlResult<WebCredential>
PermissionDenied). NotFound for a credential that is gone.Source§fn revoke_credential(
&self,
credential_id: &[u8],
) -> ControlResult<Option<WebCredential>>
fn revoke_credential( &self, credential_id: &[u8], ) -> ControlResult<Option<WebCredential>>
None if there was none.Source§fn rename_credential(
&self,
credential_id: &[u8],
label: &str,
) -> ControlResult<Option<WebCredential>>
fn rename_credential( &self, credential_id: &[u8], label: &str, ) -> ControlResult<Option<WebCredential>>
None if there
is no such credential.Source§fn first_invite(&self) -> ControlResult<Option<FirstInvite>>
fn first_invite(&self) -> ControlResult<Option<FirstInvite>>
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<()>
fn drop_first_invite(&self) -> ControlResult<()>
Source§fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>>
fn employer_of(&self, principal_id: &str) -> ControlResult<Option<String>>
principal_id as an employee, if any.Source§fn sso(&self) -> Option<&dyn SsoPlane>
fn sso(&self) -> Option<&dyn SsoPlane>
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>
fn sso_import(&self, order: ImportOrder) -> ControlResult<ImportJob>
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>>
fn sso_import_status(&self, account: &str) -> ControlResult<Option<ImportJob>>
Sso.ImportStatus), if the
server still holds it.Source§fn approve_machine_enrolment(
&self,
approval: MachineApproval,
) -> ControlResult<MachineApproved>
fn approve_machine_enrolment( &self, approval: MachineApproval, ) -> ControlResult<MachineApproved>
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)>
fn remove_principal(&self, principal_id: &str) -> ControlResult<(u32, u32)>
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>
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>
principal_id. Read morefn revoke_key(&self, fingerprint: &str) -> ControlResult<()>
fn list_keys(&self, principal_id: Option<&str>) -> ControlResult<Vec<PublicKey>>
fn grant( &self, principal_id: &str, namespace: &str, repo: &str, level: GrantLevel, ) -> ControlResult<Grant>
fn revoke( &self, principal_id: &str, namespace: &str, repo: &str, ) -> ControlResult<()>
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>
fn add_group_member( &self, group_id: &str, member_id: &str, signed: SignedMembershipEvent, ) -> ControlResult<u64>
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)>
fn remove_group_member( &self, group_id: &str, member_id: &str, signed: SignedMembershipEvent, ) -> ControlResult<(bool, u64)>
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>>
fn membership_events( &self, group_id: Option<&str>, ) -> ControlResult<Vec<SignedMembershipEvent>>
Source§fn list_group_members(&self, group_id: &str) -> ControlResult<Vec<Principal>>
fn list_group_members(&self, group_id: &str) -> ControlResult<Vec<Principal>>
fn get_protection( &self, path: &RepoPath, ref_pattern: Option<&str>, ) -> ControlResult<Vec<BranchProtection>>
fn delete_protection( &self, path: &RepoPath, ref_pattern: &str, ) -> ControlResult<()>
fn get_ref_name_policy(&self, path: &RepoPath) -> ControlResult<RefNamePolicy>
fn set_ref_name_policy( &self, path: &RepoPath, policy: RefNamePolicy, ) -> ControlResult<RefNamePolicy>
Source§fn forge_append(&self, event: SignedForgeEvent) -> ControlResult<u64>
fn forge_append(&self, event: SignedForgeEvent) -> ControlResult<u64>
Source§fn forge_append_attested(
&self,
event: SignedForgeEvent,
session_stepped_up_at_unix_ms: i64,
) -> ControlResult<(u64, String)>
fn forge_append_attested( &self, event: SignedForgeEvent, session_stepped_up_at_unix_ms: i64, ) -> ControlResult<(u64, String)>
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>)>
fn forge_review( &self, repo: &str, pull: u64, ) -> ControlResult<(ReviewState, Vec<SignedForgeEvent>)>
Source§fn forge_reviews(&self, repo: &str) -> ControlResult<Vec<ReviewState>>
fn forge_reviews(&self, repo: &str) -> ControlResult<Vec<ReviewState>>
repo that has one.Source§fn forge_issue(
&self,
repo: &str,
number: u64,
) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>>
fn forge_issue( &self, repo: &str, number: u64, ) -> ControlResult<Option<(IssueState, Vec<SignedForgeEvent>)>>
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>>
fn forge_issues(&self, repo: &str) -> ControlResult<Vec<IssueState>>
repo, folded, in the order they were opened.fn counts(&self) -> ControlResult<ControlCounts>
Source§fn verify_chain(
&self,
request: &VerifyChainRequest,
) -> ControlResult<VerifyChainResponse>
fn verify_chain( &self, request: &VerifyChainRequest, ) -> ControlResult<VerifyChainResponse>
Admin.VerifyChain, roadmap phase 2). Read moreSource§fn blame(
&self,
path: &RepoPath,
rev: &str,
file: &str,
) -> ControlResult<BlameResponse>
fn blame( &self, path: &RepoPath, rev: &str, file: &str, ) -> ControlResult<BlameResponse>
Refs.Blame). Read moreSource§fn search(
&self,
path: &RepoPath,
rev: &str,
pattern: &str,
max_file_bytes: u64,
max_matches: u32,
) -> ControlResult<SearchResponse>
fn search( &self, path: &RepoPath, rev: &str, pattern: &str, max_file_bytes: u64, max_matches: u32, ) -> ControlResult<SearchResponse>
Refs.Search). Read moreSource§fn setting(&self, name: &str) -> ControlResult<Option<Vec<u8>>>
fn setting(&self, name: &str) -> ControlResult<Option<Vec<u8>>>
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>>>
fn set_setting( &self, name: &str, value: &[u8], ) -> ControlResult<Option<Vec<u8>>>
Admin.SetSetting): value under name; the value
it replaced, None for a new name. Bounded (gunnar-core settings), and a refusal names
the bound.fn storage_info( &self, path: Option<&RepoPath>, ) -> ControlResult<StorageInfoResponse>
Source§fn grow_start(
&self,
request: &GrowStartRequest,
) -> ControlResult<GrowStatusResponse>
fn grow_start( &self, request: &GrowStartRequest, ) -> ControlResult<GrowStatusResponse>
Source§fn grow_status(&self) -> ControlResult<GrowStatusResponse>
fn grow_status(&self) -> ControlResult<GrowStatusResponse>
Admin.GrowStatus, Bootstrap.GrowStatus).Source§fn grow_resume(&self, give_up: bool) -> ControlResult<GrowStatusResponse>
fn grow_resume(&self, give_up: bool) -> ControlResult<GrowStatusResponse>
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>
fn is_group_member( &self, principal_id: &str, group: &str, ) -> ControlResult<bool>
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>
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>
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>
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)>
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
fn twin_git_listener_needs_restart(&self) -> bool
Source§fn restore_begin(&self, owner: &str) -> ControlResult<u64>
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<()>
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>
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>
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>
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>
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)>>
fn log_layout(&self) -> ControlResult<Vec<(u64, u64, u64)>>
(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)>
fn data_volume(&self) -> Option<(u64, u64)>
(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>>
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<()>
fn twin_owed_write(&self, bytes: &[u8]) -> ControlResult<()>
twin-owed.json (replace, crash-safe).Source§fn restore_snapshot_chunk(&self, chunk: &LogChunk) -> ControlResult<Option<u64>>
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>
fn health_alarms(&self) -> Vec<HealthAlarm>
Admin.Health.alarms). Default: none.Source§fn is_twin(&self) -> bool
fn is_twin(&self) -> bool
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>
fn frozen(&self) -> Option<String>
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
fn log_follows(&self) -> bool
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
fn topology_facts(&self) -> Facts
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>
fn maintenance_reasons(&self) -> Vec<String>
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>
fn primary_heartbeat(&self, silent_after_ms: u64) -> Option<PrimaryHeartbeat>
silent_after_ms. Default None: not a twin.Source§fn log_head(&self) -> ControlResult<LogHeadResponse>
fn log_head(&self) -> ControlResult<LogHeadResponse>
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>
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<()>
fn fence_by_generation(&self, proof: &GenerationProof) -> ControlResult<()>
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>>
fn log_fetch( &self, from: u64, to: u64, max_bytes: u64, ) -> ControlResult<Vec<LogEntry>>
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>
fn principal(&self, id: &str) -> Result<Option<Principal>, ControlError>
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>
fn set_server_level( &self, principal_id: &str, level: GrantLevel, ) -> Result<(), ControlError>
Source§fn replication_set(
&self,
_target: ReplicationTarget,
) -> Result<ReplicationStatusResponse, ControlError>
fn replication_set( &self, _target: ReplicationTarget, ) -> Result<ReplicationStatusResponse, ControlError>
Source§fn replication_remove(
&self,
_name: &str,
) -> Result<ReplicationStatusResponse, ControlError>
fn replication_remove( &self, _name: &str, ) -> Result<ReplicationStatusResponse, ControlError>
Source§fn replication_demote(&self) -> Result<ReplicationStatusResponse, ControlError>
fn replication_demote(&self) -> Result<ReplicationStatusResponse, ControlError>
Source§fn replication_set_role(
&self,
_role: ReplicationRole,
) -> Result<ReplicationStatusResponse, ControlError>
fn replication_set_role( &self, _role: ReplicationRole, ) -> Result<ReplicationStatusResponse, ControlError>
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>
fn replication_abdicate( &self, ) -> Result<ReplicationStatusResponse, ControlError>
Source§fn replication_status(
&self,
_include_refs: bool,
) -> Result<ReplicationStatusResponse, ControlError>
fn replication_status( &self, _include_refs: bool, ) -> Result<ReplicationStatusResponse, ControlError>
Source§fn replication_resync(
&self,
_name: &str,
_repo: Option<&RepoPath>,
_except: &[RepoPath],
) -> Result<ResyncReplicationResponse, ControlError>
fn replication_resync( &self, _name: &str, _repo: Option<&RepoPath>, _except: &[RepoPath], ) -> Result<ResyncReplicationResponse, ControlError>
Source§fn replication_flush(
&self,
_request: &FlushReplicationRequest,
) -> Result<FlushReplicationResponse, ControlError>
fn replication_flush( &self, _request: &FlushReplicationRequest, ) -> Result<FlushReplicationResponse, ControlError>
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>
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>
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>
fn webhook_add( &self, _request: AddWebhookRequest, ) -> Result<WebhookListResponse, ControlError>
Source§fn webhook_list(&self) -> Result<WebhookListResponse, ControlError>
fn webhook_list(&self) -> Result<WebhookListResponse, ControlError>
Source§fn webhook_remove(
&self,
_name: &str,
) -> Result<WebhookListResponse, ControlError>
fn webhook_remove( &self, _name: &str, ) -> Result<WebhookListResponse, ControlError>
Source§impl EnterprisePlane for CoreControlPlane
impl EnterprisePlane for CoreControlPlane
Source§fn account_of_namespace(&self, namespace: &str) -> ControlResult<Option<String>>
fn account_of_namespace(&self, namespace: &str) -> ControlResult<Option<String>>
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>>
fn account_of_principal( &self, principal_id: &str, ) -> ControlResult<Option<String>>
principal_id belongs to: its employer, or its own.Source§fn is_account_admin(
&self,
principal_id: &str,
account: &str,
) -> ControlResult<bool>
fn is_account_admin( &self, principal_id: &str, account: &str, ) -> ControlResult<bool>
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>
fn shielded(&self, principal_id: &str, namespace: &str) -> ControlResult<bool>
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<()>
fn account_audit_record( &self, account: &str, event: &AccountAuditEvent, ) -> ControlResult<()>
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>
fn account_audit_log( &self, account: &str, query: &AccountAuditQuery, ) -> ControlResult<AuditLogResponse>
account’s log, its chain’s state with it.Source§fn account_audit_export(
&self,
account: &str,
query: &AccountAuditQuery,
format: AuditExportFormat,
) -> ControlResult<ExportAuditLogResponse>
fn account_audit_export( &self, account: &str, query: &AccountAuditQuery, format: AuditExportFormat, ) -> ControlResult<ExportAuditLogResponse>
Source§fn aggregates(&self) -> ControlResult<AggregatesResponse>
fn aggregates(&self) -> ControlResult<AggregatesResponse>
Source§fn operator_access_request(
&self,
account: &str,
operator: &str,
reason: &str,
minutes: u32,
) -> ControlResult<OperatorAccess>
fn operator_access_request( &self, account: &str, operator: &str, reason: &str, minutes: u32, ) -> ControlResult<OperatorAccess>
account.Source§fn operator_access_list(
&self,
account: Option<&str>,
) -> ControlResult<Vec<OperatorAccess>>
fn operator_access_list( &self, account: Option<&str>, ) -> ControlResult<Vec<OperatorAccess>>
account (None: every account), newest first.Source§fn operator_access_get(
&self,
request_id: &str,
) -> ControlResult<Option<OperatorAccess>>
fn operator_access_get( &self, request_id: &str, ) -> ControlResult<Option<OperatorAccess>>
Source§fn operator_access_decide(
&self,
decision: &OperatorAccessDecision,
) -> ControlResult<OperatorAccess>
fn operator_access_decide( &self, decision: &OperatorAccessDecision, ) -> ControlResult<OperatorAccess>
Source§fn operator_access_revoke(
&self,
request_id: &str,
by: &str,
) -> ControlResult<OperatorAccess>
fn operator_access_revoke( &self, request_id: &str, by: &str, ) -> ControlResult<OperatorAccess>
Source§fn name_check(
&self,
kind: &str,
name: &str,
account: &str,
) -> ControlResult<NameVerdict>
fn name_check( &self, kind: &str, name: &str, account: &str, ) -> ControlResult<NameVerdict>
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>
fn name_review_add( &self, kind: &str, name: &str, account: &str, actor: &str, hold: bool, ) -> ControlResult<NameReview>
account’s admin (hold), or note a
refused one for them (!hold).Source§fn name_lists(&self) -> ControlResult<NameLists>
fn name_lists(&self) -> ControlResult<NameLists>
Source§fn set_name_lists(&self, lists: NameLists) -> ControlResult<NameLists>
fn set_name_lists(&self, lists: NameLists) -> ControlResult<NameLists>
Source§fn name_reviews(&self, account: &str) -> ControlResult<Vec<NameReview>>
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>
fn name_review_decide( &self, account: &str, id: &str, allow: bool, by: &str, ) -> ControlResult<NameReview>
Source§impl FirstBoot for CoreControlPlane
First boot, delegated straight to gunnar_core::AuthTable.
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>
fn bootstrap_state(&self) -> ControlResult<BootstrapState>
Source§fn enroll_first_identity(
&self,
name: &str,
openssh_public_key: &str,
) -> ControlResult<Enrolled>
fn enroll_first_identity( &self, name: &str, openssh_public_key: &str, ) -> ControlResult<Enrolled>
Source§impl Identity for CoreControlPlane
The one place gRPC identity is decided, and it decides nothing itself.
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]:
| question | answered 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>
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>>
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>>>
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>>
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>>
fn registered_key( &self, fingerprint: &str, ) -> ControlResult<Option<RegisteredKey>>
SHA256:… fingerprint, or None if there is
no such key, the key has expired, or its principal is gone. Read moreSource§fn may(
&self,
principal_id: &str,
access: &Access,
level: GrantLevel,
) -> ControlResult<bool>
fn may( &self, principal_id: &str, access: &Access, level: GrantLevel, ) -> ControlResult<bool>
gunnar_core::AuthTable::may
(the ACL model) seen through the seam.Source§impl SsoPlane for CoreControlPlane
impl SsoPlane for CoreControlPlane
Source§fn provider(&self, account: &str) -> ControlResult<Option<SsoProvider>>
fn provider(&self, account: &str) -> ControlResult<Option<SsoProvider>>
has_client_secret filled, no secret.Source§fn any_provider(&self) -> ControlResult<bool>
fn any_provider(&self) -> ControlResult<bool>
DiscoverResponse.any_provider).Source§fn set_provider(
&self,
provider: SsoProvider,
secret: SecretChange,
actor: &str,
) -> ControlResult<SsoProvider>
fn set_provider( &self, provider: SsoProvider, secret: SecretChange, actor: &str, ) -> ControlResult<SsoProvider>
fn remove_provider(&self, account: &str) -> ControlResult<bool>
fn account_for_domain(&self, domain: &str) -> ControlResult<Option<String>>
Source§fn client_secret(&self, account: &str) -> ControlResult<Option<String>>
fn client_secret(&self, account: &str) -> ControlResult<Option<String>>
fn login(&self, ask: SsoLoginAsk) -> ControlResult<SsoLoginOutcome>
Source§fn directory(&self, account: &str) -> ControlResult<Vec<DirUser>>
fn directory(&self, account: &str) -> ControlResult<Vec<DirUser>>
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<()>
fn import( &self, account: &str, rows: &[(String, ImportRow)], actor: &str, now_unix_ms: i64, commit: bool, ) -> ControlResult<()>
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)>
fn update_user( &self, account: &str, principal_id: &str, patch: UserPatch, actor: &str, ) -> ControlResult<(DirUser, bool)>
active changed.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>
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>
expires_at_unix_ms 0 never expires,
read_only may only read (A34-27).fn scim_tokens(&self, account: &str) -> ControlResult<Vec<ScimToken>>
fn revoke_scim_token(&self, account: &str, id: &str) -> ControlResult<bool>
Source§fn scim_account(
&self,
secret_hash: &str,
) -> ControlResult<Option<(String, Bearer)>>
fn scim_account( &self, secret_hash: &str, ) -> ControlResult<Option<(String, Bearer)>>
fn groups(&self, account: &str) -> ControlResult<Vec<GroupRow>>
fn put_group(&self, group: GroupRow) -> ControlResult<GroupRow>
fn remove_group(&self, account: &str, id: &str) -> ControlResult<bool>
Source§fn passkey_login_refused(
&self,
principal_id: &str,
) -> ControlResult<Option<String>>
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>>>
fn passkeys_of(&self, principal_id: &str) -> ControlResult<Vec<Vec<u8>>>
allowCredentials.Source§fn verify_domain(
&self,
account: &str,
domain: &str,
) -> ControlResult<SsoProvider>
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>>
fn rebind_users( &self, account: &str, principal_ids: &[String], external_id: &str, ) -> ControlResult<Vec<DirUser>>
external_id (one employee only) is its id at the new IdP.Auto Trait Implementations§
impl !RefUnwindSafe for CoreControlPlane
impl !UnwindSafe for CoreControlPlane
impl Freeze for CoreControlPlane
impl Send for CoreControlPlane
impl Sync for CoreControlPlane
impl Unpin for CoreControlPlane
impl UnsafeUnpin for CoreControlPlane
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request