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

nordisk/gunnar

crates/gunnar-front/Cargo.toml

[package]
name = "gunnar-front"
description = "vm1's one program: the FRONT medium's PID 1 — the installer's bootstrap listener presenting kind `front`, the estate's WireGuard relay, the twin's certification, the front's signed clock, and the front's keys offered to the installer"
version = "1.5.0"
edition = "2021"
rust-version = "1.85"
license = "AGPL-3.0-only"
publish = false
authors = ["Ignalina ApS", "Vetra Automation AB"]
repository = "https://git.gunnar.rs/nordisk/gunnar"

# **Why this crate exists** (owner 2026-09-25, verbatim: "twin/main gunnar
# share ISO, but NOT the ui/monetize!! BUILD an iso for them using build
# thing!!!"). The estate is two media: main and twin boot the gunnar medium
# (`gunnar-installer-open`/`-metered`, payload `gunnar-server`), and vm1 boots
# the FRONT medium (`gunnar-front-installer-open`, payload THIS binary). The
# front's role used to be a branch inside gunnar-server (lane A 54fa7bd); it is
# here now, and gunnar-server refuses a feed that makes it the front by name.
#
# **What it reuses rather than repeats.** gunnar-server's PID-1 duties — the
# netlink/DHCP bring-up of both NICs, the lease renewal, the orphan reaper, the
# ESP marker a fresh installation clears the pairing state by, the installer's
# listener over its WireGuard link, the tunnel key — are gunnar-server's
# library, linked here with ONLY the features those need (no `replication`,
# no `grow-runtime`, no `metered`). The relay and the certification are
# gunnar-pairing-door's `front::FrontRole`. The clock is edda estate-clock's
# `offer::start` (its `front::run` loop, answering both time dialects). `engine-znippy` is on
# because gunnar-server's library does not compile with no storage engine at
# all; nothing here opens a repository store or binds a git port.

