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

nordisk/gunnar

crates/gunnar-server/Cargo.toml

[package]
name = "gunnar-server"
# 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: its crates.io 1.1.0 cannot be built as a semver baseline (its build.rs takes gunnar-recipe 0.1.0, the E0063), and it asks the 2.0 gunnar-common, gunnar-core, gunnar-grpc, gunnar-proto and gunnar-ssh.
version = "3.0.0"
edition = "2021"
rust-version = "1.85"
license.workspace = true
# On the `cargo install gunnar` chain (owner order 2026-10-22).
publish = true
authors.workspace = true
repository.workspace = true
description = "The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate."
readme = "README.md"

[lib]
name = "gunnar_server"
path = "src/lib.rs"

# The appliance binary. `build.rs` writes the build-thing receipt for the
# `gunnar-server` cell (`crates/gunnar-build-thing`), and `src/main.rs`
# `include!`s it back.
[[bin]]
name = "gunnar-server"
path = "src/main.rs"

[features]
# **znippy unless told otherwise.** `src/storage.rs` refuses to compile with
# neither engine, so this default is what makes `cargo build -p gunnar-server`
# and `cargo test --workspace` work at all.
# AIRGAP (owner rule 2026-10-25): no DHCP client and no WireGuard/pairing client
# by default; every appliance row names `dhcp-client` and `wg-client` itself.
default = ["server", "engine-znippy", "mimalloc"]
# The marker every server variant names, present so the build-thing
# `gunnar-server` cell has a non-empty feature set — receipt attribution keys on
# the RESOLVED feature set, and the empty set is deliberately not a cell (see
# `gunnar-build-thing/src/cells.rs`). It carried company SSO from 2026-10-22 to
# A34-15 (2026-10-27); SSO is its own feature now, `sso` below.
server = []
# **Company SSO's protocols** (OIDC + SAML login, edda nornir-identity-sso's `oidc`
# + `saml` through `gunnar-grpc/sso`; A34-15). OFF by default: without it the Sso
# service's protocol verbs answer "not compiled into this build" and no JWT or
# SAML verifier is linked. `sso-egress` (every appliance row names it) brings it.
sso = ["gunnar-grpc/sso"]
# **The appliance proof rows reach the matrix.** Passthrough to
# nornir-testmatrix's own emitter feature, so `crate::functional_status` stops
# being a no-op and the ceremony's verdict lands as a
# `component = "gunnar-appliance"` row. OFF by default: a release appliance
# does not link the matrix. Mirrors holger `server/cli/Cargo.toml:150`.
testmatrix = ["dep:nornir-testmatrix", "nornir-testmatrix/testmatrix"]
# **The runtime data-set growth's ENGINE** (`src/grow`, DATA-SET-GROWTH-FLOW.md
# §3, ruling 1): links edda's `korp-installer` — the SAME `grow` module
# `korp-start` grows the set with at boot, called from inside the running
# server (drain → unmount → wait for larger members → G1–G6 → mount → reopen).
# In the appliance variants since 2026-09-15: korp-installer 0.1.4 (0.1.5 since, adding the plain one-member G2; released
# through `dwarves crate-release` from edda `data-set-grow`) carries
# `grow::runtime`, so the container build resolves it from the registry like
# any other dependency. Without the feature the whole phase machine, the
# go-ahead, the identity snapshot and the drain still compile and are
# unit-tested over a scripted world; `Admin.GrowStatus` answers
# `engine_present: false` and `GrowStart` is refused by name.
# ★ **and `replication`**, since 2026-09-19 (`2164c0e`, "the drain never told the
# twin"). The drain TELLS the standby before it unmounts, so `src/grow/world.rs`
# names `crate::mirror::ReplicationSlot` unconditionally. Without this edge
# `--features grow-runtime` alone did not compile at all (E0433 `could not find
# mirror in the crate root`, three errors) — a feature combination nothing in the
# variant table builds, and therefore one nobody saw break. A feature that needs
# another says so here rather than in a compile error.
grow-runtime = ["dep:korp-installer", "dep:nix", "replication"]
# **Live update** (owner 2026-10-12, `src/live_update.rs`) — feature `live-app`, never a name
# holding `date`: the build-thing receipt's audit reads such a key as a timestamp (measured on
# oden: "settings[feature.live-update] … is a timestamp"). korp-installer's `live` (the slot,
# the checks, the re-exec) behind the installer's log door (nordisk-pairing `live`).
live-app = ["dep:korp-installer", "dep:nix", "wg-client", "nix/signal"]

# ── the two capabilities a customer may say NO to ───────────────────────────
#
# An appliance customer who says "no DHCP" or "no WireGuard" is not buying a
# binary that declines to run that code — they are buying one that does not
# CONTAIN it. Both are therefore cargo features, not runtime flags: off means
# the module never enters codegen, which is a property of the artifact that a
# symbol table can be read back for (`nornir_build_thing::options::OptionAudit`)
# rather than a promise about behaviour that an `if` could break.
#
# **Both are in `default` above**, because both are OPT-OUTS: a build that says
# nothing ships the capability. And both are named again in the
# `gunnar-server` row of `.nornir/build-variants.toml`, which sets
# `no_default_features = true` — inheriting them there is impossible, so the
# row that packs the appliance has to spell them out or the packed binary
# silently loses the capability the default promises.
#
# `dhcp-client` gates edda's `appliance_pid1::dhcp` (the client, `dhcp::renew`, the
# resolver file) and the DHCP arm of `appliance_pid1::net::bring_up_network`. `socket2` (the one `SO_BINDTODEVICE` rustix has no helper
# for) is used there and, since 2026-09-05, by the control plane's listener too
# (`serve::bind_grpc_listener`, an interface spelling binds the device), so it is a
# plain dependency now, not a `dhcp-client` rider — as a rider, a build without the
# feature failed to compile the device bind (measured on installtest).
# The `:dhcp` spelling of `GUNNAR_NET` is still PARSED with the feature off,
# and REFUSED by name: an appliance told to DHCP by a binary that cannot must
# stop, not boot dark. See `appliance_pid1::net::NetConfig::parse`.
dhcp-client = ["appliance-pid1/dhcp"]
# `wg-client` gates `src/wg.rs` (the tunnel) and `src/appliance.rs` (the
# identity ceremony, which is `wg_appliance_core::ceremony` and exists only to
# serve the tunnel), plus the `gunnar wg` keygen verb. `wg-appliance-core` is
# their ONLY dependent in this crate, so the dep is optional and rides the
# feature — off, boringtun and its crypto tail (x25519-dalek,
# chacha20poly1305, blake2, ring) leave the graph entirely, which is the whole
# reason a customer ticks the box off.
wg-client = ["nordisk-pairing/wireguard", "dep:wg-appliance-core", "dep:gunnar-pairing-door"]
# The container ceremony robot's test target (`tests/appliance_container_robot.rs`).
# Test-only: no shipped row turns it on. It turns on edda's `robot` feature of
# wg-appliance-core (`wg_appliance_core::robot`, published in 0.1.7; L14).
container-robot = ["wg-client", "wg-appliance-core/robot"]
# **DEV-NARRATE** (Skidbladnir nordisk-narrate, owner 2026-09-30): this box's
# boot log — the ESP's BOOTLOG.TXT (the install phase, korp-start, this boot),
# the kernel's records, every line after — SEALED to the installer's key from
# the seed and streamed over udp/51822 from the moment the NIC has an address,
# BEFORE the installer's WireGuard link and mutual TLS exist. OFF in every
# release row: the recipe's `dev-narrate` option (default off) names
# `nordisk_narrate` a FORBIDDEN symbol when off, and only the kit's dev medium
# (`--dev-narrate`, mock + test instances; refused for the original estate)
# composes it on. Rides `wg-client`: its source is the installer's BOOT_LOG.
dev-narrate = ["wg-client", "dep:nordisk-narrate", "nordisk-narrate/dev-narrate", "nordisk-narrate/bootlog"]

