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

nordisk/gunnar

crates/gunnar-cli/Cargo.toml

[package]
name = "gunnar-cli"
description = "Gunnar's command-line tool: `cargo install gunnar-cli`, then `gunnar-cli control …` against a Gunnar server, and git with encrypted (quantum-safe) repositories (`gunnar-cli crypt …`)."
readme = "README.md"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
# A product (lane GUN-PRODUCTS, 2026-10-30): `cargo install gunnar-cli` from crates.io.
publish = true
authors.workspace = true
repository.workspace = true

# ── ONE PROGRAM, `gunnar-cli` (lane GUN-PRODUCTS, owner 2026-10-30) ──────────────────
#
# Gunnar's three products are one crate each: `gunnar` (the server, `crates/gunnar`),
# `gunnar-ui` (the console, the gunnar-ui repository) and this one, the command-line tool.
# `cargo install gunnar-cli` installs exactly one program, `gunnar-cli`: git with the
# encrypted repositories (`clone · push · crypt …`) and the control plane
# (`gunnar-cli control …`).
#
# This crate had FIVE bins over one `main.rs` until 2026-10-30, and its `gunnar` bin (the
# client-only git command) landed on the same `~/.cargo/bin/gunnar` the server crate
# installs. The old names, and what they are now (README.md has the table):
#
#   gunnar           → `gunnar-cli` (every verb it had; `control` on top)
#   gunnar-solo      → `cargo install gunnar-cli --features server` (both halves in one)
#   gunnar-service   → `cargo install gunnar-cli --features systemd` (the server, systemd's)
#
# A bin name is a FILE PATH, so a build with different features is a different artifact
# only under a different target directory or bin name: the one estate build that needs a
# second resolution of the same program keeps its own bin name below.
#
# No `required-features`: every shape of the program is this one bin — the git client
# (`--no-default-features --features client`), the product (`client,control`, the default),
# both halves (`--features server`) and the systemd service (`--features systemd`).
[[bin]]
name = "gunnar-cli"
path = "src/main.rs"

# `gunnar-cli-metered` — the estate's: the SAME program with the money half of the command
# line compiled in (`gunnar-cli control appliance-identity`, the key an appliance signs actor
# tickets with). The front of a METERED estate carries it (row `gunnar-cli-musl-metered`).
# Its own bin name, because a bin name is a file path: two rows naming one path are one
# artifact written twice. `cargo install gunnar-cli` never installs it (`metered` is no
# default and no extra).
[[bin]]
name = "gunnar-cli-metered"
path = "src/main.rs"
required-features = ["client", "control", "metered"]

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

[features]
# AIRGAP (owner rule 2026-10-25: the default build is the airgap install), and the product
# `cargo install gunnar-cli` (lane GUN-PRODUCTS, 2026-10-30): the git client and the control
# plane. Neither dials anything but the server or the remote a person names; the outbound
# surfaces beyond that (the console login over HTTPS, the other forges) are the named
# extras `online` and `federation` below. An appliance image builds
# `--no-default-features --features server` and links no client code at all; the tests turn
# the rest on through the dev-dependency on this crate below (`tests/feature_split.rs`
# asserts the split from `cargo tree`).
default = ["client", "control"]

