Skip to main content

Error

Enum Error 

Source
#[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
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

InvalidPath

The path sandbox refused an SSH command argument. This is the security boundary of the ACL model; see crate::sandbox.

Fields

§input: String

The offending input, escaped for safe logging.

§reason: PathReject

Which rule fired.

§

PathEscape

A RepoRef would have resolved outside the repository root once symlinks were followed. Only the fs backend can produce this.

Fields

§path: PathBuf

The path that was being resolved.

§reason: String

What was wrong with it.

§

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.

Fields

§name: String

The namespace.

§repos: usize

How many repositories are in it.

§

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: UnixMillis

When it happened, in milliseconds since the unix epoch.

§principal_id: String

Who it created.

§

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

§principals: usize

How many principals are in the table.

§keys: usize

How many keys are in the table.

§

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.

Fields

§name: String

The name that was asked for.

§principal_id: String

Who holds it.

§

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.

Fields

§fingerprint: String

The fingerprint that is already known.

§principal_id: String

Who holds it.

§

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

§hash: String

The hash that is already known. Never the secret.

§principal_id: String

Who holds it.

§

CredentialExists

This web credential id is already registered (Stage L).

Fields

§id: String

The credential id, base64url.

§principal_id: String

Who holds it.

§

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

§id: String

The credential id, base64url.

§stored: u32

The counter stored.

§presented: u32

The counter presented.

§

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.

Fields

§principal_id: String

The principal asked about.

§kind: String

What it is instead.

§

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.

Fields

§principal_id: String

The principal asked about.

§kind: String

What it is instead.

§

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

§subject: String

The live issuer’s subject common name.

§fingerprint: String

Its SHA256: fingerprint.

§

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

§principal_id: String

Who asked.

§level: Level

What they asked for.

§repo: RepoRef

Which repository.

§

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

§account: String

The account — a principal id, which is also its namespace.

§reason: String

administrative or purge.

§note: String

What the operator or the purge wrote, for the user to act on.

§

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.

Fields

§ref_name: String

Which document.

§reason: String

Why it could not be read.

§

Contended

A compare-and-swap update lost its race too many times in a row.

Fields

§ref_name: String

Which document.

§attempts: usize

How many times it was retried.

§

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

§what: &'static str

What was being stored.

§reason: String

Why it was refused, and what to do instead.

§

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

§account: String

The account.

§need: u64

What the enrolment asked of it.

§unused: u64

What it holds that is neither stored nor promised.

§short: u64

What a sale would have to add for the ask to fit: more than need - unused when the account already stores or promises more than its cap.

§

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.

Fields

§why: String

What the growth is doing right now.

§

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.

Fields

§clan: String

The clan (its namespace).

§refusal: ClanRefusal

Which kind of refusal.

§reason: String

The sentence.

Implementations§

Source§

impl Error

Source

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.

Source

pub fn path_reject(&self) -> Option<PathReject>

The PathReject behind an Error::InvalidPath, for tests that assert which rule fired.

Source

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.

Source

pub fn is_gated(&self) -> bool

true if the account is locked out (see crate::gate).

Trait Implementations§

Source§

impl Debug for Error

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Display for Error

Source§

fn fmt(&self, __formatter: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Error for Error

Source§

fn source(&self) -> Option<&(dyn Error + 'static)>

Returns the lower-level source of this error, if any. Read more
1.0.0 · Source§

fn description(&self) -> &str

👎Deprecated since 1.42.0:

use the Display impl or to_string()

1.0.0 · Source§

fn cause(&self) -> Option<&dyn Error>

👎Deprecated since 1.33.0:

replaced by Error::source, which can support downcasting

Source§

fn provide<'a>(&'a self, request: &mut Request<'a>)

🔬This is a nightly-only experimental API. (error_generic_member_access)
Provides type-based access to context intended for error reports. Read more
Source§

impl From<Error> for KeyRefusal

Source§

fn from(e: Error) -> Self

Converts to this type from the input type.
Source§

impl From<Error> for Error

Source§

fn from(source: Error) -> Self

Converts to this type from the input type.
Source§

impl From<Error> for Error

Source§

fn from(source: Error) -> Self

Converts to this type from the input type.
Source§

impl From<Error> for Error

Source§

fn from(source: Error) -> Self

Converts to this type from the input type.
Source§

impl From<Error> for Error

Source§

fn from(err: Error) -> Self

Converts to this type from the input type.

Auto Trait Implementations§

§

impl !RefUnwindSafe for Error

§

impl !UnwindSafe for Error

§

impl Freeze for Error

§

impl Send for Error

§

impl Sync for Error

§

impl Unpin for Error

§

impl UnsafeUnpin for Error

Blanket Implementations§

Source§

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

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

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

Source§

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

Source§

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

Source§

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

Source§

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

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

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

Source§

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

Mutably borrows from an owned value. Read more
Source§

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

Source§

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

Source§

impl<T> ErrorExt for T
where T: Error + Send + Sync + 'static,

Source§

fn raise(self) -> Exn<Self>
where Self: Sized,

Raise this error as a new exception.
Source§

fn and_raise<T>(self, context: T) -> Exn<T>
where T: Error + Send + Sync + 'static, Self: Sized,

Raise this error as a child of a new exception with the given context error. Read more
Source§

fn raise_erased(self) -> Exn
where Self: Sized,

Raise this error as a new exception, with type erasure.
Source§

fn raise_all<T, I>(self, sources: I) -> Exn<Self>
where Self: Sized, T: Error + Send + Sync + 'static, I: IntoIterator, <I as IntoIterator>::Item: Into<Exn<T>>,

Raise this error as a new exception, with sources as causes.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

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

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

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

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

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

Source§

fn into(self) -> U

Calls U::from(self).

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

Source§

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

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

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

Source§

type Error = !

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

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

Performs the conversion.
Source§

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

Source§

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

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

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

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

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

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

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

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