# ── the storage engines ─────────────────────────────────────────────────────
#
# Two implementations of ONE contract (`git-storage-trait`'s `GitOps`, the
# eleven). Cargo features are additive and must be, so BOTH may be on at once:
#
#   --features engine-znippy,engine-gix   both linked, picked at RUNTIME by
#                                         `GUNNAR_ENGINE`, defaulting to znippy
#   (default)                             znippy only, nothing to select
#   --no-default-features \
#     --features server,engine-gix        gix only, nothing to select
#
# Asking for an engine that was not compiled in fails loudly at startup and
# never falls back — `src/storage.rs` keeps that by construction (the `Engine`
# variants are themselves cfg-gated), not by a runtime check.
#
# The appliance keeps its diet: the musl/scratch image takes `engine-znippy`
# alone, so gix never enters the binary that is meant to be PID 1 with no OS
# under it.
#
# `default-features = false` on `znippy-plugin-git` is load-bearing and not
# tidiness. Its default turns on `znippy-common/openzl`, whose `openzl-sys-rs`
# build script shells out to `curl` for a pinned facebook/openzl tarball and
# then cmake-builds C and C++ — during `cargo build`, on every machine, with no
# network in the appliance pipeline. The dependency budget's rule is that none of that is in the
# build. It costs nothing here: `archive_write.rs` stores a pushed pack
# **verbatim** (`compressed: false`, "no compression, no framing"), so the git
# store never reaches the codec. znippy-common's own manifest states that with
# the feature off "openzl-sys-rs leaves the graph entirely".
#
# the old znippy engine crate's manifest warns that opting out on one edge changed
# nothing — that was measured with THREE edges into znippy (znippy-common,
# znippy-compress and znippy-plugin-git at once) and cargo unions features
# across every edge. There is exactly one edge here, which is why it binds.
engine-znippy = ["dep:znippy-plugin-git"]
engine-gix = ["dep:storage-git-gix"]
# Replication to a second gunnar, and **it is on the appliance**.
#
# The push half is `gunnar-client`'s send-pack over its russh transport; the
# READ side is the storage engine (`GitOps::refs` / `reachable` /
# `GitServe::emit_pack`), not `gix::open` — see `mirror.rs` for the measurement
# that ended that. `nornir-git` parses the standby's ssh url and dials the
# channel, and `gunnar-pktline` writes the one flush-pkt a "nothing to do"
# replication owes the remote.
#
# **`default-features = false` on `gunnar-client` is the whole feature.** This
# was `client`, and turning it on cost the appliance +128 packages — gix, ~57
# `gix-*`, and the reqwest/rustls/icu tail under `nornir-git`'s gix — MEASURED
# 2026-08-30 (365 -> 493) for a `gix::Repository` this module has not opened
# since 2026-08-30. gunnar-client 1.1.4 and nornir-git 0.1.9 each split their own
# porcelain behind a feature, so the wire half — send-pack, the ssh dialler, the
# ref plan — resolves on its own. What it adds is measured in
# the replication drill.
#
# `dep:gix` is GONE from this crate: `grep -rn '\bgix::' crates/gunnar-server/src`
# is empty and has been since 2026-08-30 replaced `gix::open` with
# `ServerStores::repo_store`. `gix-hash` (below, not optional) is the object-id
# vocabulary and is a different thing.
replication = ["dep:gunnar-client", "dep:nornir-git", "dep:gunnar-pktline", "dep:tonic", "dep:gunnar-repair"]
# Outbound webhooks: deliver events to an external URL, signed. OFF by default (a
# webhook is an opt-in, and the outbound hyper client + rustls tail is not on
# the appliance unless a customer asks for it). Turns on hyper's client half
# (already the crate resolved for the gRPC/scrape server halves, so this adds no
# package for http://) and, for https://, tokio-rustls over the OS trust store.
webhooks = ["hyper/client", "hyper/http1", "dep:tokio-rustls", "dep:rustls-native-certs"]
# The DEVELOPER binary's shape: replication plus the porcelain halves back on,
# so `gunnar-cli`'s `client` feature — which asks for `gunnar-server?/client` —
# resolves one gix for the whole build instead of two feature sets of it.
client = ["replication", "gunnar-client/porcelain", "nornir-git/porcelain"]
# Smart HTTP, the second git transport. OFF by default.
http = ["dep:gunnar-http", "gunnar-common/http"]
# **The allocator, and it is a measured 2.2x on clone CPU** — see `src/lib.rs`
# for the four-way grid. ON by default because the appliance's whole job is to
# serve clones, and glibc malloc is the single largest term in that path once
# the resolver stopped leaking its arena.
#
# This is the one place the crate takes C into the build on purpose, so the
# accounting is written down rather than assumed. The dependency budget's rule bans a *build*
# that needs a network fetch, cmake, C++ and `libstdc++` — that is the openzl
# story that `default-features = false` on `znippy-plugin-git` keeps out, four
# paragraphs up. `libmimalloc-sys` has none of the four: one `cc`
# build-dependency, C sources vendored in the crate, no cmake, no download, no
# C++ translation unit. It builds offline on musl. It is also not a new taste
# in this tree — `nornir` already pins `mimalloc = "0.1"` as its global
# allocator for the same reason (its comment: glibc "only ~2x from 32 workers;
# mimalloc scales per-thread").
mimalloc = ["dep:mimalloc"]
# estate-clock's HTTPS Date sources (ureq). OFF by default (AIRGAP, owner rule 2026-10-25):
# without it the clock asks peer and SNTP sources only and refuses an https:// source by
# name (`clock::NO_HTTPS`). Every appliance row names it, so a live box keeps its sources.
clock-fetch = ["estate-clock/fetch"]
# The boot's resolver proof (`boot::RESOLVER_PROBE`): after each DHCP lease, a DNS question
# for github.com. OFF by default (AIRGAP B3); every appliance row names it.
resolver-probe = []
# The pull channel to the vendor's monetize (`repair::pull_loop`: reports and signed orders).
# OFF by default (AIRGAP B5): a named or fed channel is refused by name. Estate and metered
# builds only: every gunnar-server row names it; gunnar-solo and gunnar-service do not.
#
# It is also what links the SHARED box pump (gunnar-repair `fleet` = monetize-estate-side
# `pump`, monetize-embed and monetize-proto; A28 M12): the community build, which has
# `replication` but not this, links no monetize crate (`gunnar-cli/tests/feature_split.rs`).
# And the resend sign-in mail (`postman`, lettre's SMTP): only a pulled `sign_in_again` order
# sends it, so a build that pulls no order carries no SMTP client either.
fleet-pull = ["replication", "gunnar-repair/fleet", "dep:lettre"]
# **The mirror pull's `https://` half** (A32-18): `Mirror.Sync` / `Mirror.Import` from a
# forge's https URL over ureq. OFF by default (the airgap install); every appliance row
# names it. Without it an https remote is refused by name and `ssh://` (replication) mirrors.
mirror-http = ["dep:ureq", "dep:webpki-root-certs"]
# **Gunnar as a Heimdall user** (Heimdall milestone 5): a fetched (mirror import)
# or pushed pack is parsed by `gunnar-pack-check` in a Heimdall sandbox before the ingest
# reads it (`crate::heimdall_gate`). OFF by default (Gunnar slim): without it the
# ingest is unchanged. With it, `GUNNAR_HEIMDALL_CHECKER` (the static checker)
# switches the gate on; a gate that cannot start refuses every fetched or pushed pack.
heimdall = ["dep:gunnar-heimdall", "dep:heimdall-container", "dep:heimdall-core"]
# **Company SSO's way out** (A32-18): the guarded ureq egress to an account's identity
# provider (`sso_egress`). OFF by default; every appliance row names it. Without it the
# SSO screens stay, and an IdP call is refused by name (no egress is set).
sso-egress = ["server", "sso", "dep:ureq", "dep:webpki-root-certs"]

