dwarves — fat node, thin UI, lifecycle, uninstall & deploy (operational reference)
⚠ AMENDED 2026-08-03 — the FIVE-ATOM LADDER, and MCP moved into the server
This document describes two binaries. The declaration now has five atoms, and
.nornir/build-variants.tomlis the authority, not this file:
atom role linkage bin dwarves-server⭐server fat dwarves-serverdwarvesclient fat dwarvesdwarves-cliclient thin dwarves-clidwarves-ui⭐client thin dwarves-uidwarves-solo⭐solo fat dwarves-soloTwo changes an operator will actually notice:
dwarves-mcpis gone as a binary. The MCP tools aredwarves::mcpin the library and are served bydwarves-server mcpon stdio. An MCP client config that said"command": "dwarves-mcp"becomes"command": "dwarves-server", "args": ["mcp"]. Nothing about the tools changed.dwarves-solois new — the UI with the forge compiled in. One binary, nothing to install, noDWARVES_SERVERto set.dwarves-uistays thin: "dwarves UI skall ALDRIG default byggas med servern compiled in" is untouched, because solo is a different file, not a different mood.
dwarves-ui-fatanddwarves-defaultwere also removed. The fat-read UI IS the solo shape; the default-features compile guard was never a shippable artifact and that surface is compiled by every plaincargo build -p dwarves/cargo test --workspace.
⚠ AMENDED 2026-08-02 —
dwarves-serverFOLDED INTOdwarves-serverEverything this document calls
dwarves-fullis now thedwarves-serverbin. There is ONE server binary, ONE[[bin]], ONE variant and ONE systemd unit; thedwarves-fullbin, its[[bin]]entry and its build variant are deleted. The occurrences below were renamed in place, because it is the same product under one name.What the fold fixed: the repo held three disagreeing declarations of the same thing —
.nornir/build-variants.tomlcalleddwarves-servera thinrole = "tool"that is "NOT the server",crates/dwarves/src/service.rs(the ONEServiceSpecboth bins installed) called it aFatstate-holding forge, and the live unit was nameddwarves-server.servicewithExecStart=/usr/local/bin/dwarves-full. Rickard settled it: dwarves-server IS the dwarves server — role=server, linkage=fat, state-holding, Skidbladnir compiled in sodwarves-server --installcreates its own unit.The fat/thin axis is now a FEATURE choice on one source, not a second file:
--features embed-all,read-fatis THE server (:7878+:7900, the declared atom);--features serveris the lean forge daemon (:7900only, reaches a remote nornir).The
file:linecitations below are as-of 2026-07-30 and were already drifting; the dated SIZE/mtime measurements are kept as the history they are.
State of the forge's runtime shapes and service lifecycle as of 2026-07-30. Every claim below is cited to file:line + commit hash and was verified against the tree. Related design doc: service-lifecycle-L1 (the skidbladnir service seam); ui-embedding-and-standalone (the thin/fat gRPC split rationale).
1. Two binaries: dwarves-server (THE SERVER, fat) vs dwarves-ui (THIN)
dwarves-server — THE FAT NODE
ONE headless process serving nornir's full gRPC stack on :7878 (via nornir::server_grpc::run, the same entrypoint the standalone nornir-server uses) side-by-side with the forge's own dwarf.v1 on :7900, on one tokio runtime (crates/dwarves/Cargo.toml:35-54, crates/dwarves/src/bin/dwarves-server.rs).
As of commit 4b2e19a ("feat(dwarves): default features embed nornir + skade…", 2026-07-30 00:02) the crate default now includes embed-nornir + embed-skade, so a plain cargo build yields the full binary — no more --features embed-nornir trap:
default = ["cli", "vault", "embed-nornir", "embed-skade"](Cargo.toml:81; was["cli", "vault"]before4b2e19a).embed-nornir = ["dep:nornir", "dep:mimalloc", "server", "grpc-web", "warehouse"](Cargo.toml:299).embed-skade = ["dep:skade"](Cargo.toml:304).- The
dwarves-server[[bin]]declaresrequired-features = ["server"](amended 2026-08-02 by the fold — it was["embed-nornir"]on the retireddwarves-fullbin). The crate default satisfies it, socargo build -p dwarvesbuilds it, and the default also carriesembed-nornir, so the plain build is the FAT shape. - Lean embedders opt out with
--no-default-features/default-features = false(e.g.dwarves-guipullsdwarveswithdefault-features = false, features["facett-adapter","dupes","coverage","mimir"]—dwarves-gui/Cargo.toml:345). - Caveat still in force:
required-featurestargets are silently skipped bycargo build --all-targets; the default now covers the plain-p dwarvesbuild.
dwarves-ui — THE THIN CLIENT (HOST-ONLY)
The [[bin]] dwarves-ui of the dwarves-gui crate (dwarves-gui/Cargo.toml:18-19; renamed from dwarves-gui for a uniform <repo>-ui launch name matching nornir-ui/korp-ui). egui/eframe front-end that knows exactly ONE server address — the dwarves forge (:7900) — and dials nothing else. It embeds NO nornir-server and NO skade, and depends on dwarves lean (default-features = false, Cargo.toml:345).
The suite-host-embedded-auth LAW. nornir is EMBEDDED in the dwarves server (the host); the UI defers to that host and reaches nornir/Mímir data THROUGH the host, never by dialing nornir itself. Concretely:
- The UI reads only
DWARVES_SERVER(or--server <url>). There is NONORNIR_SERVERknob in the client — it does not know and never opens a socket to nornir:7878. The:7878port is for EXTERNAL nornir consumers only. (dwarves-gui/src/main.rsresolves the single forge endpoint;build_native_apptakes NO nornir argument —dwarves-gui/src/lib.rs.) - The Funnel is always EMBEDDED / in-process (renders with nornir-server DOWN, no network dial) — the old
$NORNIR_SERVERthin-funnel path is gone. - SERVED-VIA-HOST (RESOLVED — the warehouse proxy landed). The warehouse/Mímir-backed panes (Warehouse navigator · Data view · Dupes · Coverage · Lenses/Mímir · Binary · Coderoom · the warehouse selector's registry list + Add/Populate) are served THROUGH the host. The dwarves forge now re-serves the
nornir.v1READ surface (Warehouse.{Tables,Scan}·Workspaces.{List,Get,Register,Remove,Fetch,PopulateStatus}· the elevenMimir.*lens folds) on its OWN:7900socket, forwarding each call to its embedded nornir and stamping dwarves' own host bearer on the hop (crate::warehouse_proxy). The single wiring seamwarehouse_endpointinbuild_native_appis now the forge endpoint: the panes'nornir-warehouse-embedthin client speaks the same/nornir.v1.Warehouse/Scan …paths, but to the dwarves host, which proxies. The:7878socket never enters the UI. A dead / embedded-nornir-less host makes a proxied read return an honestUnavailable, so the panes still degrade HONESTLY (never a fabricated empty) — they just never dial:7878. The hop is a localhost gRPC forward, not a literal Rust call (a direct call would needdwarvesto linknornir— a dependency cycle); auth is still host-governed. A genuine in-process reader trait injected throughdwarves-serveris the logged follow-up. - THIN BY DEFAULT is a rule, not a tuning. Startup banner:
dwarves-gui: HOST → dwarves {forge}. Default forge grpc-web endpoint =https://cicd.g11.se:7900(wasm web build). - Size: ~76 MB thin vs ~234 MB fat (the on-disk
dwarves-serveris ~234 MiB — see §5). - Build:
cargo build -p dwarves-gui --bin dwarves-ui. - Run (the ONE launch command):
DWARVES_SERVER=http://oden:7900 dwarves-ui(addWAYLAND_DISPLAY=…/XDG_RUNTIME_DIR=…as your session needs). Do NOT setNORNIR_SERVER— the client ignores it.
2. Lifecycle is Skidbladnir's, not dwarves'
dwarves declares a ServiceSpec and drives a skidbladnir::service::Lifecycle; it does NOT hand-roll unit files or shell systemctl (zero-shell law).
dwarves::service::lifecycle(exec, port, scope) -> Lifecycle=Lifecycle::new(service_spec(...))(crates/dwarves/src/service.rs:202-203).- Unit name is
dwarves-server.service— asserted atservice.rs:740(unit_name_is_dwarves_server_service). - systemd control is native D-Bus via
zbusoverorg.freedesktop.systemd1, the replacement for shellingsystemctl(Skidbladnir/src/systemd.rs:1426, structSystemd { conn: zbus::blocking::Connection }, manager calls atsystemd.rs:1462-1464). - The
["systemctl", "--user"?, <verb>, <unit>]argv inSkidbladnir/src/install.rs:699is a pure offline classifier (classifya verb argv —install.rs:709), NOT a live shell-out. - Bus scope:
Systemd::connect_scopeduseszbus…Connection::session()for user scope,…::system()for system scope (systemd.rs:1445-1450).
3. The uninstall fix — Skidbladnir 9456079 (NOT pushed)
Branch fix/uninstall-data-nuke-bus-optional in /home/rickard/git/Skidbladnir, head 9456079 ("idempotent / self-healing over HALF-REMOVED state…", 2026-07-30 00:23). The branch has no upstream — it is local-only, not pushed (predecessors 998f3ac scope auto-detect, ca131b9 refuse-on-mismatch). All in systemd::remove_systemd_scoped_with (Skidbladnir/src/systemd.rs:1767-1920):
- Auto-detects scope from the installed unit: system =
/etc/systemd/system/<unit>, user =~/.config/systemd/user/<unit>(systemd.rs:1774-1795).--scopeis needed ONLY when a unit exists at both scopes (the tiebreaker); "no system install + running as user" resolves to user scope on its own. - System scope needs root — an informative refusal that erases nothing (
systemd.rs:1801-1810), never an opaque mid-erase EPERM. - Bus required only when a live unit exists to stop: connects the bus only if a unit is present at the resolved scope (
unit_present,systemd.rs:1818-1845). A half-removed state (unit gone, data residue) needs no bus and finishes the erase idempotently — repeated uninstalls converge to clean. A present unit whose bus is unreachable REFUSES and erases nothing (never wrong-scope-delete). - Always keeps KEYS (
.ssh, token) per the uninstall LAW — erase the declared data root, keep the identity; account removal (opt-in) comes last and is skipped if the identity wipe removed nothing (systemd.rs:1868-1918). - Earlier root cause: a
--userscope hitsConnection::session()(systemd.rs:1447), which fails deterministically when there is no login-session bus (service account /sudo) — not random. - dwarves' declared data root =
<base>/.dwarves(DWARVES_DATAoverride), refusing a root that IS an account home (service.rs:19-22,declared_data_root).
4. Deploy / bootstrap on oden
The running server is a SYSTEM unit User=dwarves (+ Group=dwarves; asserted for system scope at service.rs:749/751, and User=root is negated at service.rs:754), warehouse at /home/dwarves/.dwarves/warehouse (resolver ladder $DWARVES_WAREHOUSE → $HOME/.dwarves/warehouse → system /home/dwarves/.dwarves/warehouse — crates/dwarves/src/store.rs:40-41), binary /usr/local/bin/dwarves-server.
- Swapping the binary by hand: stop the service AND kill any stray
dwarves-serverprocess first (otherwisecpfails withText file busy), thencp+ start. - Going forward:
dwarves-server --refresh [bundle]drives the whole build→install→migrate→restart dance as ONE idempotent, rollback-safe call viaskidbladnir::install::refresh_product(service::run_refresh, invoked atcrates/dwarves/src/bin/dwarves-server.rs) — atomic generation swap, no manual cp. - One-time bootstrap done today: the
4b2e19afat binary is live at/usr/local/bin/dwarves-server, mtime 2026-07-30 00:32, size 245,232,848 B (~234 MiB). Backups from prior deploys sit beside it (*.bak-*,*.prev).
5. (RESOLVED by the host-only change) add-workspace no longer dials nornir from the UI
The old bug: in dwarves-ui, Add workspace → Workspaces.Register failed with Unauthenticated: missing or invalid bearer token because the THIN client dialed nornir :7878 directly (token-gated) while carrying no bearer — a direct-nornir client, the very violation the host-only law forbids.
Now (LIVE via the proxy): the UI holds no nornir address and never dials :7878. The warehouse-write affordance (Add/Populate) — and the whole warehouse/Mímir read surface — is served THROUGH the host: the dwarves forge re-serves nornir.v1.Workspaces / nornir.v1.Warehouse / nornir.v1.Mimir on :7900 and forwards each call to its embedded nornir, stamping dwarves' OWN bearer on the hop (crate::warehouse_proxy, the SuiteHost authority — one token, no second ~/.nornir/token). So Add → Workspaces.Register now travels UI → dwarves host → embedded nornir, carrying the dwarves bearer the whole way; the Unauthenticated failure class is gone because there is no thin UI→nornir socket left, and the register is authenticated by the host, not an absent nornir token. Honesty flag: the host→embedded-nornir hop is a localhost gRPC forward (dial URL $NORNIR_SERVER_ADDR, default http://127.0.0.1:7878), not a literal in-process Rust call — a direct call would require dwarves to link nornir (a dependency cycle). A genuine SuiteHost reader trait injected through dwarves-server (where nornir IS linked) is the logged follow-up.
6. Robot-test coverage GAP
The robot-UI hard gate is real — the facett Severity a11y contract (facett/facett-core/src/a11y.rs:49-112; Severity::{Error,Warning,Info}). A low-cost continuous robot-UI test runner for oden was built today, but it lives in the korp repo, NOT dwarves: branch fix/w102-oden-robotui-runner, commit c4601bf ("tools(w102): low-cost continuous robot-UI test runner for oden", 2026-07-29 21:56, in /home/rickard/git/korp). W097/W102 are worklist ids tracked in workspace_nordisk/.nornir/ (not code constants).
The host-only law IS robot-gated (dwarves-gui/tests/robot_host_only.rs): a headless robot builds the SHIPPED shell given ONLY the dwarves host, with NORNIR_SERVER set to a poison :7878 value, and asserts over state_json that the full roster mounts, the Forge dials the dwarves host, and no :7878/nornir address appears anywhere in the shell — the client can never silently dial nornir. The Funnel is asserted embedded (up with nornir DOWN). The former gap — a live end-to-end "served-via-host warehouse read" — is now CLOSED: dwarves-gui/tests/robot_warehouse_proxy_live.rs stands up the REAL dwarves warehouse proxy in front of a mock nornir and drives the SHIPPED shell given ONLY that dwarves host, asserting the Warehouse navigator's world tier renders LIVE rows (synthetic_functions=false, real node/edge counts, real_source_desc names the dwarves host) with no :7878 anywhere. The proxy's own forwarding is proven at the gRPC level by crates/dwarves/tests/warehouse_proxy_live.rs (Warehouse.Scan + Workspaces.Register/List through the host return the upstream's live data; a dead upstream is honestly Unavailable).
6. The ⚒ Build-Thing + Mímir live-demo surface is robot-gated end to end
The panes that fail live (the ⚒ BUILD-THING roster, the 🔍 Lenses, the 🌐 Warehouse navigator) now have honest-state robot coverage on BOTH the display and the EFFECT axes — each with a RED-when-broken peer, so none can go green through an outage:
- Roster, both fixtures.
robot_build_thing_roster_host.rsdrives the SHIPPED host-only shell over a hermetic staged constellation (a temp TOML) — non-empty + honest, plus an EMPTY root that reads as the named "NO CHECKOUTS" state, never blank.robot_build_thing_roster_live.rsdrives the SAME shell over the REALConstellationBuilds::discoverpath on the paved checkout dir ($DWARVES_CONSTELLATION, else the parent of the dwarves repo): on oden that is genuinely non-empty (17 declared repos, ~89 build cells, a real headline); on a bare checkout it is the honest NO CHECKOUTS empty state. Adaptive but TYPED either way — never a false-green blank pane (the t14s bug). - Launch + build EFFECTS, headless.
robot_build_thing_launch_effects.rsselects the newerrdp-korp-thin-containercell (the korp-thin-rdp-viewer target) and clicks ▶ Run with an INJECTEDContainerLauncher: the run FIRES (boot.cell/kind), the readiness line reaches the console, and it endsphase=passed— with a RED peer where the launcher returnsErrand the pane endsfailed, never a fabricated green. It also drives thebootable_only("endast bootbara") filter and asserts the container cell's visibility genuinely FLIPS (hidden ON, shown OFF) while the bootable vm cell stays — a filter that filters — and clicks ▶ Build on a cargo variant to run a REALcargo buildthat resolves to an HONESTgreen=false/"FAILED" report (neverreport=null), so the build effect surfaces the true outcome. No feature flag, no socket, no QEMU — runs in stockcargo test. - Stable error CODES on the failure paths. Every non-UI dwarves error now carries a frozen
dwarves-<subsystem>-<n>code beside its human message (dwarves::errcode, registry at.nornir/dwarves-error-codes.md), so the failure tests assert the CODE and stop being blind to rewording. The recurring three are keyed: an empty roster isdwarves-buildthing-1, a container run that cannot start isdwarves-launch-2, a dead warehouse upstream isdwarves-proxy-2(the proxy's own diagnosis, which travels up into the Lenses pane). Each has a peer asserting the code is ABSENT on the healthy path, so a code can never be a constant that always passes.robot_error_codes.rsproves the code reachesstate_json, a gRPCStatusmessage and thex-dwarves-codemetadata, and re-asserts the registry's wired/reserved split. - Mímir, live + dead.
robot_lenses_live.rsfronts a LIVEMockMimirupstream with the REAL dwarves proxy and drives the shipped host-only Lenses scan:scanned=true, status NOT UNAVAILABLE,thin=true, and the REAL bloat finding (a named monomorphization) survives the proxy round-trip into the pane state. Its twinrobot_warehouse_dead_upstream.rsproves the same pane reportsUNAVAILABLE/scanned=falsewith no fabricated findings on a dead upstream — an outage reads as an outage. The Warehouse navigator has the matching live (robot_warehouse_proxy_live.rs) + dead pair.