[features]
default = ["dhcp-client", "console"]
# **The web face: the gunnar console** (gunnar-ui-server's router + the built
# console, served on :443 by `web`). The COMMUNITY half: no money code.
console = ["dep:gunnar-ui-server"]
# **The paid front** (`gunnar-front-installer-metered`, the front of
# `gunnar-twin-one`): the console's metered half, the operator's mTLS
# door (the hand-over, the off-site backup stream, the twin's health, the
# bearer's rotation), and F4's `recharge` — the front as the SOLD estate's own
# installer, calling the ONE monetize IN (`shop_door`, the thin SoldEstate
# client over monetize-proto + monetize::sign). **No embedded monetize in any
# build** (owner 13:35Z 2026-10-10, ONE monetize): no monetize-server, no
# plugin door, no self-registration; the estate's boxes call
# monetize.ignalina.dk's doors. tests/one_monetize.rs and
# tests/one_front_pull.rs hold this line.
metered = ["console", "gunnar-ui-server/metered", "dep:tonic", "dep:gunnar-proto", "gunnar-proto/metered", "dep:gunnar-grpc", "dep:gunnar-repair", "dep:prost", "dep:tokio-stream", "dep:monetize", "dep:monetize-proto", "dep:monetize-estate-side", "nordisk-pairing/kept", "dep:toml", "dep:base64", "nordisk-pairing/installer-wireguard", "dep:hyper-rustls"]
# **gunnar.rs's public face** (TASKS-12, the owner's SLIM RULE 2026-10-25):
# llms.txt, robots.txt, the sitemap, the head tags, the public home and the
# public repository pages — gunnar-ui's `gunnar-public-site` crate, mounted by
# the console under its own `public-site`. OFF by default; ON in gunnar.rs's
# release rows `gunnar-front-metered` and `gunnar-front-metered-narrate`
# (gunnar-recipe `tests/public_site_row.rs`).
public-site = ["console", "gunnar-ui-server/public-site"]
# Forge import (AIRGAP, owner rule 2026-10-25): off by default because it dials GitHub
# and GitLab; every front row names it and every front medium sets
# GUNNAR_UI_FORGES=on (crates/gunnar-recipe, tests/airgap_rows.rs).
forges = ["console", "gunnar-ui-server/forges"]
# The console's invite mail (an SMTP relay; AIRGAP): off by default; every front row names it.
mail = ["console", "gunnar-ui-server/mail"]
# Company sign-in at the console (A34-15): the SSO doors and the Identity provider API,
# off by default in gunnar-ui-server; every front row names it, so no live front changes.
sso = ["console", "gunnar-ui-server/sso"]
# The estate's root of time (edda estate-clock's front clock, HTTPS Date sources):
# OFF by default (AIRGAP); without it the front keeps no clock and says so. Every
# front row names it.
clock-fetch = ["estate-clock/fetch", "gunnar-server/clock-fetch"]
# The boot's resolver proof (a DNS question for github.com, AIRGAP B3): every front row names it.
resolver-probe = ["gunnar-server/resolver-probe"]
# ACME (Let's Encrypt, or the fed `acme_directory`) for the front's own HTTPS. OFF by default
# (AIRGAP B1): without it a certificate must be fed. A front built with it also needs
# GUNNAR_FRONT_ACME=on (`web::ACME_ENV`); every front row names it and every front medium
# sets the switch.
acme = ["estate-front/acme"]
# The egress door (an outbound CONNECT proxy for the boxes behind this front, AIRGAP B2):
# OFF by default; it also needs GUNNAR_FRONT_EGRESS=on. Every front row names it and every
# front medium sets the switch.
egress-door = []
# **Gunnar's MCP connector** (TASKS-13, owner 2026-10-25): `/mcp` and its
# OAuth 2.1 server, gunnar-ui's `gunnar-mcp` crate mounted by the console's
# `mcp` feature. OFF by default and in every release row until the owner turns
# it on: the default build has no gunnar-mcp in its tree
# (`tests/mcp_off_by_default.rs`). gunnar-server gets nothing new.
mcp = ["console", "gunnar-ui-server/mcp"]
# DHCP on both NICs (the medium's `GUNNAR_NET=eth0:dhcp,eth1:dhcp`). Off, the
# front takes a static `GUNNAR_NET` only, as gunnar-server does.
dhcp-client = ["gunnar-server/dhcp-client"]
# **DEV-NARRATE** (Skidbladnir nordisk-narrate, owner 2026-09-30): vm1's boot
# log — korp-start, the kernel, this process —
# sealed to the installer's key and streamed over udp/51822 before the
# installer's WireGuard link exists. gunnar-server's hook (`dev_narrate`), the
# same BOOT_LOG. OFF in every release row (the recipe's `dev-narrate` option,
# default off, forbids `nordisk_narrate` when off).
dev-narrate = ["gunnar-server/dev-narrate"]

[[bin]]
name = "gunnar-front"
path = "src/main.rs"

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

# The thin `shop_door` client's words held EQUAL to the shop's (dev-dependencies
# only — monetize-server and monetize-shop-pairing never ship in the front):
# the kept-bundle shape, the seed names, the SoldEstate call names, the feed keys.
[[test]]
name = "shop_pairing_contract"
required-features = ["metered"]

[[test]]
name = "one_sold_door"
required-features = ["metered"]

# The re-admission reads the front's own log back (fd 2 captured into it) and never says it
# again: the sink is the `metered` front's (nordisk-pairing `kept`).
[[test]]
name = "readmit_reads_its_own_log"
required-features = ["metered"]

# The MCP connector against a REAL gunnar-server (gunnar_grpc::router over real
# stores, on loopback): every tool, the private-repo refusal, revoke, the
# allowlist and the rate limit.
[[test]]
name = "mcp_bridge"
required-features = ["mcp"]

# ★ GRS-PUBLIC (live bug 2026-10-28): gunnar.rs's row (`metered,public-site`) against a REAL
# gunnar-server: a site laid before its `public_site` declaration serves its public site to
# anonymous readers once its main takes the ONE box's refeed; the console's paths stay gated.
[[test]]
name = "public_site_refeed"
required-features = ["metered", "public-site"]

# The front's clock exists only with `clock-fetch` (AIRGAP).
[[test]]
name = "clock_both_dialects"
required-features = ["clock-fetch"]