# ── the capability a SELF-HOSTER does not get, and a SERVICE PROVIDER pays for ──
#
# `metered` is a cargo feature for the same reason `wg-client` is: the
# community appliance does not DECLINE to enforce plans, it does not CONTAIN
# the code (the metered build split). Off means `src/entitlement.rs` never
# enters codegen, `gunnar-proto` never compiles `entitlement.proto`,
# `gunnar-grpc` mounts no `Entitlement` service, `gunnar-ssh`'s dispatch has
# no plan arm and `gunnar-core` has no entitlement document — and
# `monetize-embed` is not linked at all. The symbol table of the packed
# binary is what proves it (`OptionAudit`; `gunnar-cli/tests/feature_split.rs`
# asserts the resolved graph both ways).
#
# NOT in `default`: the community build is the default. The build-thing rows
# `gunnar-server-metered` / `gunnar-server-metered-thin`
# (`crates/gunnar-build-thing/src/variants.rs`) are what turn it on.
#
# What it links: `monetize-embed`, the verified entitlement cache (arc-swap +
# ed25519-dalek, both already in the graph under russh and ssh-key). The
# canonical form a fact is signed over is THAT crate's
# (`monetize_embed::signing::fact_message`); gunnar writes no second one.
# **The usage share** (edda_admit::UsageShare) on the admission gate, and its
# two serve flags. Off by default: without it the gate is today's, byte for
# byte (see gunnar-ssh's `admit-share`).
admit-share = ["gunnar-ssh/admit-share", "gunnar-common/admit-share"]
metered = [
    "dep:monetize-embed",
    "gunnar-core/metered",
    "gunnar-common/metered",
    "gunnar-grpc/metered",
    "gunnar-ssh/metered",
]


