[workspace]
resolver = "2"
# A member listed here but absent from disk makes cargo refuse to load the whole
# workspace, which blocks every OTHER crate's tests. So the six crates still being
# written are scaffolded as placeholders rather than left out. See
# the crate table below for what each is meant to become.
members = [
"crates/gunnar-store",
"crates/gunnar-core",
# Moved to ../edda/crates/ (shared gix/git leaves) and, since 2026-09-09,
# consumed from crates.io by VERSION rather than by path:
# gunnar-pktline 1.1.0, gunnar-pack 1.1.1, gunnar-stage 1.1.0,
# gunnar-delta 1.1.1, gunnar-bitmap 1.1.1, gunnar-sendpack 1.1.0.
"crates/gunnar-wire",
# Encrypted repositories: the envelope, the key ring, the members, the
# server's plaintext check and the age wrap to SSH keys (2026-10-05).
"crates/gunnar-crypt",
"crates/gunnar-policy",
"crates/gunnar-lfs",
"crates/gunnar-ssh",
"crates/gunnar-http",
"crates/gunnar-proto",
"crates/gunnar-grpc",
"crates/gunnar-client",
"crates/gunnar-forge",
# The server half, extracted from gunnar-cli (2026): SSH+HTTP git transports
# and the gRPC control plane. gunnar-cli re-exports it as its `server` module.
"crates/gunnar-server",
# `cargo install gunnar`: the server binary, published (owner order 2026-10-22).
"crates/gunnar",
# Shared CLI + logging + build identity, depended on by both gunnar-cli and
# (from 2026) the gunnar-server crate.
"crates/gunnar-common",
"crates/gunnar-pairing-door",
# The ONE implementation of the repair verbs (review_twin, recharge_from_twin,
# adopt_twin, fence_primary) and of the signed-order check (5.4 resilience).
"crates/gunnar-repair",
# gunnar-front (vm1's one program, the FRONT medium's PID 1) and
# gunnar-monetize-guest (gunnar's own signed wasm monetize plugin) are DETACHED
# workspaces since 2026-10-22: see `exclude` below.
# The wire a product server registers its own plugin over, into vm1's
# plugin door (both gunnar-front and gunnar-server speak it).
"crates/gunnar-cli",
# gunnar's BUILD THING — the recipe crate (2026-08-06 design). A member as of
# 2026-08-06: it was detached only because it needed
# `nornir_build_thing::contract`, which was not in the published 0.1.4, and
# 0.1.5 put it on crates.io. The repo has since moved past that: the pin is
# `0.1.7` (`crates/gunnar-build-thing/Cargo.toml:65`), which is also the one
# version `Cargo.lock` resolves. Naming 0.1.5 here as if it were current was
# how the mismatch stayed invisible. Nothing here reaches a sibling checkout
# either way, so "builds standalone from a fresh clone" holds.
#
# `gunnar-cli` BUILD-depends on it: its `build.rs` writes the receipt into
# every gunnar binary. A build dependency never reaches the shipped binary,
# so the appliance graph is unchanged — `cargo xtask guard` measures that.
"crates/gunnar-build-thing",
"crates/gunnar-recipe",
# Repository tasks: the dependency guard and `cargo xtask ci`.
# NOTHING depends on it — it is invoked as a tool, never as a dependency —
# so `cargo build -p gunnar-cli` never touches it. It has no dependencies of
# its own, so being a workspace member cannot turn a feature on anywhere.
"xtask",
]
# `cargo build --release` at the root of a lone clone builds `gunnar`, the server,
# with its minimal default features (engine-znippy, mimalloc, http) and nothing
# else (owner order 2026-10-22). Every other crate still builds with `-p`.
default-members = ["crates/gunnar"]
# The guard's red-first proofs (`xtask/tests/guard.rs`) are asserted against
# deliberately-bad workspaces. They carry gunnar's own crate names on purpose,
# because `gunnar-store` depending on `gunnar-client` is the edge the guard singles
# out. So they must stay out of this workspace, and out of every glob that might
# one day replace the explicit member list above.
exclude = [
# DETACHED (owner order 2026-10-22, a lone clone must load this workspace):
# gunnar-front and gunnar-monetize-guest need crates that live ONLY in sibling
# checkouts (monetize, monetize-proto, monetize-server, gunnar-ui-server, …),
# and cargo refuses a workspace any of whose members cannot find a path even
# when the edge is optional or a dev-dependency. Each is its own workspace with
# its own lock, built with `--manifest-path` and this repo's `target/`.
"crates/gunnar-front",
"crates/gunnar-monetize-guest",
# the old znippy engine crate was here and is DELETED (2026-08-10). It was the
# znippy arm's wire adapter — decoded `read`, `header`, bulk `sizes`,
# `emit_pack`, `ReachSet` with the `Ok(None)` escape hatch, symbolic `HEAD` —
# written before there was a trait to hang any of it on. `GitServe`
# is that trait, and the znippy plugin implements
# it directly, so the adapter has no place left to stand.
#
# ⚠ IT IS THE REFERENCE IMPLEMENTATION OF `GitServe` AND IT IS NOT LOST.
# `git show cb43c7ec3ef00de3775137b543c262d71ed5df93 -- \
# the old znippy engine crate/src/git_store.rs` (1298 lines, 2026-08-08)
# Read it before writing `GitServe`'s znippy half. Two facts it proves that
# the design must not lose: `emit_set` is INHERENT on the concrete znippy
# store (so the gix arm must grow one from scratch — the expensive half),
# and the read path deliberately bypasses `GitOps::get`, which is the whole
# reason `GitServe` has to exist.
#
# Do NOT port its code: it has 87 `gix_` references and carries a serial
# sort/reorder/hash tail plus one heap allocation per object. Port its INTENT. The
# Arrow arm's answer is a byte-range copy out of the archive, with no gix in
# it at all (R3b).
# `crates/gunnar-znippy` was here and is DELETED (2026-08-10). It existed
# only because the storage engine was chosen by linking a different crate
# into a runtime `OnceLock`, so something had to call `install()` before
# `run()` and that something needed a `main`. It emitted `[[bin]] gunnar`
# and `[[bin]] gunnar-server` — two names `.nornir/build-variants.toml` had
# already assigned to `gunnar-cli` and `gunnar-server` — while being absent
# from that declaration itself. The engine is a FEATURE of `gunnar-server`
# now (`engine-znippy` / `engine-gix`), so there is one writer per artifact
# name and the matrix has its authority back.
"crates/gunnar-growbench", # detached like installtest/: the KVM data-set-growth bench (DATA-SET-GROWTH-DESIGN.md §7), its own lock
"crates/gunnar-loadbench", # detached for the same reason: the estate's load/capacity bench, its own lock — gunnar's must never learn about a harness
"xtask/tests/fixtures/rayon_creeps_in",
"xtask/tests/fixtures/s_depends_on_client",
"xtask/tests/fixtures/gix_porcelain_in_server",
]
[workspace.package]
# The two co-owners (50/50), companies only: a personal name or address in a
# published crate can never be taken back (crates.io prep, 2026-10-11).
authors = ["Ignalina ApS", "Vetra Automation AB"]
version = "1.5.1"
edition = "2021"
rust-version = "1.85"
license = "AGPL-3.0-only"
# Where the canonical tree is served (gunnar's own forge) — what a crates.io
# reader is sent to, as gunnar-forge's published page already says.
repository = "https://git.gunnar.rs/nordisk/gunnar"
# **Nothing here goes to a public registry unless it says so by name.**
#
# `[repos.gunnar]` in the constellation descriptor is `release = false` and
# `crates/gunnar-forge/Cargo.toml` spends thirty lines explaining that it is
# "the ONE crate in this repo that goes to a public registry". That was the
# whole of the mechanism: prose, in two places, and nothing that would stop a
# `cargo publish -p gunnar-server` typed in the wrong directory from putting the
# appliance's server half on crates.io permanently — a publish cannot be undone,
# only yanked, and a yanked crate is still downloadable.
#
# Inherited rather than repeated: every member carries `publish.workspace = true`
# and gunnar-forge overrides it with an explicit `publish = true` and its
# reason. One value, one place, and the exception is visible as an exception.
# `xtask/tests/publishable.rs` fails the build if a member stops saying either.
publish = false
[workspace.dependencies]
# ── internal ────────────────────────────────────────────────────────────────
gunnar-pktline = { version = "1.1.0" }
# The requirements below are each crate's MINIMUM: a patch release (gunnar-crypt, gunnar-wire,
# gunnar-client, gunnar-http 1.5.1, lane GUN-INSTALL 2026-10-26) changes no API, so its
# dependents keep asking the earlier version and stay what crates.io holds; a major (2.0.0)
# is asked by name. A raised requirement is a changed published manifest, i.e. a publish row
# (`cargo xtask crates-io-install --stage` names it).
gunnar-store = { version = "1.1.0", path = "crates/gunnar-store" }
gunnar-core = { version = "2.0.0", path = "crates/gunnar-core" }
gunnar-pack = { version = "2.0.0" }
# Donation-shaped — no gunnar dependencies, gix's own conventions. Reached ONLY
# through `gunnar-pack`'s optional `delta`
# feature, which `gunnar-client` turns on and the appliance profile does not, so
# the server graph is unchanged. No `version.workspace` / `features.workspace`
# is pushed back onto it: its own manifest must keep standing on its own.
gunnar-delta = { version = "2.0.0" }
gunnar-bitmap = { version = "2.0.0" }
# Donation-shaped. No `version.workspace` / `features.workspace` is
# pushed back onto it: its own manifest must keep standing on its own. The
# `default-features` choice is made at each consumer, because that is what
# decides whether `gix-packetline` enters that consumer's graph — `gunnar-wire`
# says `false` and pays nothing, `gunnar-client` says nothing and gets the
# driver.
#
# `default-features = false` is set HERE rather than at each consumer, because
# cargo ignores a member's `default-features` unless the workspace entry sets it
# too — and the failure mode is silent: the member reads as if it opted out and
# the framer is linked anyway. So the workspace default is the CHEAP half, and a
# consumer that wants the pkt-line driver names `features = ["blocking-io"]`
# out loud. That is the right way round for an appliance.
gunnar-sendpack = { version = "2.0.0", default-features = false }
gunnar-wire = { version = "1.1.0", path = "crates/gunnar-wire" }
gunnar-stage = { version = "1.1.0" }
gunnar-policy = { version = "1.1.0", path = "crates/gunnar-policy" }
gunnar-crypt = { version = "1.5.0", path = "crates/gunnar-crypt", default-features = false }
gunnar-lfs = { version = "1.1.0", path = "crates/gunnar-lfs" }
gunnar-ssh = { version = "2.0.0", path = "crates/gunnar-ssh" }
gunnar-http = { version = "1.1.0", path = "crates/gunnar-http" }
gunnar-proto = { version = "2.0.0", path = "crates/gunnar-proto" }
gunnar-grpc = { version = "2.0.0", path = "crates/gunnar-grpc" }
gunnar-repair = { version = "2.0.0", path = "crates/gunnar-repair", default-features = false }
# **The LIST half of mirroring, and the one crate that must never reach the
# appliance.** It speaks forge REST over outbound HTTPS and borrows a user's
# personal access token for the length of one call; the appliance has neither an
# egress path nor any business holding that credential. Named here so the ONE
# consumer — `gunnar-cli`'s `control` feature — inherits a single copy, and
# `crates/gunnar-forge/Cargo.toml`'s header explains why `xtask guard` does not
# track it: nothing in the `server` graph resolves it, and the day something
# does, the appliance crate count moves where the guard prints it.
gunnar-forge = { version = "1.1.0", path = "crates/gunnar-forge" }
# **`default-features = false` HERE, and it has to be here.** A member that
# writes `default-features = false` beside `workspace = true` is IGNORED by
# cargo unless the workspace table says it too — cargo says so out loud
# ("`default-features` is ignored for X, since `default-features` was not
# specified for `workspace.dependencies.X`") and MEASURED 2026-08-30 it cost
# `gunnar-server`'s new `replication` feature its entire point: the appliance
# graph came back at 493 packages, gix and all, exactly as before the split.
#
# So the default is the WIRE half, and every consumer that wants the porcelain
# names it: `gunnar-cli`'s `client`, `gunnar-common`'s `client`,
# `gunnar-server`'s `client`, and `bench` (a separate workspace, so it says it
# in its own manifest).
gunnar-client = { version = "1.1.0", path = "crates/gunnar-client", default-features = false }
# The parts every gunnar binary shares: the `Command` line, the logging ladder,
# and the build identity. `gunnar-cli` and `gunnar-server` both depend on it.
#
# `default-features = false` HERE, not only on the two members. Cargo IGNORES a
# member's `default-features = false` on an inherited dependency unless the
# workspace entry carries it too, and says so on every build ("this could become
# a hard error in the future"). Both members wrote it and neither got it. It
# costs nothing today — `gunnar-common` declares no `default` feature — and the
# whole point of writing it down is the day somebody adds one: the appliance
# graph the dependency guard measures must not grow in silence.
gunnar-common = { version = "2.0.0", path = "crates/gunnar-common", default-features = false }
# The estate's wall clock (lane L10, 2026-10-21): edda `estate-clock`, where
# gunnar-clock moved; Gunnar's own crate is deleted. 0.1.1 speaks both wires
# (`ESTATE-TIME/1` and Gunnar's `GUNNAR-TIME/1`, `peer::Dialect`) and reads
# `GUNNAR_CLOCK*` as an alias of `ESTATE_CLOCK*` (`config::var`), so a front and
# its boxes keep talking across the release. A PATH onto the sibling `../edda`
# with its version, like appliance-pid1; same ssh-key (=0.7.0-rc.11) and rand
# (0.10) as this workspace, so the graph keeps ONE ssh-key.
estate-clock = { version = "0.1.1", default-features = false }
# The server half (SSH+HTTP git transports + gRPC control plane), extracted from
# gunnar-cli. gunnar-cli depends on it under its `server` feature and re-exports it.
gunnar-server = { version = "2.0.0", path = "crates/gunnar-server" }
# **The recipe, and the cdylib that exports it — two crates since 2026-09-04,
# for one measured reason.**
#
# `gunnar-recipe` is the declaration and the receipt writer, a PLAIN RLIB, and
# it is what gunnar-cli's and gunnar-server's `build.rs` build-depend on.
# `gunnar-build-thing` is a shim: a `pub use` of the recipe plus
# `export_build_thing!`, keeping the package name and the `cdylib` crate-type
# that `dwarves/crates/dwarves/src/build_thing/code.rs:125-133` demands of a
# `<repo>-build-thing`.
#
# They were one crate, and a `crate-type` containing `cdylib` gets no
# `--extra-filename` from cargo — so the crate's two units (a
# `[build-dependencies]` one and a normal one, kept apart by resolver 2) wrote
# one filename, last writer won, and the build script linked the loser with
# `undefined symbol` for a function that was in the rlib. `cargo build
# --workspace` was **red 6 of 8 from cold** and always green warm, which is why
# it lived so long. cargo#6313; measurement in
# the storage-lever measurements of 2026-09-03.
#
# Both release in wave 0, before the things whose receipts name them, and both
# carry the SAME version: a receipt's coordinate is
# `gunnar-build-thing:<version>` (`gunnar_recipe::recipe::KRATE`), so the
# version dwarves resolves and the version the recipe was built from must be one
# number.
gunnar-recipe = { version = "0.1.1", path = "crates/gunnar-recipe" }
gunnar-build-thing = { version = "0.1.0", path = "crates/gunnar-build-thing" }
# ── git plumbing (trait-generic; safe for the no-OS server) ─────────────────
# The SERVER never opens `gix::Repository`. It is worktree-free and reaches the
# object/ref layer through its own traits.
gix-hash = { version = "0.27", features = ["sha1", "sha256"] }
gix-object = "0.65"
# `parallel` is declared here rather than inherited: undeclared, the build takes
# the serial path silently. gix-pack has ZERO
# `cfg(feature = "parallel")` gates of its own. The feature purely forwards to
# `gix-features/parallel`, and whether `in_parallel` is the threaded
# implementation or a plain `for` loop is decided by whatever else in the
# resolved graph happens to switch it on. MEASURED 2026-08-03: the appliance
# build gets it only via `gunnar-store/fs` → `gix-odb/parallel`, and
# `cargo tree -p gunnar-pack --no-dev-dependencies` showed it ABSENT. The one
# crate that actually passes `IndexOptions::thread_limit` on the receive path was
# silently serial when built alone, with no compile error, no warning and no
# runtime signal. `crates/gunnar-cli/tests/feature_split.rs` asserts it in both
# feature sets now.
gix-pack = { version = "0.75", features = ["streaming-input", "parallel"] }
# These two must track what gix-object 0.65 / gix-pack 0.75 / gix 0.88 actually
# require (^0.50 and ^0.43; moved together in the 2026-10-02 gix 0.88 wave, which
# also put the server plumbing and the client porcelain on ONE set of gix-*
# majors — before it the server pinned gix 0.86's set under a gix 0.87 client,
# and the lock carried two copies of object/pack/odb/ref/actor). An older pin resolves a SECOND, semver-incompatible
# copy into the graph, and the mismatch surfaces as a confusing trait error:
# `DynNestedProgress` from the wrong gix-features not satisfying
# `Bundle::write_to_directory`. Verified against the published manifests.
gix-features = { version = "0.50", features = ["progress"] }
# gix 0.88 reports failures as `gix_error` exceptions (`Exn`), which are not
# `std::error::Error`; crates that keep a gix failure in their own error type
# hold it as `gix_error::Error`.
gix-error = "0.4"
gix-actor = "0.43"
# The `fs` backend's two halves: `gix-odb` for loose objects and packs,
# `gix-ref` for the loose + `packed-refs` file store with `gix-lock` rename
# atomicity. They assume a filesystem, which is why the backend is behind a
# feature. But they are PLUMBING, and neither reaches for a worktree, a
# subprocess or `.git/config`. `gix` (below) does all three, which is why this
# backend no longer goes through it. See `crates/gunnar-store/src/fs.rs` and
# `xtask/src/guard.rs`, which asserts their absence from the resolved appliance
# tree.
#
# `parallel` is what `gix`'s `max-performance-safe` used to switch on for these
# two, and it is load-bearing rather than a tuning knob: without it gitoxide's
# `OwnShared` is `Rc`, `gix_ref::file::Store` stops being `Send`, and no
# `RefStore` (`Send + Sync`) can hold one. It costs `crossbeam-channel` and
# `parking_lot`, both already in the tree. NOT rayon, which gix does not use
# and the multicore rule forbids.
gix-odb = { version = "0.85", features = ["sha1", "sha256", "parallel"] }
gix-ref = { version = "0.68", features = ["sha1", "sha256", "parallel"] }
gix-lock = "25.0"
# Plain SHA-1 for the pack *trailer* only: the checksum role, not the
# object-id role. Already in the graph under `sha1-checked`, which `gix-hash`
# uses; this asks for the same crate without the collision-detection layer that
# keeps it off `sha_ni`. `crates/gunnar-pack/src/checksum.rs` carries the whole
# argument for why that is safe here and would NOT be safe for an object id.
sha1 = "0.10"
# gix PORCELAIN, CLIENT ONLY. Anything that links this has assumed a filesystem
# AND a worktree AND a shell: `gix` pulls `gix-discover`, `gix-config`,
# `gix-worktree`, `gix-index`, `gix-ignore`, `gix-attributes`, `gix-filter`, and
# via `blob-diff` also `gix-command`, whose whole job is spawning subprocesses.
# The appliance build bans every one of them.
gix = { version = "0.88", default-features = false, features = [
"max-performance-safe",
"revision",
"blob-diff",
"sha1",
"sha256",
] }
# ── SSH ─────────────────────────────────────────────────────────────────────
# 0.61 -> 0.63 (release-3 merge, 2026-10-03): the W3 keystone edda's nornir-git
# took (5594b81) — ssh-key =0.7.0-rc.11, ed25519-dalek 3, rand_core 0.10 inside
# russh. Forced here: gunnar-front links monetize (ed25519-dalek 3), and russh
# 0.61's =3.0.0-rc.0 pin cannot share a lock with it.
russh = "0.63"
# 0.6 -> =0.7.0-rc.11 (the RustCrypto bump on the release-3 merge, 2026-10-03):
# the version russh 0.63 rides, so gunnar's keys and russh's are one type.
# Pre-release: no stable 0.7 yet; EXACT pin because rc APIs change between
# candidates; move to the stable when it ships. Its rand_core is 0.10.
ssh-key = { version = "=0.7.0-rc.11", default-features = false, features = ["ed25519", "std", "rand_core", "encryption"] }
# rand_core 0.6 -> rand 0.10 (same bump): rand_core 0.10 has no OsRng; the OS
# generator is `rand::rngs::SysRng` (getrandom), wrapped in
# `rand::rand_core::UnwrapErr` where a CryptoRng is asked for. Still the OS
# CSPRNG, nothing seeded here. Already in the lock (monetize, russh).
rand = { version = "0.10", default-features = false, features = ["std", "sys_rng"] }
# ── crypto at rest ──────────────────────────────────────────────────────────
# The ONE crypto backend in this binary. It is already here: `russh` links it,
# and the stripped appliance carries 123 `AWS_LC` strings with zero PT_INTERP
# and zero DT_NEEDED, i.e. statically. `gunnar-core::seal` names it directly so
# the AES-256-GCM that seals secrets at rest is the same AES the SSH transport
# uses. Version tracks what russh already resolved, so this adds no package.
aws-lc-rs = "1.17"
# **The server's own X.509 issuer**: `gunnar_core::issuer`. Minting a
# CA, signing a CSR, and parsing an imported certificate.
#
# NOT `cert-helper`, which `bench/src/trust.rs` uses.
# That crate is built on `openssl`, and `openssl`/`openssl-sys` are on
# `xtask::guard::FORBIDDEN_ANYWHERE` — banned from BOTH halves, and the
# appliance is a static musl binary in a distroless image with no system
# libssl to link against. `bench/` is a detached workspace the guard does not
# resolve, which is why `trust` may keep using it and this may not. That split
# is a feature: `trust` and the tests here verify gunnar's issuance with
# **openssl**, sharing no code with what minted it.
#
# `rcgen` is pure Rust over the AEAD/signature backend this tree already
# carries. `default-features = false` + `aws_lc_rs` keeps gunnar-core's
# one-crypto-backend rule (see its manifest); the `x509-parser` feature is what
# parses a CSR and an imported CA, and it pulls `ring` in through its own
# `verify` feature — already resolved here under tonic's `tls-ring`.
rcgen = { version = "0.14", default-features = false, features = [
"crypto",
"pem",
"aws_lc_rs",
"x509-parser",
] }
# Named directly for ONE thing: pulling the SubjectPublicKeyInfo out of an
# imported CA certificate, so `issuer import` can refuse a certificate and key
# that are not each other's. It costs no package — `rcgen`'s `x509-parser`
# feature already resolves it, and the version is pinned to what rcgen asks
# for so the two share one copy.
x509-parser = "0.18"
# The validity window an issued certificate carries. `rcgen` takes
# `time::OffsetDateTime` in its public API and does not re-export the crate, so
# naming it is how a caller computes "a year from now". It costs no package:
# `rcgen` already resolves it.
time = { version = "0.3", default-features = false, features = ["std"] }
# The CLIENT half of git-over-SSH, reused not twinned: protocol-v2 `ls-refs`,
# SHA-256 oids, and the in-runtime channel teardown.
#
# **`default-features = false` is the whole appliance-replication story**, and
# it is why the entry gained a `path`. nornir-git 0.1.9 splits itself: the
# `porcelain` half (`gitio`/`gitops`/`gitclean`/`fleet`) is gix with `status`,
# `blocking-network-client` and `blocking-http-transport-reqwest-rust-tls`; the
# other half is the russh SSH transport, which is what `gunnar-client`'s
# send-pack — and therefore `gunnar-server`'s replication — actually
# dials. MEASURED on 0.1.9: 326 packages with the feature, 153 without.
# `gunnar-client`'s own `porcelain` feature turns it back on for the `gunnar`
# binary; the appliance leaves it off.
#
# **The `path =` is gone (2026-09-09) and nothing else changed** — which is exactly
# what the entry that stood here promised: "the version is kept so the day edda
# publishes it the `path` can be deleted with nothing else changing". edda published
# `nornir-git 0.1.9`; this line is the deletion. The mandatory `../edda` sibling
# checkout it used to create — cargo reads a path dependency's manifest at RESOLVE
# time, so it was a hard requirement of every build here — is gone with it.
#
# It was never a `[patch.crates-io]`, which is the structural publishing blocker the
# rule forbids, and that is why the change was one key wide instead of a rewrite.
nornir-git = { version = "0.2.0", default-features = false }
# **The build thing's own types — ONE entry, and it is a PATH, for the same
# reason `nornir-git` above it is** (2026-08-30).
#
# It was `version = "0.1.10"` spelled FOUR times (gunnar-build-thing's normal +
# dev entries, gunnar-server's, gunnar-cli's dev entry at `0.1.4`), and the pin
# had a MEASURED consequence, not a stylistic one: `OptionSpec` is
# `#[serde(deny_unknown_fields)]`, so on 0.1.10 the key `kernel_requires` inside
# a `[[build.installer.options]]` row makes the WHOLE recipe unloadable for this
# repo's own reader —
#
# unknown field `kernel_requires`, expected one of `name`, `label`, `default`,
# `feature`, `params_when_on`, `params_when_off`, `param_kinds`,
# `forbid_symbols`, `bakes`
#
# — so gunnar's kernel closure had to be parked on a temporary
# `[[build.installer.option_requires]]` table that `InstallerBuild` (not
# `deny_unknown_fields`) merely dropped on the floor here. The declarations were
# invisible to the repo that owns them. 0.1.11 is where `kernel_requires` lives,
# and it is NOT on crates.io: local 0.1.10 had already drifted from the
# published 0.1.10 (edda's own manifest says so), so a version bump alone cannot
# reach it.
#
# The road was the sanctioned one, which `nornir-git` already walked: a PATH
# dependency with the version KEPT — never a `[patch.crates-io]`, which is the
# structural publishing blocker that is forbidden and which
# `crates/gunnar-build-thing/Cargo.toml` refused by name. **That day came on
# 2026-09-09: the `path =` is deleted and nothing else changed.** The registry now
# serves 0.1.14, well past the 0.1.11 this note was waiting for.
#
# Why it took from 0.1.11 to 0.1.14 to get there, because it is not what anyone
# would guess: nornir-build-thing could not be published AT ALL. After 0.1.12 it
# grew an optional `nornir-xtask-support` dependency behind its `wasm-stage`
# feature, that leaf was `publish = false`, and cargo requires every declared
# dependency to resolve in the index whether or not the feature is on —
# `no matching package named nornir-xtask-support found`. One unpublished leaf
# behind an off-by-default feature held a whole published crate back, and nothing
# said so until a release actually ran. edda published the leaf; this line followed.
#
# ONE entry rather than four, and that is load-bearing rather than tidy: a path
# dependency and a registry dependency of the same crate resolve to two
# DIFFERENT packages, which is exactly how dwarves b81545c earned an
# `E0609: no field dsse` — so every member names this one line.
#
# 0.1.24 (2026-09-30): the first reader of schema 5, which monetize declares.
# 0.1.20 refused it ("understands up to schema 4"), so `xtask payload` could
# not resolve another suite member's cell (stinky `payload-schema-skew`;
# `payload::tests::another_suite_members_schema_5_cell_resolves_through_the_payload_door`).
# 0.2.6 (2026-10-26, lane GUN-INSTALL): gunnar-recipe's struct literals name every field
# of the version it is written for, and 0.2.6 added `InstallerBuild::payload_entries`.
# `^0.2.5` let the published gunnar-recipe 0.1.0 (written for 0.2.5) resolve 0.2.6, and
# `cargo install gunnar` stopped at E0063. The requirement IS the version the lock
# builds (`xtask/tests/both_ways.rs`): a lock that follows edda past it is a red here,
# and the fields it adds get their decision in gunnar-recipe before the bump.
nornir-build-thing = { version = "0.2.6", default-features = false }
# **The shared admission trait** (`edda_admit::Admit`: plan → admit → record),
# 2026-09-28. gunnar-ssh's gate asks it for a keyed transfer's CLASS when the
# `admit-share` feature is on; without the feature the gate never calls it and
# only `Usage.AdmitPlan` reads a PassThrough through it. Product-neutral, so it
# lives in edda and holger takes the same crate.
#
# A PATH onto the sibling `../edda` (with its version), like `guest-host` and
# `wg-appliance-core`, and NOT a git dependency. MEASURED 2026-09-28 on loki: the
# Build Thing compiles in the container with `--network none --offline --locked`
# and mounts only the host-fetched `CARGO_HOME/registry` — never `CARGO_HOME/git`
# — so the git pin (521270d) failed EVERY gunnar container build, not only the
# rows with `admit-share`: "failed to get `edda-admit` … can't checkout from
# the old forge's edda.git: you are in the offline mode". The
# sibling `edda` is already in `[build.toolchain] siblings`, so the container
# sees it. The sibling must carry `crates/admit` (edda `admit`, 521270d). The
# `path` is deleted the day edda publishes `edda-admit`, as `nornir-git`'s was.
# The line lives in its one consumer, crates/gunnar-ssh/Cargo.toml, NOT here: the
# root manifest leaves the repository nowhere (xtask one_clone
# the_scanner_reads_real_path_lines), as disk-snapshot's lives in gunnar-server.
# ── async / rpc ─────────────────────────────────────────────────────────────
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-util", "net", "sync", "fs", "time", "signal"] }
tokio-util = { version = "0.7", features = ["io-util"] }
tokio-stream = "0.1"
# `tls-ring` is mTLS on the control plane. It pulls tokio-rustls and
# ring into the server graph, which the dependency guard has an opinion about —
# recorded as a deliberate cost: the control plane authenticates operators, and
# an unauthenticated one is a hole no amount of graph tidiness pays for.
# tonic 0.14 (was 0.13, bumped 2026-08-23). 0.14 moved prost codegen into
# `tonic-prost`/`tonic-prost-build`, so the `prost` FEATURE is gone and the codec
# is named as its own dependency. One tonic across the constellation: two majors in
# one graph make `Routes` and `ServerTlsConfig` different types.
tonic = { version = "0.14", default-features = false, features = ["router", "transport", "codegen", "tls-ring"] }
tonic-prost = "0.14"
# `tonic` depends on this directly, so naming it costs no package in any graph.
# gunnar needs it because the request-id span is a tower LAYER around the
# whole `Routes` table, and an interceptor cannot see a response.
tower-layer = "0.3"
# tonic-prost-build, not tonic-build's `prost` feature: 0.14 split the codegen out.
tonic-prost-build = "0.14"
# The fallback protoc when none is on PATH or in $PROTOC: `cargo install gunnar` needs no
# pre-step (owner 2026-10-22). A build dependency only, never in a shipped binary.
protoc-bin-vendored = "3.2"
tonic-build = { version = "0.14", default-features = false }
prost = "0.14"
prost-types = "0.14"
# ── storage ─────────────────────────────────────────────────────────────────
# NO DATABASE. Refs and secrets are Arrow IPC streams: one RecordBatch per push,
# so batch atomicity is a property of the IPC frame rather than of a transaction
# manager. Objects are pushed packs folded into a znippy archive at repack. redb
# was here and was dropped: a database inserted between two archive formats.
arrow-ipc = "59"
arrow-array = "59"
arrow-schema = "59"
# ── plumbing ────────────────────────────────────────────────────────────────
anyhow = "1"
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# `flate2`'s default backend is miniz_oxide; `zlib-rs` is ~2x faster at the
# same compression ratio and is ALREADY linked into this tree via `gix-zlib`, so
# selecting it costs no new dependency and, unlike zlib-ng, no C toolchain.
# `zlib-rs` implies flate2's `any_zlib`, which is what actually switches the
# backend: `flate2/src/ffi/mod.rs` only compiles its miniz_oxide module when
# `any_zlib` is off. `default-features = false` drops miniz_oxide from what
# *gunnar* asks for; it stays in `Cargo.lock` because `russh` still requests
# flate2's defaults, but it is then dead weight, not the backend in use.
flate2 = { version = "1", default-features = false, features = ["zlib-rs"] }
clap = { version = "4", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
hex = "0.4"
tempfile = "3"
# **The control plane's storage**: the catalog, the identity table
# and every document `gunnar_core::doc` puts behind a name.
#
# NOT a new crate in the appliance graph in the sense that matters: redb is the
# znippy engine's own tail, so the binary that carries a storage engine already
# links it. It has no dependencies of its own, no C, and a build script that
# only probes the compiler version — so the build that does *not* carry an
# engine (`gunnar`, the client) pays one pure-Rust crate for it.
#
# It is here rather than in the object store because the engine takes objects as
# PACKS. `GitBackedObjects::write` refuses a single-object write by design, and
# a control document is one 200-byte JSON blob per change — the one shape that
# storage cannot express. See `crates/gunnar-core/src/control.rs`.
redb = "2"
# The shared job runtime (A32-14): ChainQueue lanes for a box's background duty and job queue.
# Lean (no container or VM engine); crates.io, as monetize and Holger name it.
jera = { version = "0.2.1", default-features = false }
# PID 1's syscalls, and only PID 1's: `waitpid` to reap orphans and an
# `AF_NETLINK` socket to bring the appliance's own interface up
# (`crates/gunnar-cli/src/server/pid1.rs`). Chosen over `libc` and over a
# netlink binding crate for three reasons that all matter here:
#
# * on `x86_64-linux` rustix's default backend is `linux_raw`: raw syscalls
# written in Rust, **no libc and no C at all**, so it cannot re-introduce the
# thing `containers/Containerfile.bin-musl` asserts is absent (a PT_INTERP);
# * the API is safe, so `pid1.rs` needs no `unsafe` block to become the one
# file in the server that talks to the kernel directly;
# * `default-features = false` plus two feature flags is `bitflags` +
# `linux-raw-sys`, both already resolved in this tree.
#
# `std` is on because gunnar is a hosted binary; `process` is `waitpid`, `net`
# is the socket. Nothing else is enabled: not `fs`, not `mount`, not `runtime`.
rustix = { version = "1", default-features = false, features = ["std", "process", "net"] }
# ONE setsockopt rustix cannot spell: `SO_BINDTODEVICE`, which the PID-1 DHCP
# client (`crates/gunnar-server/src/dhcp.rs`) needs so a limited broadcast has
# an output device while the interface still has no address. rustix 1.1.4's
# sockopt module has no helper for it (checked 2026-08-12); `socket2` does
# (`bind_device`, behind its `all` feature), it is safe Rust, MIT/Apache, and it
# was ALREADY resolved in this lockfile under tokio and hyper-util — this edge
# adds a direct dependency, not a new crate. Everything else about the DHCP
# socket is std's `UdpSocket`.
socket2 = { version = "0.6", default-features = false, features = ["all"] }
# ── the one patched dependency ──────────────────────────────────────────────
#
# `vendor/gix-features` is gix-features **0.50.0**, verbatim, plus ONE changed
# file — `src/parallel/reduce.rs`, fix 2. Rebased from 0.49.1 on 2026-10-02 (gix
# 0.88 wave): 0.50.0's `src/` is byte-identical to 0.49.1's, so the rebase was a
# copy of stock 0.50.0 plus the same reduce.rs diff. See that directory's `PATCH.md` for the
# diff and `crates/gunnar-pack/tests/index_floor.rs` for the assertion that goes
# red the day the patch stops being applied.
#
# It was 0.49.0 plus TWO changed files until 2026-08-25. Fix 1 shipped upstream in
# `gix-features 0.49.1` (PR #2877, verbatim — diffed), so the fork was rebased onto
# 0.49.1 and fix 1 dropped from it. THE REBASE WAS NOT OPTIONAL: a `[patch]` entry
# is a CANDIDATE and not an override, so the resolver still takes the greatest
# compatible version, and a fork pinned at 0.49.0 simply lost to registry 0.49.1 —
# cargo said `patch ... was not used in the crate graph` and gix-pack linked stock.
# A fork left behind a release is a fork that has silently stopped being used.
#
# What fix 1 fixed, kept here because it is why the patch table was created:
# `in_parallel_with_slice` spawns a watcher thread that
# `sleep`s out its whole interval, and `stop_everything` is set only after every
# worker has been joined. The watcher lives inside the same `std::thread::scope`,
# so the sleep is APPENDED to the call rather than overlapped with it.
# `gix-pack`'s delta traversal asks for 50 ms, so EVERY `index_pack` — the whole
# of gunnar's receive path — cost 50 ms of nothing. MEASURED 2026-08-04 on the
# desktop:
# an 8-object push (a five-file commit, 506 bytes) took 54.0 ms wall for 1.5 ms
# of CPU. The patch parks instead of sleeping and unparks after the joins.
#
# It is patched HERE, at `gix-features`, and not in `gix-pack` where the 50 ms
# literal lives, because the interval is not the defect — an uninterruptible wait
# is. Fixing the layer that owns the wait leaves `gix-pack` stock, fixes every
# other caller, and keeps 50 ms as what it was always meant to be: the latency of
# noticing a Ctrl-C, which nobody needs to be 1 ms.
#
# The patch does not move the dependency graph: same crate, same version, same
# dependencies, and the guard counts third-party by name. MEASURED before and
# after: appliance 280 crates / 267 third-party, both.
#
# This does NOT reintroduce what the publishing rule forbids. That rule is about a `[patch]` onto a
# SIBLING CHECKOUT (`nornir-git = { path = "../edda" }`), whose cost was that gunnar
# stopped building from a fresh clone. `vendor/gix-features` is IN this repository,
# so a fresh clone still builds standalone with no sibling and no network beyond
# crates.io. What it does cost, and this is stated as an expectation rather than a
# measurement because the nornir server was unreachable when it was written: a
# blanket no-path-patches release gate will fail on this table, and gunnar cannot
# `cargo publish` a crate whose resolution depends on it. gunnar has no users and
# publishes nothing today, which is the whole reason this was affordable now.
#
# **THE SIBLING COST IS PAID OFF, 2026-09-09.** The sentence above says the cost of
# a path onto a sibling checkout "was that gunnar stopped building from a fresh
# clone" in the past tense, and it was describing a thing this repository was still
# doing: eleven crates in `../edda`, two in `../znippy` and one in `../monetize`
# were named by `path` right up to that date, so `git clone && cargo build -p
# gunnar-cli` on a bare machine did not resolve — cargo reads a path dependency's
# manifest at RESOLVE time, optional or not, so even `metered` (off by default) was
# a hard requirement for `../monetize` to exist. Every one of them is now a
# crates.io VERSION, published deps-first through `dwarves crate-release`:
#
# gunnar-bitmap 1.1.1 · gunnar-delta 1.1.1 · gunnar-pack 1.1.1 · gunnar-pktline
# 1.1.0 · gunnar-sendpack 1.1.0 · gunnar-stage 1.1.0 · nornir-git 0.1.9 ·
# nornir-build-thing 0.1.14 · nornir-testmatrix 0.2 · storage-git-gix 0.1.1 ·
# wg-appliance-core 0.1.4 · git-storage-trait 0.1.2 · znippy-plugin-git 0.1.2 ·
# monetize-embed 0.1.0
#
# `cargo metadata` over this workspace now lists exactly ONE package resolved from a
# path outside a workspace member, and it is `vendor/gix-features` — which is in the
# repository. The `[patch]` below is therefore the whole remaining answer to "does a
# fresh clone build", and the answer is yes. `.nornir/detail.md` §10 "Building
# against sibling checkouts" says how a developer points the graph back at local
# trees when they want to — through their OWN cargo config, never through a
# `[patch.crates-io]` in this file, because that is the entry that would put the
# sibling requirement back for everybody.
#
# REMOVE THIS the day FIX 2 is released upstream, and delete `vendor/` with it.
# Fix 1 already went that way: filed as
# https://github.com/GitoxideLabs/gitoxide/pull/2877, released in 0.49.1; see
# the slice-watcher sleep measurement. Fix 2 is gitoxide issue 2881 and
# is still open, so the table stays.
[patch.crates-io]
# The SIBLING patches (edda's git leaves, nornir-git, nornir-build-thing,
# storage-git-gix, git-storage-trait, and every crate the members used to take from
# a sibling checkout by path) are NOT here, and not in `.cargo/config.toml` either:
# the root builds from crates.io in every checkout and its committed lock is the
# crates.io lock (G-B7, owner order G-BOTH-1, 2026-10-26; before it, an optional
# `.cargo/siblings.toml` include patched the root in every estate checkout). The
# estate workspaces (`xtask::siblings::ESTATE_WORKSPACES`) carry the ones they take in
# their own manifests. `xtask/tests/lone_clone.rs` holds both.
# NO `package =` HERE, AND THE VENDORED CRATE IS NAMED `gix-features`. Both were
# changed on 2026-08-23 (8952778) to `package = "gix-features-gunnar"` against a
# vendored `name = "gix-features-gunnar"`, and MEASURED 2026-08-25 that stopped the
# patch applying at all: `package =` renames the crate the patch is FOR, not the
# copy it supplies, so cargo went looking for a `gix-features-gunnar` in the graph,
# found none, and said `warning: patch ... was not used in the crate graph` while
# stock gix-pack quietly re-resolved `gix-features 0.49.1` from the registry. Drop
# the `package =` alone and cargo hard-errors instead: `patch location
# vendor/gix-features does not contain packages matching gix-features`.
# So a PATH patch binds on the vendored `[package] name` and nothing else, which is
# what `xtask/src/vendor_patch.rs` now asserts. Stock gix-pack must resolve through
# here — see vendor/gix-features/PATCH.md.
gix-features = { path = "vendor/gix-features" }
[profile.release]
lto = "thin"
codegen-units = 1
# `strip = true` removed the SYMBOL TABLE, not just the debug info, and that cost
# us the one measurement that mattered.
#
# MEASURED 2026-08-14 on the desktop, 39 `perf record` captures of `gunnar serve`
# during
# `fetkaos_monkey` (a `linux.git` mirror clone, ~25 min at 1.1 cores on a 32-core
# box): the kernel side resolved and named the copy chain outright —
# `rep_movs_alternative` 11.0%, the `filemap_read` path ~8%, `kernel_init_pages`
# 1.4%, together ~20% of cycles. The single hottest thing in the profile,
# **~18% across `0xe6078e` / `0xe60868` / `0xe607b9`**, stayed an anonymous
# address, because a stripped binary has nothing for `perf` to resolve against.
# So the profile could prove that one function dominates and could not say which
# one, and the fix for a 25-minute serial clone was left to a guess.
#
# `"debuginfo"` and not `false`: it drops DWARF (which is the bulk of the size)
# and KEEPS the symbol table, so the binary stays close to what it was and every
# frame gets a name.
strip = "debuginfo"
# ★ DEV media only (turn 2026-10-12, owner: "full LTO only for release media"): the
# `*-narrate` variants — test2's and the mock's media, never a release — build with this.
# Release's optimisation level, without the whole-program passes: codegen-units 16 and no
# LTO. MEASURED on dwarf, `gunnar-server-narrate` after a touch of gunnar-server: release
# 194 s; see .nornir/build-variants.toml for the number this profile gives.
[profile.dev-media]
inherits = "release"
lto = false
codegen-units = 16
# Project ruling, 2026-09-05: test artifacts carry line tables only — full DWARF in every
# test binary and its deps was the bulk of a target directory (271 GB for this repository,
# 852 GB for a sibling, on the day the disks filled); backtraces still show file:line.
[profile.test]
debug = "line-tables-only"
# The post-quantum wrap, optimised in dev/test builds (crypt-pq, 2026-10-06): at
# opt-level 0 ML-KEM's Keccak and the X25519 arithmetic make one hybrid
# decapsulation take tens of milliseconds, and the 500-member ring and the
# 4,000-mutation no-crash test (gunnar-crypt tests/pq_stress.rs) take many
# minutes. Only these crates; release is unchanged.
[profile.dev.package.ml-kem]
opt-level = 3
[profile.dev.package.x-wing]
opt-level = 3
[profile.dev.package.keccak]
opt-level = 3
[profile.dev.package.sha3]
opt-level = 3
[profile.dev.package.shake]
opt-level = 3
[profile.dev.package.turboshake]
opt-level = 3
[profile.dev.package.x25519-dalek]
opt-level = 3
[profile.dev.package.curve25519-dalek]
opt-level = 3
[profile.dev.package.hpke]
opt-level = 3
[profile.dev.package.chacha20poly1305]
opt-level = 3
# The WASM compiler, optimised in dev/test builds (2026-09-25): the front's plugin door
# tests hand a real 0.6 MB guest to monetize's watcher, and cranelift at opt-level 0
# compiles it slower than any door can wait for an answer. Only these crates; release is
# unchanged.
[profile.dev.package.cranelift-codegen]
opt-level = 3
[profile.dev.package.cranelift-bforest]
opt-level = 3
[profile.dev.package.cranelift-entity]
opt-level = 3
[profile.dev.package.cranelift-frontend]
opt-level = 3
[profile.dev.package.cranelift-assembler-x64]
opt-level = 3
[profile.dev.package.cranelift-control]
opt-level = 3
[profile.dev.package.cranelift-bitset]
opt-level = 3
[profile.dev.package.regalloc2]
opt-level = 3
[profile.dev.package.wasmparser]
opt-level = 3
[profile.dev.package.wasmtime]
opt-level = 3
[profile.dev.package.wasmtime-environ]
opt-level = 3
[profile.dev.package.wasmtime-internal-cranelift]
opt-level = 3
[profile.dev.package.wasmtime-internal-core]
opt-level = 3