server = [
    # The server half is its own crate now; this feature just pulls it in and
    # forwards the shared CLI's server verbs. Every server dependency (gunnar-store,
    # gunnar-ssh, gunnar-wire, gix-*, tokio, hyper, rustix, …) moved to
    # `crates/gunnar-server/Cargo.toml`.
    "dep:gunnar-server",
    # The server marker: company SSO (OIDC + SAML) with it, as before 2026-10-22.
    "gunnar-server/server",
    # The appliance halves gunnar-server no longer takes by default (AIRGAP, owner
    # rule 2026-10-25): a `--features server` gunnar-cli (the old gunnar-solo) keeps them.
    "gunnar-server/dhcp-client",
    "gunnar-server/wg-client",
    "gunnar-server/clock-fetch",
    "gunnar-server/resolver-probe",
    "gunnar-common/server",
    # **The storage engine, named out loud.** It is `gunnar-server`'s default
    # too, so this line changes no resolved graph today. It is here because a
    # server binary that silently carries whichever engine another crate's
    # defaults happened to select is the failure mode this whole seam was built
    # to remove — `gunnar-znippy`'s manifest said the same thing about
    # `gunnar-cli`'s defaults and was right. `gunnar-server` refuses to compile
    # with no engine at all, so an accidental `default-features = false` on the
    # edge below becomes a build error rather than a server that starts and
    # stores nothing.
    "gunnar-server/engine-znippy",
    # **Replication, and it ships.** Decided 2026-08-30, after a drill
    # measured the feature working end to end (60 ms lag; after `SIGKILL` of
    # the primary the standby served 5/5 repositories fsck-clean) and NOT
    # compiled into the appliance.
    #
    # It is on the `server` feature and not on `client` because replicating is a
    # SERVER's job — a forge that keeps a second copy of itself — and because
    # `xtask guard`'s `appliance` profile resolves exactly this feature set. If
    # the appliance ships it, the graph the guard checks has to contain it, or
    # the guard is checking a shape nobody builds.
    #
    # It is the NARROW half: `gunnar-server/replication` takes `gunnar-client`
    # with `default-features = false`, which is +4 packages against the +128 the
    # all-or-nothing `client` feature cost. See that feature's own comment.
    "gunnar-server/replication",
    # **Outbound webhooks, and they ship.** Same home as replication and
    # for the same reason: delivering events to an external URL is a SERVER's
    # job, and `gunnar control webhook add/list/remove` are only useful against a
    # server that actually runs the delivery. The outbound HTTP client rides the
    # hyper already in the graph (http://) plus tokio-rustls for https://.
    "gunnar-server/webhooks",
]

# **The gix arm (a named extra), for a `gunnar-cli --features server` that can A/B both
# engines from one binary.**
#
# Additive on top of `server`, never instead of it: with both compiled the
# selection is at runtime (`GUNNAR_ENGINE`) and the default is still znippy. Off
# by default and absent from every declared atom, so no shipped binary pays for
# it — a `--features server,engine-gix` build opts in by hand when someone is measuring.
engine-gix = ["gunnar-server?/engine-gix"]

# **THE GIT CLIENT, and nothing else.** `clone · fetch · push · log · status ·
# commit · branch · tag` — what a person types instead of `git`.
#
# ⚠ It said `client = [… , "control"]` until 2026-08-05, and dropping that one
# word is what makes the `gunnar` atom possible. `control` pulls tonic's
# `channel`, `gunnar-proto` and `gunnar-grpc`; while it rode in here, EVERY
# client build paid for a gRPC stack whether or not it ever spoke to a control
# plane, and there was no way to declare a git client that did not. The two are
# different jobs — talking git to a remote, and administering a server — so they
# are now two features and two atoms, and the cost of the second one is a
# measured number instead of an assumption:
#
#   --features client            332 crates / 326 third-party   the git client
#   --features client,control    348 crates / 340 third-party   + the control plane
#
# **+16 packages, and they are named rather than counted**: `tonic`, `prost`,
# `prost-derive`, `axum`, `axum-core`, `matchit`, `hyper-timeout`, `tokio-stream`,
# `async-trait`, `httpdate`, `pin-project`, `pin-project-internal`, `itertools`,
# `either`, plus gunnar's own `gunnar-proto` and `gunnar-grpc`.
#
# ⚠ AND WHAT IT IS NOT. `h2`, `hyper`, `tower` and `tokio` are in BOTH graphs —
# they arrive through `reqwest` under `nornir-git`, which is git-client work — so
# the honest claim for the `gunnar` atom is *no tonic, no prost, no axum, no
# gunnar-grpc*, not "no async HTTP stack". MEASURED 2026-08-05,
# `cargo tree -e normal -p gunnar-cli`, unique package names, both directions.
#
# Nothing implies `control` any more; `default` names it, and the default graph
# is unchanged at 370.
#
# `gunnar crypt` (encrypted repositories, 2026-10-05) is a client verb: `gunnar-crypt` with everything (age wrap, envelope, diff) and
# `rpassword` for an encrypted SSH key's passphrase.
client = ["dep:gunnar-client", "gunnar-client/porcelain", "dep:gix", "gunnar-common/client", "gunnar-server?/client", "dep:gunnar-crypt", "dep:rpassword", "dep:tempfile", "dep:p256", "dep:aes", "dep:cbc", "dep:hmac", "dep:sha2"]