[dependencies]
# The pushed pack over `SPOOL_ABOVE` is spooled to a temp file and mapped, not held.
memmap2 = "0.9"
tempfile.workspace = true
# **`Mirror.Sync`'s smart-HTTP half** (`src/mirror_http.rs`) and the company-SSO
# egress (`src/sso_egress.rs`). OPTIONAL since A32-18 (owner rule 7, 2026-10-25: the
# default build is the airgap install and makes no outbound call): `mirror-http` and
# `sso-egress` turn them on, and every appliance row names both (an appliance that can
# mirror from `ssh://` and not from `https://` can mirror from almost nothing, because
# `https://` is the URL every forge prints on its clone button). Without them an
# `https://` mirror and an identity provider's call are refused by name.
#
# Blocking, not async, and that is what it is for: the adapter is called from
# the job queue's blocking worker inside a `BufRead::fill_buf`, and reaching the
# async `hyper` that is already in this graph from there is a `block_on` inside
# a blocking read — the deadlock `src/transport.rs` documents from the other
# direction.
#
# `default-features = false, features = ["tls"]` is rustls and no curl, which
# is what keeps `grep -rn curl` empty across this constellation. The same line
# `gunnar-forge` resolved, so the packages are shared and not doubled:
# MEASURED 2026-09-08, `cargo tree -p gunnar-server -e normal --prefix none`,
# 323 -> 327 — `ureq`, `ureq-proto` and two `webpki-roots` majors.
ureq = { version = "3", default-features = false, features = ["rustls"], optional = true }
# The public roots AS CERTIFICATES, so a call may trust them AND an account's
# own IdP CA (`sso_egress::UreqEgress::agent_trusting`); ureq's own `WebPki`
# root set cannot be extended. Already resolved in this graph: no package.
webpki-root-certs = { version = "1", optional = true }
# The wall clock (`src/clock.rs`): the rules live in edda estate-clock, which
# the front (gunnar-front) links too — one implementation for both machines.
estate-clock = { workspace = true, features = ["peer", "adjust"] }
# The global allocator. Optional so `--no-default-features` still gives a
# C-free build, which is the honest escape hatch for anyone who reads the dependency budget more
# strictly than the paragraph above does.
mimalloc = { version = "0.1", optional = true }
# The shared command line, logging ladder and build identity.
# `jobs` (A32-14): the job queue (gc, purge, mirror) runs on jera lanes; nothing else in the server
# links jera.
gunnar-common = { workspace = true, default-features = false, features = ["server", "jobs"] }
jera = { workspace = true }
# For the binary's `Cli::parse()`. `env` so `--data-root` reads `GUNNAR_DATA_ROOT`.
clap = { workspace = true, features = ["env"] }

# ── THE storage contract ────────────────────────────────────────────────────
#
# It lives in znippy beside its main implementation; edda's gix arm depends on
# znippy for this and for nothing else.
#
# **THE DURABLE FIX LANDED, 2026-09-09.** This entry used to be a `path` into a
# sibling znippy checkout, and it said so at length: cargo reads a path
# dependency's manifest at RESOLVE time whether or not a feature selects it, so
# the line made a sibling checkout a hard requirement of every build of every
# crate in this workspace. MEASURED 2026-08-10 on the 4-core laptop, by pointing
# the path at a directory that does not exist:
#
#     $ cargo metadata --format-version 1
#     error: failed to load manifest for workspace member
#            `~/git/gunnar/crates/gunnar-server`
#            referenced by workspace at `~/git/gunnar/Cargo.toml`
#     Caused by: failed to load manifest for dependency `znippy-plugin-git`
#     Caused by: failed to read `~/git/znippy-DOES-NOT-EXIST/...`
#
# That is the whole workspace, not this crate. The note ended: "The durable fix is
# publication, not detachment: the day `git-storage-trait` and `znippy-plugin-git`
# are on crates.io these become `version` requirements and the resolve-time edge
# disappears." Both are on crates.io (0.1.2 and 0.1.2, published deps-first
# through `dwarves crate-release` after `znippy-common 0.9.15`), and these four
# lines are what that sentence turns into. The count of mandatory sibling
# checkouts went from three to ZERO — the same `cargo metadata` above now succeeds
# with no sibling on disk at all.
#
# `GitOps`, the eleven. 278 lines, `anyhow` and nothing else — deliberately tiny
# so neither implementer drags the other's backend into its graph.
git-storage-trait = { version = "0.1.2" }
# The Arrow-IPC implementation. See the `engine-znippy` feature for why
# `default-features = false`.
# 0.1.5 (2026-09-21) is the floor and not a courtesy bump: it is the version that has
# `set_explode_default`, `set_explode_budget_default`, `set_resolve_workers_default` and
# `set_resolve_lookahead_default` beside 0.1.3's `set_graph_cache_default`. Without all five
# `Engine::hold_twin_memory_bound` cannot compile, and the twin's memory bound goes back to
# being five tokens on a kernel command line — which is a per-box parameter, which the
# one-medium ruling forbids.
# 0.1.6 — published on crates.io (2026-09-28); Cargo.lock names it (release-2:
# `cargo update -p znippy-plugin-git --precise 0.1.6`). It is the version with the
# exploded cache's 80 % disk line (the oldest lazy segments go when the filesystem
# holding a store is >= 80 % full), the push-side kick of that maintenance, and the
# eviction-race fixes (znippy branch cache-evict-at-80: cb91646, 1120408, 38e381c,
# version bump 6a05714).
# =0.1.8 (2026-10-16): znippy master with the 80 % disk line AND the per-repository
# object tail on redb 4 (a small repository: 1.65 MB -> tens of KB of disk; an old
# redb 2 tail is migrated once, on open). It replaces vendor/znippy-plugin-git, which
# carried the redb-4 tail on top of 0.1.6 from 2026-10-06; crates.io 0.1.7 had
# neither change.
# 0.2.0 (2026-10-18): the znippy 0.10 / plugins 0.2 line (HandlerSpec states its claim);
# a caret requirement again, so the lock holds the one znippy-common 0.10 everyone shares.
znippy-plugin-git = { version = "0.2.2", default-features = false, optional = true }
# **Every disk on the box**, for `Admin.Capacity { disks: true }` (2026-09-28, the
# console's admin disk view). edda's product-neutral `disk-snapshot`, written for
# holger's admin view and adopted here instead of a second mount-table walker:
# /proc/self/mounts, one statvfs(3) per storage mount, /sys/block with md members.
# Pure Rust over rustix (already in this graph). A GIT pin, not a registry version,
# ONLY because edda has not published 0.1.0 yet (branch `admin-view`); `cargo deny`
# allows git sources. The day edda publishes it this line becomes `version = "0.1"`
# and nothing else changes — the same road `nornir-git` took.
# release-2: a PATH onto the sibling edda (crates/disk-snapshot), not a git pin, for
# the reason edda-admit's workspace line gives (385adc3): the Build Thing container is
# offline and mounts no CARGO_HOME/git. The sibling edda is edda release-2 (main
# 114fabb + admin-view fc0714b + admit 521270d), so the path resolves from edda main.
disk-snapshot = { version = "0.1.0" }
# The gix implementation.
storage-git-gix = { version = "0.3.0", optional = true }
# **The metered appliance's entitlement cache** (`metered` only). This was the
# WORST of the three sibling paths and it is worth keeping the reason: it is
# OPTIONAL, `metered` is off by default, and the path was still read at resolve
# time — so a workspace without `../monetize` beside it did not resolve even for a
# build that would never compile a line of it. Now a version, from a crate whose
# whole workspace is otherwise `publish = false`.
monetize-embed = { version = "0.3.0", optional = true }