# The UI = CLI hole (A6): the real gunnar CLI (this test binary re-run as
# `gunnar`'s own main, hence no libtest harness) against this front's :443 in
# front of a real control plane. The hole is the console's builds' (`console`).
[[test]]
name = "grpc_hole"
harness = false
required-features = ["console"]

[dependencies]
gunnar-server = { version = "3.0.0", path = "../gunnar-server", default-features = false, features = ["server", "engine-znippy", "mimalloc", "wg-client", "live-app"] }
# edda's PID 1, which gunnar-server runs too (same version, same features: no graph
# change): the stop, the reaper, the interface address, the power-off.
appliance-pid1 = { path = "../../../edda/crates/appliance-pid1", version = "0.1.5", features = ["runtime", "net"] }
gunnar-common = { version = "3.0.0", path = "../gunnar-common", default-features = false, features = ["server", "stderr-capture", "jobs"] }
gunnar-core = { version = "3.0.0", path = "../gunnar-core" }
estate-clock = { version = "0.1.1", default-features = false, features = ["peer", "adjust"] }
gunnar-pairing-door = { path = "../gunnar-pairing-door", version = "1.6.0" }
# `mesh-probe` (P4c, A28 M11): the front's mesh probe is nordisk-pairing's, shared with Holger.
nordisk-pairing = { path = "../../../Skidbladnir/crates/installer/nordisk-pairing", version = "0.5.27", features = ["wireguard", "mesh-probe", "frontside-update"] }
ssh-key = { version = "=0.7.0-rc.11", default-features = false, features = ["ed25519", "std", "rand_core", "encryption"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-util", "net", "sync", "fs", "time", "signal"] }
tracing = { version = "0.1" }
anyhow = { version = "1" }

# ── the front's own HTTPS (`web`): no Caddy on an OS-less front ────────────
#
# rustls over `ring`, named explicitly (`builder_with_provider`) because the
# graph also carries aws-lc-rs and rustls refuses to guess between two
# providers. The ACME client is `instant-acme` (pure Rust, RFC 8555) with its
# `ring` + `rcgen` halves only: NOT its `hyper-rustls` feature, which brings the
# platform verifier — an OS-less front has no trust store to verify against,
# so the client here is hyper-rustls with the compiled-in Mozilla roots.
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
# **The web half is edda's estate-front** (A32-11; it was a fork in `web/`): the certificate, the
# two listeners, the callback-only names, the hand-over. Its ACME client is its `acme` feature,
# which this crate's `acme` turns on (AIRGAP B1: off by default).
estate-front = { path = "../../../edda/crates/estate-front", version = "0.3.1" }
# Optional: under `metered` (the update's presigned fetch).
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "ring", "tls12", "webpki-roots"], optional = true }
webpki-roots = "1"
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "server", "server-auto", "service", "tokio"] }
http = "1"
bytes = "1"
# The update's fetch of a presigned release object (`update::fetch`): the body, collected and capped.
http-body-util = "0.1"
hyper = { version = "1", default-features = false }
# The web router: axum 0.8 is already in this graph under tonic, and the
# console (gunnar-ui-server) hands the front an `axum::Router`.
axum = { version = "0.8", default-features = false, features = ["tokio", "http1", "http2", "query"] }
tower = { version = "0.5", features = ["util"] }
x509-parser = { version = "0.18" }
serde_json = { version = "1" }
blake3 = "1"
# The sha256 the download page shows beside each signed CLI (`downloads`).
sha2 = "0.11"
# The console's BFF, as a library: its `serve::router` is mounted on :443.
# By path: the front is built beside a gunnar-ui checkout (a declared sibling
# of the toolchain container, like edda).
gunnar-ui-server = { path = "../../../gunnar-ui/crates/gunnar-ui-server", optional = true }
rand = { version = "0.10", default-features = false, features = ["std", "sys_rng"] }
serde = { version = "1", features = ["derive"] }
# **The operator door's off-site stream** (`operator_door`, h-forge-offsite): the twin's
# Mirror.LogHead/SnapshotFetch/LogFetch, signed with the console's key (gunnar-grpc's one
# credential signer), framed as protobuf for the operator's copy. All in this lock.
gunnar-proto = { path = "../gunnar-proto", optional = true }
gunnar-grpc = { version = "3.0.0", path = "../gunnar-grpc", optional = true }
prost = { version = "0.14", optional = true }
tokio-stream = { version = "0.1", optional = true }
# The off-site stream is the ONE pump (`recharge_from_twin`) written to frames.
gunnar-repair = { version = "3.0.0", path = "../gunnar-repair", default-features = false, features = ["proto"], optional = true }
tonic = { version = "0.14", default-features = false, features = ["router", "transport", "codegen", "tls-ring"], optional = true }
# ── F4 under ONE monetize: the pull channel's wire and signature ───────────
# The thin SoldEstate client (`shop_door`): the front CALLS the ONE monetize in
# (Recharge/Upgrade/TakeMaterial) and verifies what it signed. These two crates
# are the WIRE of the pull — no server, no plugin host, no dial-in lane.
monetize = { path = "../../../monetize/crates/monetize", optional = true }
monetize-proto = { path = "../../../monetize/crates/monetize-proto", optional = true }
# The ONE SoldEstate client every product's front calls the shop through (P4a, T16; A28 M11):
# monetize-estate-side `sold_door`. Gunnar's own copy (src/shop_door.rs) is the shared one now.
monetize-estate-side = { path = "../../../monetize/crates/monetize-estate-side", default-features = false, features = ["sold-door"], optional = true }
# The fed `shop_door.toml` fragment and the kept bundle's link keys.
toml = { version = "1", optional = true }
base64 = { version = "0.23", optional = true }

