gunnar-core 3.0.0

gunnar's repository model, identity table, ACL and path sandbox. gunnar-ssh, gunnar-grpc and gunnar-cli all sit on it.
Documentation
//! **gunnar's repository model, identity table, ACL and path sandbox.** The
//! layer `gunnar-ssh`, `gunnar-grpc` and `gunnar-cli` all sit on (the ACL model, multi-tenancy).
//!
//! # What this crate answers
//!
//! An SSH connection arrives with a public key and a command line. Four
//! questions have to be answered before a single byte of git protocol is
//! spoken, and this crate answers all four:
//!
//! | question | answer |
//! |---|---|
//! | *who is this?* | [`AuthTable::principal_by_fingerprint`], on the key's `SHA256:` fingerprint, looked up in gunnar's own table |
//! | *what did they ask for?* | [`resolve_repo_path`]: **the sandbox**, and the security core of the server |
//! | *does it exist?* | [`RepoCatalog::open`] |
//! | *may they?* | [`AuthTable::may`]. `upload-pack` needs [`Level::Read`], `receive-pack` needs [`Level::Write`] |
//!
//! ```
//! use gunnar_core::{resolve_repo_path, Level};
//!
//! // Already unquoted: git wraps this in single quotes on the wire and
//! // `gunnar_ssh::command` takes them off. The sandbox does not know SSH
//! // exists (the one-sandbox rule).
//! let path = resolve_repo_path("team/sub/project.git").expect("a legal path");
//! assert_eq!(path.namespace(), "team/sub");
//! assert_eq!(path.name(), "project");
//! assert_eq!(path.key(), "team/sub/project");
//!
//! // The escape does not resolve, and never becomes a filesystem path at all.
//! assert!(resolve_repo_path("ns/../../etc/passwd").is_err());
//! assert!(Level::Admin.permits(Level::Read));
//! assert!(!Level::Read.permits(Level::Write));
//! ```
//!
//! # One sandbox, one fingerprint
//!
//! `gunnar-ssh` was written concurrently with this crate and grew **its own**
//! path sandbox, key fingerprint, host-key loader and auth lookup. The two
//! sandboxes disagreed: one enforced exactly two path components, this one
//! permits nested namespaces. Two security checks that disagree are worse than
//! either alone. `gunnar-ssh` now depends on this crate and its copies are
//! deleted. What survives lives here:
//!
//! | the one implementation | here |
//! |---|---|
//! | path sandbox → `(namespace, repo)` | [`sandbox::resolve_repo_path`] |
//! | key fingerprint | [`auth::fingerprint_of_blob`] → [`Fingerprint`] |
//! | host key | [`host_key::ensure_host_key`] |
//! | principal + grant lookup | [`AuthTable`] |
//!
//! The fingerprint is defined over **bytes**, not over an `ssh-key` type,
//! because russh pins `ssh-key =0.7.0-rc.10` and this crate links the
//! workspace's `0.6`; see [`auth::fingerprint_of_blob`].
//!
//! # No OS underneath
//!
//! There are **no OS users**. No `/etc/passwd`, no home directories, no
//! `~/.ssh/authorized_keys`, no `ssh-keygen` subprocess. gunnar owns its own
//! identity table, mints its own host key in-process, and works when this
//! binary is PID 1 in a distroless image (the no-OS server rule). The one function that
//! touches a filesystem, [`RepoRef::path_under`], exists for the `fs` backend
//! and is where the symlink check lives; nothing else in this crate knows what
//! a path is.
//!
//! # Where the state lives
//!
//! In the control store ([`control`]) and, for the host key, on the
//! [`gunnar_store`] seam — and on nothing else. The catalog and the identity
//! table are JSON documents behind a name in a redb database, updated by a real
//! compare-and-swap; the host key is a [`SecretStore`](gunnar_store::SecretStore)
//! entry. Until 2026-08-09 the documents were git blobs behind git references,
//! spelled in gunnar's own `ObjectStore`/`RefStore` traits; both are gone (the
//! storage contract is `GitOps` now, and it takes objects as packs). See
//! [`control`] for why they moved and [`doc`] for what the shape costs.
//!
//! # Verification
//!
//! Measure, never assert, concretely, in `tests/`:
//!
//! * the sandbox's escape table is asserted to go **red** against a
//!   deliberately broken resolver, and its legitimate-path table is asserted to
//!   still resolve, because a sandbox that rejects everything is the
//!   identity-value trap;
//! * fingerprints are pinned against known answers from the **real OpenSSH
//!   binary**, not against gunnar recomputing its own arithmetic;
//! * the ACL is asserted in both directions: a `Read` principal refused a
//!   write, an `Admin` allowed one;
//! * the host key is asserted byte-identical across a simulated restart, *and*
//!   two fresh mints are asserted to differ, so the first assertion cannot pass
//!   on a constant;
//! * both sha1 and sha256 repositories are exercised.

