Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-ssh/Cargo.toml

[package]
name = "gunnar-ssh"
description = "gunnar's SSH server: a russh listener with publickey identity, a strict git exec dispatch, and the repository path sandbox."
readme = "README.md"
# 2.0.0 (2026-10-26, lane GUN-INSTALL / G-BOTH, the R5C publish): master differs from crates.io, so
# it gets its own version; a major: cargo-semver-checks names breaking changes against crates.io 1.5.0.
version = "3.0.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
# On the `cargo install gunnar` chain (owner order 2026-10-22).
publish = true
authors.workspace = true
repository.workspace = true

[features]
# **The metered plan gate.** `plan.rs` and the gate CHAIN in
# `Dispatch::authorise_service` are ALWAYS compiled now (2026-09-04): the
# community binary carries one link, `headroom::DiskHeadroomGate` — the
# "free lunch": checking disk has size before a new repo is created. This
# feature adds nothing to this crate any more; it is kept so
# `gunnar-server/metered` keeps a name to enable, and so the metered-only
# integration test (`gunnar-server/tests/entitlement.rs`) still has a feature
# to require. The second link — the entitlement gate — lives in `gunnar-server`.
metered = []
# **Usage-share admission** (`edda_admit::UsageShare`). With it, the gate asks
# `Admit::plan` for every keyed transfer: a namespace whose rolling-window bytes
# (the flow ledger's in + out) are over its budget is held to the ANONYMOUS
# ceiling instead of the per-principal share, and refused fast with its own
# reason — never queued. Without it the gate has no policy field and no call:
# the admission decision is today's, byte for byte. The budget itself is a
# setting (`--admit-limit`, `none` = PassThrough), which exists only in a build
# that has this feature.
admit-share = []

[dependencies]
# **The sandbox, the fingerprint, the host key and the auth lookup, ONCE**
# This crate grew its own copy of all four while `gunnar-core` was being
# written in parallel, and the two path sandboxes disagreed: this one demanded
# exactly two components, that one permits nested namespaces. Two security
# checks that disagree are worse than either alone. `src/path.rs`,
# `src/hostkey.rs` and `access::KeyFingerprint` are deleted; everything routes
# to `gunnar-core`.
gunnar-core = { workspace = true }
# The `gunnar-user-key` door's line format (CRYPT-YUBI): format only, no cipher.
gunnar-crypt = { workspace = true }
# The admission trait and its two policies. Always linked, so `Usage.AdmitPlan`
# can answer on every build; only `admit-share` puts it on the admission path.
# A sibling path, not a git pin (the offline Build Thing container); the reason is at
# the root Cargo.toml where the workspace line used to be.
edda-admit = { version = "0.1.0" }
# The `SecretStore` the host key lives in (the secret-store seam). Finished; depended on
# directly.
gunnar-store = { workspace = true }
# pkt-line framing, used for exactly one thing here: the `ERR <msg>` packet that
# turns "you may not have this repository" into a message a stock `git` prints,
# instead of an opaque channel failure. Zero dependencies of its own.
gunnar-pktline = { workspace = true }
# LFS over SSH. Taken for ONE type: `gunnar_lfs::Operation`, and the verb constant that
# names it. `git-lfs-transfer <path> <operation>` means the exec parser has to
# know which operations exist and this crate must not carry a second list of
# them (one implementation): a `download` the dispatch admitted and the protocol did not
# would be a session gunnar opens and cannot serve. `gunnar-lfs` is a leaf: it
# pulls `gunnar-pktline` (already here) and `gix-hash`, and nothing else.
gunnar-lfs = { workspace = true }

russh = { workspace = true }
# ⚠ NO `ssh-key` DEPENDENCY, DELIBERATELY. See the note in `src/lib.rs`:
# russh 0.61.2 pins `ssh-key = "=0.7.0-rc.10"`, and the workspace pins
# `ssh-key = "0.6"`. Those are two different, semver-incompatible crates. Every
# key type on russh's server API is the 0.7-rc one, so this crate uses
# `russh::keys::ssh_key` and never names the workspace dependency. Taking it
# would link a second copy that could not be passed to a single russh function.
#
# `gunnar-core` above linked ssh-key 0.6 until the RustCrypto bump (2026-10-03),
# so both copies were in this crate's graph. That is why the shared fingerprint
# is defined over BYTES (`gunnar_core::fingerprint_of_blob`): a russh key becomes
# a blob with `to_bytes()`, and the blob crosses any version boundary. Since the
# bump the workspace pins the same =0.7.0-rc.11 russh 0.63 rides, so there is one
# copy; the rule stands, so the next russh that moves its pin is still a blob.
#
# The rand_core 0.6 -> 0.10 bridge (`src/rng.rs`) went with the same bump, as its
# own note asked: keys are minted from `gunnar_core::os_rng`.

tokio = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
# `fs` for ONE call: `statvfs` on the store root, in `headroom.rs`. It is the
# one place this crate looks at a disk, and it reads no path — it asks the
# filesystem how much room is left, which is what `DiskHeadroomGate` refuses a
# push or a `Repos.Create` on. `tests/accept_emfile.rs` also uses rustix (for
# RLIMIT_NOFILE), which is why it used to be dev-only.
rustix = { workspace = true, features = ["fs"] }

[dev-dependencies]
# "The teardown failure is logged now" is a claim until a subscriber
# has caught one. Dev-only, so it cannot reach the appliance graph.
tracing-subscriber = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
tempfile = { workspace = true }
# Real wraps for the `gunnar-user-key` door's tests (CRYPT-YUBI).
gunnar-crypt = { workspace = true, features = ["user-key"] }

[lints.rust]
unsafe_code = "forbid"

# NO rayon (the multicore rule is gatling, never rayon). No subprocess: no
# `ssh-keygen`, no `sshd`, no `git`. This
# crate is in the `S` (server) set, so it must still work with no filesystem, no
# shell and no user database, as PID 1. Nothing here READS a
# path on disk (see `src/path.rs`); the one filesystem call is `statvfs` in
# `src/headroom.rs`, which asks the store root how much room is left.