[dev-dependencies]
# tests/public_site_refeed.rs: the main's executor for a pulled refeed (gunnar-server
# `repair`, the `replication` half every data box runs; never in the front's own build).
gunnar-server = { version = "3.0.0", path = "../gunnar-server", default-features = false, features = ["server", "engine-znippy", "replication"] }
# estate-front's test certificates (`certs::self_signed`), A32-11.
estate-front = { path = "../../../edda/crates/estate-front", version = "0.3.1", features = ["test-support"] }
gunnar-proto = { path = "../gunnar-proto" }
tempfile = { version = "3" }
rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem", "aws_lc_rs", "x509-parser"] }
time = { version = "0.3", default-features = false, features = ["std"] }
# stop::tests: the stop line is caught as it is said (before the quiesce runs).
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
base64 = "0.23"
sha2 = "0.11"
# tests/one_monetize.rs and tests/one_front_pull.rs read this manifest.
toml = "1"
# ── the contract-parity dev half (nothing of it ships; see [[test]] above) ──
# tests/shop_pairing_contract.rs and src/recharge.rs's cfg(test) helpers hold
# the thin client's words equal to the shop's: the SoldEstate call names, the
# seed file names, the kept-bundle shape, mint_material/plan_of fixtures.
monetize-server = { path = "../../../monetize/crates/monetize-server" }
monetize-shop-pairing = { path = "../../../monetize/crates/monetize-shop-pairing" }
# Gunnar's own facts (its contract with the shop and its sale proof, v2.0 P3): what the
# shop's pairing speaks for a sold Gunnar is read off them, as the ONE box reads its guest.
gunnar-monetize-guest = { path = "../gunnar-monetize-guest" }
# ── tests/mcp_bridge.rs (the `mcp` feature): a real control plane over real
# stores with two planted commits, and a P-256 test passkey. Dev-only, all
# already in this lock's graph through gunnar-server.
gunnar-grpc = { version = "3.0.0", path = "../gunnar-grpc" }
gunnar-store = { path = "../gunnar-store", features = ["conformance"] }
gix-object = "0.66"
gix-hash = { version = "0.28", features = ["sha1", "sha256"] }
p256 = { version = "0.14", default-features = false, features = ["ecdsa"] }
serde_json = "1"
# ── tests/one_sold_door.rs: the shared SoldEstate client signs what the shop verifies.
# (one_order_check.rs and one_sign_in_link.rs, which held Gunnar's copies of the order check
# and the sign-in link to the shared ones, went with the copies, A28 M11, 2026-10-26.)
monetize-estate-side = { path = "../../../monetize/crates/monetize-estate-side", default-features = false, features = ["pump"] }
monetize-embed = { path = "../../../monetize/crates/monetize-embed" }
ed25519-dalek = "3"
# ── tests/grpc_hole.rs (A6): the REAL gunnar CLI's dispatch (`gunnar_cli::run`,
# what the `gunnar` binary's main calls), its control verbs only. Dev-only.
gunnar-cli = { path = "../gunnar-cli", default-features = false, features = ["control"] }