#![warn(missing_docs)]
#![warn(clippy::all)]
#![doc(
    html_logo_url = "https://gunnar.rs/download/gunnar.webp",
    html_favicon_url = "https://gunnar.rs/download/favicon.svg"
)]

/// The account admin's audit log: per account, hash-chained, a year (2026-10-05).
pub mod account_audit;
/// When a principal last signed in and last changed something (2026-10-21).
pub mod activity;
pub mod arrow_log;
pub mod audit;
pub mod auth;
pub mod catalog;
pub mod control;
pub mod doc;
pub mod encryption_key;
#[cfg(feature = "metered")]
pub mod entitlement;
pub mod error;
/// **How close a certificate is to bricking the box it protects.** An
/// appliance has no shell and its only door is the mutual listener, so this is
/// graded and REFUSED early rather than reported.
pub mod expiry;
/// **The flow ledger**: what MOVED through a namespace, per UTC day — bytes in
/// and out, pushes, fetches, refusals, and when the tenant was last seen. Every
/// other meter gunnar keeps is a STOCK meter and reads the same for a busy
/// tenant and a silent one.
pub mod flow;
/// The forge's evidence spine: signed, append-only issue and review events.
pub mod forge;
pub mod history;
pub mod host_key;
pub mod issuer;
/// Where a repository's pull-mirror registration lives (`refs/gunnar/mirrors`).
/// Group membership as signed, append-only evidence — the signing half of
/// punkt 9, beside [`auth`]'s live table.
pub mod key_attestation;
pub mod membership;
pub mod mirror_store;
pub mod model;
/// The name filter: offensive, abusive and reserved names (2026-10-05).
pub mod name_filter;
pub mod observe;
/// Where a repository's policy document lives (`refs/gunnar/policy`).
pub mod policy_store;
/// Build provenance, signed — links 5 and 6 of the chain.
pub mod provenance;
pub mod pulls;
pub mod sandbox;
/// A CycloneDX SBOM out of a locked `Cargo.lock`.
pub mod sbom;
pub mod seal;
pub mod secrets;
pub mod settings;
/// One signer and one verifier for every signed event log (one implementation).
pub mod sshsig;
/// Company single sign-on: the identity providers, the directories, SCIM.
pub mod sso;
/// The transparency log's arithmetic: a Merkle tree and a signed tree head.
pub mod transparency;
pub mod txlog;

/// Outbound webhook delivery signatures: the sshsig over each delivered body.
pub mod webhook;

/// **The account gate**: the row that says a user may do nothing, and the lease
/// that keeps a dead purge from bricking an account.
pub mod gate;

pub use gate::{AccountGate, GateReason, ADMINISTRATIVE_HOLDER, PURGE_LEASE_MS};

pub use activity::{PrincipalActivity, Seen as ActivitySeen, ACTIVITY_FILE};
pub use audit::{AuditChain, AuditEntry, AuditError, AuditEvent, Recorded, AUDIT_FILE};

