#[non_exhaustive]pub enum Error {
Show 44 variants
InvalidPath {
input: String,
reason: PathReject,
},
PathEscape {
path: PathBuf,
reason: String,
},
NoSuchRepo(RepoRef),
RepoExists(RepoRef),
NoSuchNamespace(String),
NamespaceExists(String),
NamespaceNotEmpty {
name: String,
repos: usize,
},
AlreadyBootstrapped {
at: UnixMillis,
principal_id: String,
},
AlreadyProvisioned {
principals: usize,
keys: usize,
},
NoSuchPrincipal(String),
PrincipalExists(String),
NameExists {
name: String,
principal_id: String,
},
NotAGroup(String),
ReservedPrincipalId(String),
KeyExists {
fingerprint: String,
principal_id: String,
},
TokenExists {
hash: String,
principal_id: String,
},
CredentialExists {
id: String,
principal_id: String,
},
NoSuchCredential(String),
CounterRollback {
id: String,
stored: u32,
presented: u32,
},
InviteInvalid,
InviteExists(String),
NotAPerson {
principal_id: String,
kind: String,
},
NotAMachine {
principal_id: String,
kind: String,
},
Key(String),
BadFingerprint(String),
IssuerExists {
subject: String,
fingerprint: String,
},
NoIssuer,
Pki(String),
Denied {
principal_id: String,
level: Level,
repo: RepoRef,
},
Gated {
account: String,
reason: String,
note: String,
},
CorruptDocument {
ref_name: String,
reason: String,
},
Contended {
ref_name: String,
attempts: usize,
},
BadBranchName(String),
InvalidInput {
what: &'static str,
reason: String,
},
RoomShort {
account: String,
need: u64,
unused: u64,
short: u64,
},
SsoRefused(String),
NoOpenSeat(String),
Store(Error),
ControlStore(String),
OffStripe {
why: String,
},
LogFollows,
Json(Error),
Other(Error),
Clan {
clan: String,
refusal: ClanRefusal,
reason: String,
},
}Expand description
Everything the repository model, the identity table and the sandbox can refuse.
The variants are deliberately shaped so the SSH and gRPC layers can map them
onto their own vocabularies without parsing strings: Error::InvalidPath
carries a PathReject, Error::Denied carries the level that was
missing.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
InvalidPath
The path sandbox refused an SSH command argument. This is the security
boundary of the ACL model; see crate::sandbox.
Fields
reason: PathRejectWhich rule fired.
PathEscape
A RepoRef would have resolved outside the repository root once
symlinks were followed. Only the fs backend can produce this.
NoSuchRepo(RepoRef)
No such repository in the catalog.
RepoExists(RepoRef)
A repository with this path is already in the catalog.
NoSuchNamespace(String)
No such namespace. Namespaces are created explicitly; a repository is never allowed to conjure one, because a typo would otherwise become a tenant.
NamespaceExists(String)
A namespace with this name is already in the catalog.
NamespaceNotEmpty
A namespace was deleted without delete_repos while it still held
repositories.
AlreadyBootstrapped
The unauthenticated enrolment window has already been used.
It is shut permanently, and this is the answer whatever the identity table now holds — including when every identity it created has since been deleted. The record lives beside the table, not in it, precisely so that this refusal cannot be undone by a delete.
Fields
at: UnixMillisWhen it happened, in milliseconds since the unix epoch.
AlreadyProvisioned
Enrolment was attempted against a table that already holds identities but carries no bootstrap record: a table written before the record existed.
A separate variant from Error::AlreadyBootstrapped so an operator
can tell “somebody already enrolled” from “this is an older data path”,
and so a test can prove which of the two refusals fired.
Fields
NoSuchPrincipal(String)
No principal with this id.
PrincipalExists(String)
A principal with this id already exists.
NameExists
Another principal already carries this name — as its name, or as its id.
Refused so that a listing never shows two rows a human reads as one, and so a name can never be mistaken for a different principal’s id on the wire. The id is the identity; the name is the label; the label is still unique, by construction, in the same compare-and-swap as the row.
NotAGroup(String)
This principal exists but is not a Group,
so it cannot have members.
Membership is a property of groups alone: adding a member to a user or a machine would create an edge the ACL never reads, which is worse than refusing it, because it looks like access was granted when none was.
ReservedPrincipalId(String)
The id is reserved by gunnar itself and may not be registered.
Today there is exactly one:
ANONYMOUS_PRINCIPAL_ID. A
principal registered under that name would inherit the anonymous
public-read rule in AuthTable::may, and,
because it would be in the table, would be handed a key and a grant
as well. Refusing the name is what keeps “anonymous holds nothing” a
property of the system rather than of the operator’s care.
KeyExists
This public key is already registered, possibly to another principal.
Re-registering is refused rather than silently re-pointed: a key that maps to two principals makes the SSH auth answer ambiguous, and the ambiguity would be resolved by whichever came first in a map.
TokenExists
This access token secret is already registered.
Refused for the reason Error::KeyExists gives, and for one more: two
principals sharing a secret means the holder of that secret cannot be
told which of them they are acting as, and revoking it for one revokes
it for both. Colliding by accident takes a 256-bit coincidence; getting
here means the same secret was registered twice on purpose.
Fields
CredentialExists
This web credential id is already registered (Stage L).
NoSuchCredential(String)
No web credential with this id (Stage L).
CounterRollback
A web login whose signature counter did not increase (Stage L): a cloned authenticator, or a replay. Re-checked inside the update that would have stored the new counter.
Fields
InviteInvalid
An invite that cannot be redeemed (Stage L): unknown, already used, expired, or for somebody else — one sentence for all four, so a refusal says nothing about which secrets exist.
InviteExists(String)
This invite id is already taken (Stage L).
NotAPerson
No human credential on a machine principal (Stage L): a web credential (a passkey) is a person’s, and only a USER is a person.
NotAMachine
A machine’s credential never lands on a person (2026-10-05): an approved machine enrolment names a principal that exists and is not a MACHINE.
Key(String)
A public key could not be parsed, or is not in OpenSSH form.
BadFingerprint(String)
A fingerprint string was not in OpenSSH’s SHA256:<base64> form.
IssuerExists
This server already has an issuer (the certificate-identity plan).
Refused rather than silently ignored, and refused rather than replaced. A second issuer is a second answer to who may mint a user, and the failure mode of two is that nobody can say which one signed a given client. Replacing the one that exists invalidates every certificate in the field, so it is a different verb.
Fields
NoIssuer
Something asked this server to issue a certificate and it has no issuer.
Pki(String)
Minting, importing or issuing failed.
Its own variant rather than Error::Key, which is about OpenSSH
public keys: an operator reading “not a usable OpenSSH public key” after
handing over an X.509 CSR would look in the wrong place.
Denied
Access was refused by the ACL.
Fields
Gated
The account is gated (see crate::gate): locked out entirely, at
every level, until the gate is lifted.
Its own variant and not an Error::Denied with a different message,
because the two ask different things of whoever reads them. Denied
means get a grant; this means a grant will not help, the account is
shut. A user handed the first when the second is true spends the
afternoon in the ACL.
Fields
CorruptDocument
A stored control document could not be decoded. The store is not trusted to be a valid document store, so this is a real error and never a silent empty result. An empty catalog and a corrupt catalog must not look the same.
Contended
A compare-and-swap update lost its race too many times in a row.
BadBranchName(String)
A default branch name is not a legal refs/heads/... name.
InvalidInput
A stored document was handed a value it will not keep.
The empty policy document is the case this was added for: an empty
string reads back as “a policy exists and configures nothing”, which
is indistinguishable from a write that lost its content, so it is
refused and the caller is told which call removes a policy instead. It
is deliberately not InvalidPath — nothing about a path is wrong —
and deliberately not a CorruptDocument, which is about reading.
Fields
RoomShort
Not enough room (owner, 2026-10-04, “Add users”): the account’s room that is neither stored nor promised is less than what was asked. Names how much is missing, because that is what the account admin has to act on.
Fields
SsoRefused(String)
A single sign-on the identity table will not commit (company SSO, 2026-10-05): a subject bound elsewhere, a deactivated directory user, an account with no identity provider, no free id for a new employee. The sentence says which; it never carries a token or a secret.
NoOpenSeat(String)
No open seat: a seat claim on an account whose seats are all filled, or that has none.
Store(Error)
The storage layer failed.
ControlStore(String)
The control store failed — the redb database the catalog and the
identity table live in (the certificate-identity plan, crate::control).
Its own variant rather than an Error::Other, because it is not the
same fault as Error::Store: that one is a repository’s backend,
and an operator whose control plane is unreadable has a different
problem from one whose repository is. Never produced for “the document
has never been written”, which is Ok(None).
OffStripe
The control store is OFF THE STRIPE — suspended by the runtime
data-set growth (ControlDb::suspend): /data is unmounted, every
read is answered from the RAM copy taken before it went, and every
WRITE is refused with this until the set is mounted and the store
resumed. DATA-SET-GROWTH-FLOW.md G4/T2/D7: refuse by name, never
queue; the caller retries once the growth is done. Maps to
UNAVAILABLE on the control plane, never to INTERNAL.
LogFollows
This box’s transaction log FOLLOWS another’s — it is being restored (or it is a twin): it originates no write until the restore is whole. Not a fault of the server and not the caller’s: try again (the control plane answers UNAVAILABLE).
Json(Error)
A control document could not be encoded or decoded as JSON.
Other(Error)
Anything else.
Clan
A clan verb refused (owner, 2026-10-05: “ordinary accounts can
group with other in a clan, where group created repos are administered
by the group admin”). refusal says which kind of no it is — the clan
is not there, the caller is not its admin, the rule (room, the floor,
the last admin, an employee) — and reason is the whole sentence the
person reads.
Implementations§
Source§impl Error
impl Error
Sourcepub fn is_path_rejection(&self) -> bool
pub fn is_path_rejection(&self) -> bool
true if this is the sandbox refusing an input, rather than a fault.
The SSH layer answers these with a channel failure and a log line, and must not confuse them with a storage error.
Sourcepub fn path_reject(&self) -> Option<PathReject>
pub fn path_reject(&self) -> Option<PathReject>
The PathReject behind an Error::InvalidPath, for tests that
assert which rule fired.
Sourcepub fn is_denied(&self) -> bool
pub fn is_denied(&self) -> bool
true if this is an access refusal.
Deliberately false for Error::Gated. The SSH and gRPC layers
route on these two, and folding a lockout into “denied” is how a user
gets sent to look for a grant that would change nothing.
Sourcepub fn is_gated(&self) -> bool
pub fn is_gated(&self) -> bool
true if the account is locked out (see crate::gate).
Trait Implementations§
Source§impl Error for Error
impl Error for Error
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()