# ── DETACHED WORKSPACE (owner order 2026-10-22) ──────────────────────────────
#
# gunnar-front (vm1's one program: the FRONT medium's PID 1, with the console
# from gunnar-ui and the metered half from open monetize)
# needs crates that live ONLY in sibling checkouts, and cargo refuses to load a
# workspace any of whose members cannot find a path dependency, optional and
# dev edges included. So this crate is its own workspace and the root one, which
# a LONE clone must load, excludes it. What it inherited from the root is
# spelled out above; the profiles and the in-repo patch below are the root's,
# copied, and `xtask/tests/detached_same_as_root.rs` fails if they drift. The
# estate's sibling patches it takes are in its own `[patch.crates-io]` below (G-B7:
# the root takes none, and no cargo config includes them).
# Build it from the repository root with this repo's target dir, as before:
#   cargo build --manifest-path crates/gunnar-front/Cargo.toml --target-dir target …
[workspace]
resolver = "2"

[patch.crates-io]
gix-parallel = { path = "../../vendor/gix-parallel" }
# gunnar-ui-server names gunnar-forge from crates.io ("1.4"); this front links this
# repo's own by path. One copy, not two (and not ureq 2 beside ureq 3), 2026-10-22.
gunnar-forge = { path = "../gunnar-forge" }
# The estate's sibling patches this workspace's lock takes that the root's old
# `.cargo/siblings.toml` include used to supply (G-B7, 2026-10-26: the root takes
# none now, so every estate workspace names its own; xtask/tests/lone_clone.rs).
gunnar-bitmap = { path = "../../../edda/crates/gunnar-bitmap" }
gunnar-sendpack = { path = "../../../edda/crates/gunnar-sendpack" }
gunnar-pktline = { path = "../../../edda/crates/gunnar-pktline" }
nornir-build-thing = { path = "../../../edda/crates/nornir-build-thing" }
git-storage-trait = { path = "../../../znippy/git-storage-trait" }
estate-clock = { path = "../../../edda/crates/estate-clock" }
disk-snapshot = { path = "../../../edda/crates/disk-snapshot" }
appliance-pid1 = { path = "../../../edda/crates/appliance-pid1" }
wg-appliance-core = { path = "../../../edda/crates/wg-appliance-core" }
edda-admit = { path = "../../../edda/crates/admit" }
nornir-auth-oidc = { path = "../../../edda/crates/nornir-auth-oidc" }
nornir-auth-saml = { path = "../../../edda/crates/nornir-auth-saml" }
nornir-auth-webauthn = { path = "../../../edda/crates/nornir-auth-webauthn" }
nornir-identity = { path = "../../../edda/crates/nornir-identity" }
nornir-identity-session = { path = "../../../edda/crates/nornir-identity-session" }
nornir-identity-sso = { path = "../../../edda/crates/nornir-identity-sso" }
nornir-identity-directory = { path = "../../../edda/crates/nornir-identity-directory" }
nornir-auth-sshsig = { path = "../../../edda/crates/nornir-auth-sshsig" }
nornir-service = { path = "../../../edda/crates/nornir-service" }
nordisk-pairing = { path = "../../../Skidbladnir/crates/installer/nordisk-pairing" }
nordisk-narrate = { path = "../../../Skidbladnir/crates/installer/nordisk-narrate" }
korp-installer = { path = "../../../tunnr/crates/korp-installer" }
monetize-product = { path = "../../../monetize/crates/monetize-product" }
monetize-embed = { path = "../../../monetize/crates/monetize-embed" }
monetize-proto = { path = "../../../monetize/crates/monetize-proto" }
monetize-estate-side = { path = "../../../monetize/crates/monetize-estate-side" }

[profile.release]
lto = "thin"
codegen-units = 1
strip = "debuginfo"

[profile.dev-media]
inherits = "release"
lto = false
codegen-units = 16

[profile.test]
debug = "line-tables-only"

[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

[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