# `gunnar control …`: the CLIENT half of the gRPC control plane, which is
# what makes a first boot possible from a laptop — `gunnar control bootstrap`
# is the one call that needs no credential, and it needs a gRPC CLIENT to make
# it.
#
# Independent of BOTH halves since 2026-08-05 (see `client` above), and not
# implied by `server` either, so the appliance image
# (`--no-default-features --features server`) resolves exactly the graph it did
# before: `xtask guard`'s `appliance` profile is the witness, and it is
# unchanged at 281 crates. What it costs a client build is `gunnar-proto`,
# `gunnar-grpc` and tonic's `channel` feature.
#
# `gunnar-grpc` rather than a hand-rolled signer: `sign_credential` and the
# server's verifier are the SAME function in the SAME file, which is the only
# way a change to the credential format cannot land on one side only (one
# implementation, and `gunnar-grpc/src/auth.rs` says so where it defines the
# wire format).
control = [
    "dep:gunnar-core",
    "dep:gunnar-proto",
    "dep:gunnar-grpc",
    # `control twin catch-up` drains the twin's LogFetch stream; already in the
    # graph via gunnar-grpc.
    "dep:tokio-stream",
    # `control twin replay` is a thin caller of gunnar-repair's recharge_from_twin:
    # the ONE pump (poisoned body on a broken fetch, resumable base) behind the
    # console's button, the boot check and a pulled monetize order (5.4).
    "dep:gunnar-repair",
    # `control mirror import` (the other forges) is `federation`'s, and `gunnar login` (the
    # console over HTTPS) is `online`'s: the named extras below (lane GUN-PRODUCTS).
    "dep:tonic",
    "tonic/channel",
    # `--at https://<site>` through a site's front (the hole, A6): the site's
    # certificate is checked against the compiled-in Mozilla roots. webpki-roots
    # is already in this graph (ureq, below).
    "tonic/tls-webpki-roots",
    "dep:tokio",
    "dep:ssh-key",
    "dep:rand",
    # The shared CLI's control verbs (`GrantLevel`/`Visibility`/`HashKind`).
    "gunnar-common/control",
]

# Smart HTTP, the second git transport. **OFF by default**, the same
# shape as `systemd`, so an appliance image that does not want an HTTP surface
# compiles none of it.
#
# The cost is measured rather than assumed: hyper, hyper-util, http-body-util,
# http, bytes and base64 are ALREADY in the appliance graph because tonic pulls
# them in for gRPC, so `--features server,http` resolves the same crate count as
# `--features server`. `xtask`'s dependency guard asserts that from `cargo tree`
# on both feature sets, and `TRACKED_DEVIATIONS` still records `axum` as tonic's
# and not gunnar's: this transport uses hyper directly and registers no router.
http = ["gunnar-server/http", "gunnar-common/http"]
# **The usage share** on the admission gate (`edda_admit::UsageShare`) and its
# serve flags `--admit-limit` / `--admit-window-days`. `?` for `metered`'s
# reason: a client-only build must not be dragged into linking the server.
admit-share = ["gunnar-server?/admit-share", "gunnar-common/admit-share"]

# **The metered appliance** — a licensed cargo feature. Forwarded the way `http`
# is — its own feature, NOT a member of `server` — because the developer's
# default `gunnar` binary is the community one: a self-hoster building from
# source must get plan enforcement only by asking for it by name. The
# build-thing rows `gunnar-server-metered{,-thin}` build `-p gunnar-server`
# directly; this forwarding is for `cargo test -p gunnar-cli --features
# server,metered` and the developer binary.
metered = [
    # The SERVER's metered half, and only if a server is being built at all.
    # The `?` is not cosmetic: `gunnar-server/metered` (no `?`) ENABLES the
    # optional `gunnar-server` dependency, so `--features client,control,metered`
    # silently linked the whole appliance into a client binary — which is why the
    # front's operator CLI could not be given this feature and shipped the
    # verb-less build instead.
    "gunnar-server?/metered",
    # The money half of the shared COMMAND LINE: `gunnar control
    # appliance-identity` is `#[cfg(feature = "metered")]` in
    # `gunnar-common::cli`, and without this forwarding the verb does not exist
    # however the binary is built.
    "gunnar-common/metered",
    # `billing.proto` is not in codegen without it, and the verb calls
    # `BillingClient`. `?` for the same reason as above: control already turns
    # `gunnar-proto` on, and a client build that does not want it must not gain
    # it here.
    "gunnar-proto?/metered",
    # `gunnar control entitlement set` reads monetize's OWN `EntitlementFact`
    # off a file, so the fact pushed is the value monetize signed and not a
    # second shape of it. `monetize-product` is serde and nothing else — no key,
    # no verifier, no signer: this verb signs nothing. Already in the lock via
    # `monetize-embed`; no package is added.
    "dep:monetize-product",
]