gunnar-core = { workspace = true }
gunnar-store = { workspace = true }
gunnar-ssh = { workspace = true }
gunnar-wire = { workspace = true }
gunnar-heimdall = { path = "../gunnar-heimdall", version = "1.6.0", optional = true }
heimdall-container = { package = "nordisk-heimdall-container", version = "0.2.1", optional = true }
heimdall-core = { package = "nordisk-heimdall-core", version = "0.2", optional = true }
gunnar-lfs = { workspace = true, features = ["fs"] }
gunnar-policy = { workspace = true }
gunnar-proto = { workspace = true }
gunnar-grpc = { workspace = true }
# The ONE implementation of the repair verbs and the pull channel to monetize
# (5.4): the boot check and the order executor (`repair.rs`), with replication.
gunnar-repair = { workspace = true, optional = true, features = ["proto"] }
# The identity core (IDENTITY-HOST): built at the start over Gunnar's host (`identity.rs`).
nornir-identity = { version = "0.2.0" }
# S10: a sold estate's main mails its own resend sign-in link (`postman`). The console's
# lettre (gunnar-ui-server `auth/mail.rs`), the same rustls feature set: no native-tls, no
# OpenSSL, no pool.
lettre = { version = "0.11", default-features = false, optional = true, features = ["builder", "smtp-transport", "tokio1", "tokio1-rustls-tls", "ring", "webpki-roots"] }
gunnar-http = { workspace = true, optional = true }
# `default-features = false`: the WIRE half only (send-pack + the ssh dialler +
# the ref plan). `client` above adds `gunnar-client/porcelain` back for the
# developer binary. See the `replication` feature for the package count.
gunnar-client = { workspace = true, optional = true, default-features = false }
# The PublicOnly replica's forget path (the browse replica) dials the
# replica's control plane itself — `Repos.Delete` over gRPC — so the send-pack
# half is not the only client half any more. Optional with `replication`; a
# build without it never dials anything.
# `tls-webpki-roots`: a fed Fleet door (the ONE monetize, https, no CA file) is
# checked against the public roots (`repair::fleet_channel`).
tonic = { workspace = true, optional = true, features = ["tls-webpki-roots"] }
# The two leaves the replicator says by name, both already inside
# `gunnar-client`'s graph: the ssh url parser and the pkt-line writer.
# `nornir-git`'s own `default-features = false` is set in the workspace table.
nornir-git = { workspace = true, optional = true }
gunnar-pktline = { workspace = true, optional = true }

# ── the gix RESIDUE, and exactly what it is ─────────────────────────────────
#
# **Two crates, both value types and one object decoder, and no pack pipeline.**
# This block was five entries on 2026-08-10 — `gix-hash`, `gix-object`,
# `gix-pack`, `gix-actor` and the optional `gix` — of which only the last was
# optional. `gix-pack` was therefore in EVERY build including
# `--no-default-features --features server,engine-znippy`, the appliance's, and
# it brought `gix-diff`, `gix-traverse`, `gix-revwalk`, `gix-commitgraph`,
# `gix-fs`, `gix-tempfile`, `gix-path`, `gix-chunk`, `gix-zlib` and
# `gix-features/parallel` with it.
#
# Nothing called any of it. `gix-pack` was three tokens — `data::Version::V2`,
# `::V3` and one struct field — populating a receipt for a push the *engine*
# had already stored through `GitOps::put_pack`; `gix-actor` was the parameter
# type of one private four-line function. Both are gone. The pack vocabulary is
# `gunnar_wire::pack` now (see that module's header), and the version enum is
# two variants of gunnar's own.
#
# What is left, and why each one stays:
#
# * `gix-hash` — `ObjectId` and `Kind`. A 32-byte inline array and a
#   two-variant enum, no pipeline behind either. It is the object-id vocabulary
#   of nine crates in this workspace; the contract's `Oid<'a> = &'a [u8]` is a
#   *borrow* and cannot be what an owned id in a returned struct is spelled as.
#   Replacing it is a workspace-wide rename with no dependency payoff.
# `gix-object` left on 2026-08-10, and it took six crates with it —
# `gix-actor`, `gix-date`, `gix-error`, `gix-hashtable`, `gix-utils`,
# `gix-validate`, none of which this crate ever named. The argument for keeping
# it was sound and still is: the control plane's `log` and `peel` DECODE bytes
# that came out of `GitServe::read`, and parsing bytes you were handed is not
# reaching around the thing that handed them to you. What changed is the
# *price*. Seven crates for two grammars gunnar reads and never writes is not a
# price a decode has to cost, and `gunnar_store::object` reads them in one file
# with no dependency of its own. `gix-object` is still the oracle that file is
# tested against — in `gunnar-store`'s dev-dependencies, where the disagreement
# is worth something.
#
# `xtask::engine_seam` is the guard that keeps this list from growing back.
gix-hash = { workspace = true }
# 2026-10-02, gix 0.88: `gix-hash` 0.27 reports failures (`ObjectId::from_hex`,
# hashing) as `gix_error` exceptions, which are not `std::error::Error`, so they
# are converted to `gix_error::Error` before `anyhow` context is attached. No new
# crate: `gix-hash` 0.27 already depends on `gix-error`; this only names it.
gix-error = { workspace = true }
# **`gix` is not a dependency of this crate any more, and the absence is the
# point.** It was here for the `client`-gated `mirror` module's `gix::open`,
# which `1457afbc` deleted: the read half now goes through
# `ServerStores::repo_store` and the storage engine's `GitOps`/`GitServe`.
# MEASURED 2026-08-30: `grep -rn '\bgix::' crates/gunnar-server/src` is empty.
# Keeping the optional edge would have meant `replication` could not be honest
# about what it costs, because the name would still be reachable.