pub use auth::operator_access::{
    OperatorAccess, OperatorAccessState, Shield, OPERATOR_ACCESS_DEFAULT_MINUTES,
    OPERATOR_ACCESS_MAX_MINUTES, OPERATOR_REQUEST_TTL_MS,
};
/// Clans, round 2 (2026-10-05): a repository offered out of a clan.
pub use auth::ClanRepoOffer;
pub use auth::{
    fingerprint_of_blob, mint_token_secret, token_hash, AuthTable, Bootstrap, BootstrapRecord,
    BulkUser, EmployeeRoom, Enrolled, Fingerprint, Readiness, RedeemedInvite, RoomAnswer, RoomAsk,
    RoomGrant, SeatLot, SsoImportOp, SsoImportRow, SsoLogin, SsoLoginDone, SsoProvision,
    SsoUserPatch, AUTH_REF, BOOTSTRAP_ADMIN_ID, LAST_USED_GRANULARITY, REDEEMED_INVITE_KEEP_MS,
    SHA256_PREFIX,
};
/// Clans (2026-10-05): ordinary accounts grouped, each giving part of its room.
pub use auth::{AccountRoom, Clan, ClanInvite, ClanMember, ClanRefusal, ClanRole};
pub use catalog::{point_head_at, NewRepo, RepoCatalog, CATALOG_REF};
pub use control::{
    memory_control, memory_control_store, open_control, open_control_store, open_control_with_db,
    BlobId, ControlDb, ControlStore, ControlView, DocVersion, RamCopy, RedbControlStore,
    ResumeReport, SharedControlStore, BLOB_ID_LEN, RAM_COPY_CEILING_BYTES, TWIN_APPLIED,
};
#[cfg(feature = "metered")]
pub use entitlement::{Entitlement, EntitlementState, EntitlementStore, ENTITLEMENT_REF};
pub use error::{human_bytes, Error, Result};
pub use flow::{
    day_of, iso8601_utc, midnight_of, Fleet, FlowDay, FlowLedger, FlowObserver, FlowStore, Glance,
    NamespaceFlow, Refusal, Transfer, DAY_MS, FLOW_REF, PRINCIPAL_CEILING, RETENTION_DAYS,
};
pub use forge::{Attested, Event, ForgeEvent, Review, SignedEvent, FORGE_NAMESPACE};
pub use history::{read_sealed, History, RefTransaction, SealReport};
pub use host_key::{ensure_host_key, mint_ed25519, os_rng, HostKey};
pub use issuer::{
    Issued, Issuer, IssuerOrigin, Pki, TrustedCa, DEFAULT_ISSUER_DAYS, DEFAULT_LEAF_DAYS,
    ISSUER_REF,
};
pub use key_attestation::{
    AttestationProblem, AttestingKey, KeyAttestation, SignedKeyAttestation,
    KEY_ATTESTATION_NAMESPACE,
};
pub use membership::{
    fold as fold_membership, Membership, MembershipAction, MembershipEvent, SignedMembershipEvent,
    MEMBERSHIP_NAMESPACE,
};
pub use mirror_store::{
    credential_secret_key, MirrorCredentialKind, MirrorRecord, MirrorStore, MAX_RESUMES, MIRROR_REF,
};
pub use model::{
    now_ms, AccessToken, FirstInviteRow, Grant, HashKind, Level, Namespace, Principal,
    PrincipalKind, PublicKey, Repo, Scope, UnixMillis, Visibility, WebCredential,
    ANONYMOUS_PRINCIPAL_ID, DEFAULT_BRANCH,
};
pub use observe::{
    InFlightGuard, MetricsSnapshot, RequestId, ServerMetrics, TARGETS, TARGET_AUDIT, TARGET_GIT,
    TARGET_METRICS, TARGET_POLICY, TARGET_SSH, TARGET_STORE,
};
pub use policy_store::{PolicyStore, POLICY_REF};
pub use provenance::{BuildRecord, SignedBuild, Subject, PROVENANCE_NAMESPACE};
pub use pulls::{PullRecord, PullState, Pulls, Transition as PullTransition, PULLS_REF};
pub use sandbox::{resolve_repo_path, PathReject, RepoRef};
pub use seal::{
    is_sealed, seal, unseal, Kek, PlaintextPolicy, SealedSecretStore, KEK_ENV, KEK_FILE, SEAL_MAGIC,
};
pub use secrets::{legacy_secret_log, MigratingSecretStore, RedbSecretStore, LEGACY_SECRET_DIRS};
pub use sshsig::EventProblem;
pub use transparency::{SignedTreeHead, TreeHead, STH_NAMESPACE};
pub use webhook::{
    sign_delivery, verify_delivery, DeliveryProblem, SignedDelivery, WEBHOOK_NAMESPACE,
};

// Re-exported so downstream crates can name these types without pinning the
// exact versions gunnar resolved.
pub use gunnar_store;
pub use ssh_key;