# The `systemd` integration. OFF by default, and the appliance build never pays for it.
# Pinned to the published 0.1.12: 0.1.13 depends on the unpublished
# `nornir-escape` and cannot be resolved from a registry-only clone.
#
# ⚠ It IMPLIES `server`, added 2026-08-04, and the reason is not a compile fix.
# The unit this feature installs renders `ExecStart=<bin> serve --ssh-bind …`, so
# `--features systemd` without `server` declares a service for a verb the binary
# does not have: it built, installed, and produced a unit that could only ever
# fail at `ExecStart`. The `gunnar-service` cell in `.nornir/build-variants.toml`
# says `server,systemd` and always did; this makes the combination the ONLY one
# the feature can be built in, rather than a convention the declaration follows.
#
# It changes no graph the guard measures — `server` is already on in the
# `service` profile — and it does not put `skidbladnir` anywhere new: implication
# runs systemd ⇒ server, never the reverse, which is exactly what
# `tests/feature_split.rs::the_systemd_feature_is_not_paid_for_unless_it_is_on`
# asserts and what the appliance profile's `skidbladnir` ban enforces.
# `dep:gunnar-core` is NOT decoration. `src/service.rs` is gated on THIS feature
# and calls `gunnar_core::observe::service_log_filter`, but the only other line
# naming `dep:gunnar-core` is under `control` — so `--no-default-features
# --features server,systemd` resolved without it and the crate did not compile:
#
#     error[E0433]: cannot find module or crate `gunnar_core`
#       --> crates/gunnar-cli/src/service.rs:150:25
#
# That feature set is the DECLARED `gunnar-service` cell, and it is also what
# `installtest` builds the subject with — so the break took out
# `install_container`, `install_iso` and `install_kvm`, and then every arm that
# needs a freshly built subject binary, which failed instead on the staleness
# guard and looked like a different problem. Costs nothing to resolve: `server`
# already pulls `gunnar-server`, which names `gunnar-core` itself.
systemd = [
    "dep:skidbladnir",
    "dep:gunnar-core",
    "server",
    "gunnar-common/systemd",
]

# ── The named extras (the same on every Gunnar product: gunnar, gunnar-ui, gunnar-cli) ──
# Each is off by default; `full` is all of them. The public site and the MCP connector are
# the console's (`gunnar-ui`); a command-line tool serves neither, so they add nothing here.
public-site = []
mcp = []
# `engine-gix` (above) is the third extra: the gix storage engine beside znippy, for a
# `gunnar-cli --features server` (both halves in one program).
# `online`: `gunnar-cli login <console>`, the browser device flow, asks the console over
# HTTPS (ureq with rustls; 5.4 §3b).
online = ["control", "dep:ureq"]
# `federation`: `gunnar-cli control mirror import --from github|codeberg|gitlab…` lists the
# other forge's repositories over its REST API with a personal access token (`gunnar-forge`,
# outbound HTTPS). It comes with `online`.
federation = ["online", "dep:gunnar-forge"]
full = ["public-site", "mcp", "engine-gix", "online", "federation"]