tokio = { workspace = true }
tokio-util = { workspace = true }
tokio-stream = { workspace = true }
# PID 1's syscalls: `waitpid`, the `AF_NETLINK` socket that brings the
# appliance's interface up, and (`fs`) the `mknod` that creates `/dev/tty0` so
# the startup banner can reach a KVM graphics console. See `src/pid1.rs`.
#
# The `system` feature is GONE with the kernel-WireGuard work it existed for:
# it was `finit_module(2)`, and nothing loads a kernel module here any more.
# The tunnel's data plane is compiled into this binary (`wg-appliance-core`
# below), so the appliance needs no `.ko` and no `modprobe`.
# `fs` for the repository layout; `thread` for the build sandbox's
# `unshare(CLONE_NEWUSER|CLONE_NEWNET)` (`crate::sandbox`); `termios` is one
# ioctl, `TIOCGWINSZ`, for the console dashboard's frame size.
#
# **Features, not crates.** The workspace's own note says why rustix was chosen
# over `libc` in the first place — on `x86_64-linux` its backend is `linux_raw`,
# raw syscalls written in Rust, so it cannot re-introduce the PT_INTERP that
# `containers/Containerfile.bin-musl` asserts is absent. Reaching for `libc` to
# get `unshare` would have undone that for one syscall rustix already spells.
# `time` is `clock_settime` and nothing else — the feature's dependency list in
# rustix 1.1.4 is literally `time = []`, so it costs no package. `src/clock.rs`
# steps `CLOCK_REALTIME` with it at boot; the alternative was `date(1)` or
# `ntpdate`, and there is no shell under this binary to run either.
rustix = { workspace = true, features = ["fs", "thread", "termios", "time", "mount", "system"] }
# **The appliance's stop request and power-off** (edda, shared with holger's
# PID 1): `runtime::shutdown_request` waits on SIGTERM/SIGINT/SIGQUIT AND the
# ACPI power button (a cloud's soft stop is a button press on the `acpi_event`
# generic-netlink group, never a signal), `power::power_off` is sync(2) +
# reboot(POWER_OFF). 0.1.2 is the first version with the button. libc +
# tokio(signal) only.
# release-3: a PATH onto the sibling edda with the version KEPT (disk-snapshot's form
# above): 0.1.2 is not on crates.io, and the Build Thing container is --offline.
# The `path` is deleted the day edda publishes appliance-pid1 0.1.2.
# 0.1.4 (lane L10, 2026-10-21): ALL of Gunnar's PID 1 — the netlink writer and the
# net spec (`net`), the DHCPv4 client, its renewal and the resolver file (`dhcp`, riding
# `dhcp-client`), the stop sources, the install record, the gfx console mirror and the
# panic as a fault. gunnar-server's own `pid1.rs` and `dhcp.rs` are deleted; what is
# Gunnar's (GUNNAR_NET, the host-name prefix, GN-99) is `src/boot.rs`.
# 0.1.5 (lane L10, 2026-10-21): additive. The netlink framing is public (the
# dashboard probe's own copy deleted), the resolver file is on `net` alone
# (`src/resolver.rs` deleted), a late DHCP lease is narrated and renewed on
# Gunnar's T1 seam (`boot::late_lease`), and a panic's crash record holds the
# narrator's memory (`fault::narration_from`).
appliance-pid1 = { version = "0.1.5", features = ["runtime", "net"] }
# **The ONE userspace WireGuard implementation in the suite** (edda), shared
# with tunnr's `wg-appliance` PID-1 payload: x25519 keys + keygen, the
# `wg-quick` INI tokenizer, the `/dev/net/tun` device and the boringtun packet
# pump (`src/wg.rs`). It brings boringtun and its crypto tail
# (x25519-dalek/chacha20poly1305/blake2/ring) into the appliance graph — the
# cost of a tunnel that does not depend on what the packed kernel was compiled
# with. MEASURED, in `.nornir/build-variants.toml`.
# 0.1.5 and the same edda source as gunnar-pairing-door: published, the two resolve to ONE
# registry crate, and two semver-compatible copies (0.1.4 + 0.1.5) cannot (2026-10-22).
wg-appliance-core = { version = "0.1.7", optional = true }
# **The pairing rules** (seed, one-time join, election, the state machine, the
# signal) — edda's shared crate, used by gunnar and holger alike (owner
# 2026-09-24). Path while it is unreleased; `dwarves crate-release` pins it.
# NOT optional since 0.5.15 (L10, 2026-10-20): its feed bus (`feedbus`) is the
# one `crate::feed` publishes on and every live taker subscribes to, in every
# build. Without `wg-client` (its `wireguard` feature) it adds only itself and
# `toml` to the bare `--no-default-features` graph; every shipped shape already
# linked it through `wg-client`.
# 0.5.26 (T16 / P4-REST): the box's seed reader is its `seed::metadata` (and `egress` was
# already 0.5.26's); a lone clone takes it once 0.5.26 is published.
nordisk-pairing = { version = "0.5.27" }
# DEV-NARRATE only (feature `dev-narrate`): absent from every release row.
nordisk-narrate = { version = "0.1.0", optional = true }
# The names this box shares with the FRONT medium (`gunnar-front`): the offer
# keys (`OFFER_VERSION`, the certify key the primary enrols as `front-certify`).
# The front ROLE is not here: vm1 boots its own medium (owner 2026-09-25).
gunnar-pairing-door = { path = "../gunnar-pairing-door", version = "1.6.0", optional = true }
# The grow engine (feature `grow-runtime`, see there). `default-features =
# false` is the dep-light installer core: gpt / mdraid / mdassemble / grow /
# datahome / xfs — no znippy, no OpenZL toolchain, no image codecs. The
# registry floor is 0.1.2 (what the bench links); `grow::runtime` shipped in
# 0.1.4, and 0.1.5 adds the plain one-member G2 (`grow::plain_member_g2`).
korp-installer = { version = "0.2.0", default-features = false, optional = true }
# `mount(2)` / `umount2(2)` for the read-only guard tmpfs the growth lays over
# the empty `/data` mountpoint while the set is unmounted (`src/grow/world.rs`).
# `korp-installer` already links this exact crate for the same calls.
nix = { version = "0.31", default-features = false, features = ["mount"], optional = true }
# `SO_BINDTODEVICE` for the DHCP client's socket (`src/dhcp.rs`) — the one
# option rustix has no helper for, and its ONLY user in this crate.
#
# Optional, riding `dhcp-client`. Note what that does and does not buy: tokio's
# `net` feature carries socket2 of its own, so the CRATE stays in the build
# graph either way. What the gate removes is gunnar's DHCP client — the 1522
# lines of RFC 2131 in `dhcp.rs` + `dhcp/renew.rs` and the raw AF_PACKET-ish
# socket legs they open — not a third-party crate's presence in `cargo tree`.
# The WireGuard gate below is the one that empties a graph; this one empties a
# symbol table, which is the thing the customer asked about.
socket2 = { workspace = true }