# ── there is no `znippy` feature here, and the reason changed ───────────────
#
# There used to be `znippy = ["dep:the old znippy engine crate", "server"]`, removed
# 2026-08-05 because the old znippy engine crate reached znippy by PATH and cargo reads a
# path dependency's manifest at RESOLVE time whether or not the feature
# selecting it is on. One optional line made a sibling `../znippy`
# checkout a hard requirement of every gunnar build. The capability then lived
# in a detached crate, `crates/gunnar-znippy`, which installed an engine into a
# runtime `OnceLock` and emitted its own `gunnar` and `gunnar-server` binaries —
# two names the build declaration had already assigned elsewhere.
#
# **Both of those are gone as of 2026-08-10.** `crates/gunnar-znippy` is
# deleted; the engine is `gunnar-server`'s `engine-znippy` feature, forwarded
# from `server` above. There is nothing to name here because there is nothing
# optional left: a `gunnar-cli` with `server` on has an engine, by construction.
#
# The C-toolchain cost did not go away — it was **paid**. `gunnar-server` now names
# `git-storage-trait` and `znippy-plugin-git` by path, so a sibling znippy
# checkout is a hard requirement of the whole workspace. That was affordable
# only because the property it costs was already gone: `../edda` has been a
# hard resolve-time requirement since `gunnar-pack` moved there, MEASURED
# 2026-08-10 and failing identically. See `crates/gunnar-server/Cargo.toml` for
# the command and its output.

[dependencies]
# `control twin replay` (see the `control` feature). Already in the graph.
tokio-stream = { workspace = true, optional = true }
anyhow.workspace = true
# `env` is added here rather than in the workspace entry: only this crate reads
# `GUNNAR_DATA_ROOT` / `GUNNAR_MIRROR_ROOT` off the environment.
clap = { workspace = true, features = ["env"] }
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

# The shared half: the `Command` line, the logging ladder and the build
# identity, which used to live in this crate's own `cli.rs` / `logging.rs`.
# Always linked (every bin parses a command line and logs); its optional deps
# are gated by the same server/client/control features forwarded below, so a
# client build still links no gunnar-wire and the appliance no gunnar-proto.
gunnar-common = { workspace = true, default-features = false }

# ── the server half — its own crate now ─────────────────────────────────────
# Everything the appliance links (gunnar-store, gunnar-ssh, gunnar-wire,
# gunnar-lfs, gunnar-pack, gunnar-policy, gunnar-http, gix-*, tokio-util,
# tokio-stream, rustix, hyper*, http-body-util, bytes) moved to
# `crates/gunnar-server`. This one optional edge, under the `server` feature, is
# all that is left of it here.
gunnar-server = { workspace = true, optional = true }

# ── shared by the control and client halves ─────────────────────────────────
# `control.rs` and `client.rs` name gunnar-core (fingerprints, grants, repo
# refs, hash kinds) and the gRPC stack, so these are NOT server-only.
gunnar-core = { workspace = true, optional = true }
gunnar-proto = { workspace = true, optional = true }
gunnar-grpc = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }

# ── client half ─────────────────────────────────────────────────────────────
gunnar-client = { workspace = true, optional = true }
gunnar-crypt = { workspace = true, optional = true, features = ["wrap", "diff"] }
# The crypt filter spools a large file to an unnamed 0600 file in the clone's
# git dir (git hands a filter the whole file before it reads an answer).
# Already in the client graph through gix-odb.
tempfile = { workspace = true, optional = true }
rpassword = { version = "7.5", optional = true }
# `gunnar crypt unlock --security-key` (CRYPT-YUBI): a FIDO2 key's hmac-secret over
# CTAP 2.1, PIN/UV auth protocol one — P-256 ECDH, AES-256-CBC and HMAC-SHA256,
# the RustCrypto generation already in the lock. Message glue only (src/fido2.rs).
p256 = { version = "0.14", optional = true, default-features = false, features = ["ecdh", "arithmetic"] }
aes = { version = "0.9", optional = true }
cbc = { version = "0.2", optional = true }
hmac = { version = "0.13", optional = true }
sha2 = { version = "0.11", optional = true }
gix = { workspace = true, optional = true }