# The scrape endpoint (`src/scrape.rs`): hyper directly, no framework.
hyper = { version = "1", default-features = false, features = ["server", "http1"] }
hyper-util = { version = "0.1", default-features = false, features = ["server", "tokio"] }
http-body-util = { version = "0.1", default-features = false }
bytes = { version = "1", default-features = false, features = ["std"] }

# ── outbound webhooks (`--features webhooks`) ───────────────────────────────
# The client half rides the `hyper`/`hyper-util`/`http-body-util` already above
# (turned on by the `webhooks` feature adding `hyper/client`), so an http://
# webhook adds no package. https:// wants a TLS client: tokio-rustls over the
# `ring` provider gunnar already pins, verifying against the OS trust store via
# rustls-native-certs. Both optional and ride the feature — off, neither is
# linked, so the appliance's dependency diet is unchanged.
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"], optional = true }
rustls-native-certs = { version = "0.8", optional = true }

# **Where a pushed packfile ends.** `read_pushed_pack` used `std::io::copy`,
# which ends at EOF — and a `receive-pack` client does not send EOF after the
# pack, it holds the channel open waiting for the status report. That is a
# deadlock, and it hung `gunnar.real_repo_clone` for 452 seconds at 0% CPU on
# both sides (2026-08-11). A pack is self-delimiting only if you walk it, and
# walking it means ending each entry's zlib stream, which is what this is for.
# `git index-pack --stdin` pays exactly the same inflate for exactly the same
# reason. One crate: the workspace pins the `zlib-rs` backend, and
# `gunnar-http` already carries this edge, so the appliance's dependency diet
# gains a decoder it did not have and nothing else.
flate2.workspace = true

# **The layout config's integrity and authenticity** (`src/layout_config.rs`):
# blake3 over the config body, ed25519 over the domain-separated digest — the
# same two primitives `korp-installer` seals it with, arm for arm. NEITHER adds
# a package to the appliance: both were already in this crate's normal graph
# (blake3 under znippy, ed25519-dalek under ssh-key/russh), so `cargo tree -p
# gunnar-server -e normal` is unchanged by these two lines. Naming them
# directly is what lets the verifier be written here instead of being
# reimplemented from something that happens to re-export them.
blake3 = "1"
ed25519-dalek = "3"
anyhow.workspace = true
# The twin decodes the oids its frames carry as hex (`twin.rs`).
hex.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true

# **The appliance proof rows.** Optional and feature-gated, so a RELEASE
# appliance links none of it — `crate::functional_status` compiles to an
# `#[inline]` no-op with `nornir-testmatrix` absent from the graph entirely
# (the same shape holger's `server/cli` uses). A crates.io version, like every
# other edda leaf here since 2026-09-09 — it was already resolving from the
# registry before that (0.2.11), because the `path` beside it pointed at a crate
# whose published line the requirement already reached.
nornir-testmatrix = { version = "0.2", optional = true }

# ── the WireGuard tests name the feature they need ──────────────────────────
#
# Both reach `wg_appliance_core` directly, which with `wg-client` off is not in
# the graph at all. `required-features` makes a featureless `cargo test` SKIP
# these targets by name — "target requires features wg-client" — instead of
# failing to resolve an import, which reads like a broken tree rather than a
# deliberate diet. Same shape holger uses on its own conditional targets.
#
# The container robot also needs `wg_appliance_core::robot`, which edda keeps
# behind its own `robot` feature (a test tool, A28 L14). `container-robot` names
# the target and turns that feature on (published in wg-appliance-core 0.1.7, so a
# lone clone resolves it); run it with `--features container-robot -- --ignored`.
[[test]]
name = "appliance_container_robot"
required-features = ["container-robot"]

[[test]]
name = "appliance_matrix_rows"
required-features = ["wg-client"]

# The metered appliance's tests: `tests/entitlement.rs` drives `Entitlement.Set`
# and the plan arm of `authorise_service`, neither of which exists without the
# feature — so the target is skipped BY NAME on a community `cargo test`, the
# same way the WireGuard ones are.
[[test]]
name = "entitlement"
required-features = ["metered"]

# The actor-ticket signing key: minted only by the metered appliance, because
# only the metered appliance carries the service that spends it. Same shape as
# the target above — skipped BY NAME on a community `cargo test` rather than
# failing to resolve `ServerStores::actor_ticket_key`, which does not exist
# there.
[[test]]
name = "actor_ticket"
required-features = ["metered"]

# Members never buy (2026-10-07): `Billing.MintActorTicket` over the real
# plane — metered only, as the service is.
[[test]]
name = "members_never_buy"
required-features = ["metered"]