# ── the `control` feature: the gRPC client ──────────────────────────────────
# The same `ssh-key` gunnar-grpc signs with, so the credential this CLI produces
# and the one the server verifies are made by one function over one key type.
tonic = { workspace = true, optional = true }
gunnar-repair = { workspace = true, optional = true, features = ["proto"] }
ssh-key = { workspace = true, optional = true }
ureq = { version = "3", default-features = false, features = ["rustls"], optional = true }
rand = { workspace = true, optional = true }
# `control mirror import`'s LIST half: "what repositories does this account
# have", which git cannot answer. Optional and reached only from the `control`
# feature above, which is the whole of what keeps a forge REST client and a
# user's token off the appliance.
gunnar-forge = { workspace = true, optional = true }

# ── the `systemd` feature ───────────────────────────────────────────────────
# No path dep on ../Skidbladnir: gunnar must build standalone from a fresh
# clone, the same rule that removed the `nornir-git` patch block (reuse, not vendoring).
skidbladnir = { version = "0.2", default-features = false, optional = true }
monetize-product = { version = "0.3.0", optional = 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, not out of anything
# anyone typed twice — and emits the `#[used]
# #[link_section = ".nordisk.buildthing"]` static that `src/main.rs` includes.
#
# A BUILD dependency and never a normal one. The receipt is a few hundred bytes
# of data; pulling `nornir-build-thing`'s serde_json / toml / object /
# cargo_metadata stack into the graph of a binary that is meant to be able to be
# PID 1 would be paying for a compiler to ship a string. `cargo tree` after this
# edge resolves the appliance profile to the same crates it did before, which
# `cargo xtask guard` asserts.
# The RECIPE, not the cdylib shim: this is a `[build-dependencies]`, and it is
# exactly the edge whose collision with the normal one made `cargo build
# --workspace` red 6 of 8 from cold. See the root manifest.
gunnar-recipe.workspace = true

[dev-dependencies]
tempfile.workspace = true
# A real P-256 passkey for the web-credential verbs' test (Stage L).
p256 = { version = "0.14", default-features = false, features = ["ecdsa", "ecdh"] }
# The software FIDO2 authenticator in `tests/fido2_soft.rs` (CRYPT-YUBI).
sha2 = "0.11"
# `tests/bootstrap.rs` records a clock-sync report and reads it back off the
# credential-free `Bootstrap.Status` (2026-09-14).
estate-clock = { workspace = true }

# `tests/mtls_handshake.rs` mints a **second, unrelated** certificate authority.
# It has to be built outside `gunnar_core::Pki`: the arm it feeds asserts that a
# certificate is refused for its PROVENANCE, and material minted through
# gunnar's own issuer would be gunnar-shaped no matter what CA signed it. A
# dev-dependency, so the graph `xtask guard` measures (`cargo tree -e normal`)
# is unchanged and no shipped binary links it.
rcgen = { workspace = true }

# **This crate, on itself, to compile the second storage engine into the test
# binaries.** The same shape `gunnar-core` uses for its `conformance` feature.
#
# `tests/replication.rs` needs the `gix` engine: replication's send-pack opens the
# primary as a filesystem git repository, which the shipped znippy archive is
# not. It also turns on `server` and `control`, which left the default (AIRGAP), so the
# test binaries keep the whole CLI. A dev-dependency, so no shipped binary grows the second engine — the
# appliance still carries exactly one, and `tests/feature_split.rs` still says so.
gunnar-cli = { path = ".", features = ["engine-gix", "server", "control", "full"] }

# ── server-half crates the tests reach directly ─────────────────────────────
# The server suite (bootstrap, wire_config, push_events, observability, …) lives
# here and drives the server through `gunnar_cli::server` (re-exported from the
# gunnar-server crate) but also names these plumbing crates directly. They moved
# to `crates/gunnar-server`'s NORMAL dependencies; as DEV-dependencies here they
# are available to the tests and reach neither the library nor any shipped
# binary — `cargo tree -e normal` (every graph `xtask guard` measures) is
# unchanged, and the appliance never links a test.
# PATH-ONLY (no `workspace = true`, no version), all eight: the lib has no
# normal dependency on any of these, and a dev-only sibling edge carrying a
# version makes this crate publishable only in an order no manifest states —
# `dwarves workspace-release` reads normal dependencies alone, so its wave
# order cannot account for the edge. `tests/publish_order.rs`
# (`no_sibling_dev_only_dependency_carries_a_version`) is the guard that
# demands this shape; the paths resolve to the same trees the workspace table
# names, so nothing about what compiles changes.
gunnar-store = { path = "../gunnar-store", features = ["conformance"] }
gunnar-ssh = { path = "../gunnar-ssh" }
gunnar-wire = { path = "../gunnar-wire" }
gunnar-lfs = { path = "../gunnar-lfs", features = ["fs"] }
# PATH ONLY, no `version`, for all three edda siblings below — see
# `xtask/tests/publish_order.rs`. They are dev-only here (no normal edge), so a
# version requirement is an ordering constraint `dwarves workspace-release`
# cannot see, and Gate B refused a 17-crate cascade over that shape on
# 2026-08-05. Cargo strips a path-only dev-dependency from the published
# manifest, so the order cannot be wrong about what is not there.
gunnar-pack = { version = "3.0.0" }
# `default-features = false` restated by hand. It came in with `workspace =
# true` and dropping the version drops the inheritance with it, so leaving it
# off here would have quietly turned this crate's default features back on for
# the test build only — a difference between what the tests link and what ships.
gunnar-sendpack = { version = "3.0.0", default-features = false }
gunnar-policy = { path = "../gunnar-policy" }
gunnar-http = { path = "../gunnar-http" }
gix-hash = { workspace = true }
gix-object = { workspace = true }
gix-pack = { workspace = true }
gix-parallel = { workspace = true }
gix-utils = { workspace = true }
rustix = { workspace = true }
tokio-stream = { workspace = true }
hyper = { version = "1", default-features = false, features = ["server", "http1"] }
bytes = { version = "1", default-features = false, features = ["std"] }
# `tests/wire_config.rs` frames one LFS conversation to assert the quota flags
# are applied. The framer is gunnar's own and shared; hand-rolling a second
# 4-hex length prefix in a test is precisely the twin one implementation forbids.
# By PATH and not `workspace = true`: a workspace inherit carries a `version`,
# and `cargo package` then resolves this dev-edge against the registry, which
# makes gunnar-cli publishable only in an order no manifest states. See
# `xtask/tests/publish_order.rs`.
gunnar-pktline = { version = "1.1.0" }
# The HTTP suite writes its own HTTP/1.1 requests by hand (see
# `tests/http_transport.rs`) and needs base64 for one Authorization header. The
# same crate `gunnar-core` already encodes fingerprints with.
base64ct = { version = "1", features = ["alloc"] }
# The end-to-end tests drive a real `gunnar serve` and then a real `git` binary
# against it, so they need a runtime and the stores to set the fixture up. Both
# are already regular dependencies under the `server` feature; naming them here
# only makes them available when the feature is off, and dev-dependencies never
# reach the appliance build.
tokio = { workspace = true }
# Only to build a channel: gunnar-proto generates its clients without tonic's
# `channel` feature, so `Client::connect` does not exist.
tonic = { workspace = true }
# `tests/thin_client.rs`: the SHARED `LinkageAudit` + `THIN_FORBIDDEN_SYMBOLS`
# that check `.nornir/build-variants.toml`'s `linkage = "thin" | "fat"` against
# the produced BINARY. dwarves and holger are held to the same audit; writing a
# second symbol reader here would be the twin one implementation forbids, and
# the shared one already carries the trap a hand-rolled `grep -c` falls into
# (it counts DEMANGLED symbols and reclassifies compiler destructor glue).
#
# Through the WORKSPACE entry, which is a PATH dep on the sibling edda checkout
# since 2026-08-30 — and this line said the opposite until then ("by
# REGISTRY version and not by a sibling path … this repository must build in a
# checkout with nothing beside it"). That claim was already false when it was
# written: `nornir-git` two entries up in the root manifest, and `gunnar-pack`
# before it, are path deps, so `../edda` has been a hard requirement of every
# build here for weeks. Naming a REGISTRY version here while the root names a
# PATH is not neutrality — cargo resolves those as two different packages, which
# is how dwarves b81545c got `E0609: no field dsse`. One entry, one copy.
#
# It is a dev-dependency, so `cargo tree -e normal` — which is every graph
# `xtask/src/guard.rs` measures, including the 281-crate appliance — is unchanged,
# and none of it can reach a shipped artifact.
nornir-build-thing = { workspace = true }