[dev-dependencies]
# **This crate, on itself, for its tests** (the shape gunnar-core uses for `conformance`):
# `wg-client` and `dhcp-client` left the cargo default (AIRGAP S4, 2026-10-25), and the
# bootstrap, twin, pairing and boot tests exercise them (`restore_keys` reads the keys fed
# through `pairing/joined.toml` only under `wg-client`). A dev-dependency, so no shipped
# build changes; `cargo test -p gunnar-server` runs every test it ran before S4.
gunnar-server = { path = ".", features = ["wg-client", "dhcp-client"] }
tempfile.workspace = true
# `tests/encryption_key_rpc.rs` makes real age wraps (an SSH key, a security key) for the
# encryption-key doors driven over the router. Already in the graph under gunnar-core.
gunnar-crypt = { path = "../gunnar-crypt", features = ["wrap"] }
# The txlog's test hold for audit_read_io (`gunnar_core::txlog::TEST_GROUP_ENV`):
# this crate's test builds only; a shipped server never reads it.
gunnar-core = { workspace = true, features = ["test-hooks"] }
# An https IdP behind its own CA, minted in the test (tests/sso_egress_private.rs).
# Both already resolve here (ureq's rustls, the workspace's rcgen): no package.
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rcgen = { workspace = true }
# Company SSO's end-to-end test (`tests/sso.rs`): the fake OpenID provider and
# the fake SAML IdP edda's leaves carry behind `test-support`, run in-process
# over real HTTP; `sha2` builds the passkey ceremonies' bytes. Dev-only.
nornir-auth-oidc = { version = "0.2.0", default-features = false, features = ["test-support"] }
nornir-auth-saml = { version = "0.1.2", default-features = false, features = ["test-support"] }
sha2 = "0.11"
# A real P-256 passkey per web-credential test (Stage L), the same p256 0.14
# gunnar-grpc verifies with.
p256 = { version = "0.14", default-features = false, features = ["ecdsa"] }
# The identity core's soft passkey and test relay (IDENTITY-HOST): a login in a test is the
# core's, over the real plane.
nornir-identity = { version = "0.2.0", features = ["test-support"] }
# `tests/capacity.rs` decodes a `CapacityResponse` into the message as an OLDER
# client compiled it, to prove the additive fields cost that client nothing.
prost.workspace = true
# The Heimdall gate's tests (tests/mirror_import.rs, feature `heimdall`): the
# real checker in-process behind a signing fake runner. Dev-only.
gunnar-heimdall = { path = "../gunnar-heimdall", features = ["checker"] }
heimdall-core = { package = "nordisk-heimdall-core", version = "0.2" }
# `tests/entitlement.rs` SIGNS facts the way monetize-server does — with a
# real Ed25519 key over `monetize_embed::signing::fact_message` — so nothing in
# that file is verified against a stand-in. Both are already in the graph
# (ed25519-dalek under ssh-key; monetize-embed under `metered`); a
# dev-dependency never reaches a shipped binary.
ed25519-dalek = "3"
monetize-embed = { version = "0.3.0" }
# **The `blame` and `search` unit tests build a commit graph they know the
# answer to.** They need `gunnar-store`'s `conformance` feature for
# `conformance::pack::PackBuilder` (which writes a real pack `MemGitServe` can
# absorb) and `gix-object` for the object `Kind` and `compute_hash` those
# helpers name. Both are dev-only: `gunnar-store` is already a normal dependency
# here, so this just turns the feature on for the test build, and `gix-object`
# never reaches a shipped binary through a dev-dependency (the whole point of
# this crate keeping it out of `[dependencies]`).
gunnar-store = { workspace = true, features = ["conformance"] }
gix-object.workspace = true
# `tests/orphan_store_recovery.rs` drives the gRPC `Repos` service directly —
# the composition it locks down spans the ACCESS CHECK (gunnar-grpc) and the
# DISK (this crate), so a test on either side alone cannot see it. It needs
# `tonic::Request` to attach a `Caller` with no interceptor in the way; the
# crate is already in the graph through `gunnar-grpc`, so this links nothing
# new into a shipped binary (a dev-dependency never reaches one).
tonic.workspace = true
# The console dashboard's integration test installs its own subscriber with the tap.
tracing-subscriber.workspace = true
# **NO `gunnar-build-thing` DEV-DEPENDENCY, AND THE ABSENCE IS LOAD-BEARING.**
#
# There was one, so the container robot could call `GunnarBuildThing.variants()`
# directly. It made `tests/appliance_container_robot.rs` uncompilable, and the
# reason is a file collision rather than a version skew:
#
# * `gunnar-build-thing` is ALREADY a `[build-dependencies]` here (`build.rs`
#   writes the receipt), so a `cargo test` of this package reaches it twice —
#   once as a HOST unit under `[profile.release.build-override]`
#   (`-C embed-bitcode=no`, no `opt-level`) and once as a target unit under
#   `[profile.release]` (`-C opt-level=3 -C codegen-units=1`, thin LTO). Two
#   units, two `-C metadata` hashes, two different `nornir-build-thing` units
#   underneath them.
# * `gunnar-build-thing`'s `[lib] crate-type` includes `cdylib` (the wasm module
#   the forge loads), and for such a target cargo emits NO `--extra-filename`.
#   MEASURED in `cargo test -v`: both units are
#   `--out-dir target/release/deps` with no extra filename, so both write
#   `libgunnar_build_thing.rlib` and the last one to finish wins.
#
# So the test linked whichever unit had run last, and got, deterministically per
# feature set and per build history, either
#
#   error[E0599]: no method named `variants` found for struct `GunnarBuildThing`
#   note: there are multiple different versions of crate `nornir_build_thing`
#
# (the trait came out of one unit, the type out of the other) or
#
#   error: failed to get bitcode from object file for LTO (Can't find section
#   .llvmbc)
#
# (the host unit's bitcode-less rlib linked into a thin-LTO test binary). No
# version bump reaches either — both sides were already on 0.1.10 — and a
# `[patch.crates-io]` onto `../edda` is refused by name in
# `crates/gunnar-build-thing/Cargo.toml` and by the reuse rule.
#
# The robot reads the DECLARATION instead of linking the recipe: see that file's
# header. That needs only the types, which is the entry below.
# The declaration TYPES the robot acts on — `BuildVariantSet` and its
# `load_file`, `ContainerBuild::build_args`/`containerfile`,
# `BuildVariant::cargo_args`. An ordinary rlib, so its host and target units get
# distinct `-C extra-filename`s and coexist; that is exactly what
# `gunnar-build-thing` cannot do, and why the robot goes through the file.
#
# Through the WORKSPACE entry (a path dep on the sibling edda checkout since
# 2026-08-30) rather than a version repeated here. Naming the same
# version was never the sameness guarantee it used to claim to be (see above),
# and with the root on a PATH it would be the opposite of one: a registry entry
# beside a path entry is a SECOND package, and the `E0599 … multiple different
# versions of crate nornir_build_thing` this whole block is about is precisely
# that failure. One entry, so the loader that reads the mirror is the loader
# that wrote it.
nornir-build-thing = { workspace = true }

[build-dependencies]
# **The build thing writes the receipt.** `build.rs` calls
# `gunnar_build_thing::emit::write_receipt`, which resolves WHICH declared cell
# this compilation is out of the features cargo resolved, and emits the
# `#[used] #[link_section = ".nordisk.buildthing"]` static that `src/main.rs`
# includes. See `crates/gunnar-cli/build.rs` for the long story.
# The RECIPE, not the cdylib shim — see the root manifest for why they are two
# crates.
gunnar-recipe = { workspace = true }