⚒ dwarves — how-to guide
📍 THIS DOC LIVES IN
dwarves(moved fromnornir/.nornir/, 2026-07-22, move=delete). The code it documents is dwarves' own (crates/dwarves/src/…), so the doc follows the code. nornir keeps only the read-only viz surface that renders this engine's output. Source links below are dwarves-repo-relative.
Class:
guide· how to use dwarves. What it is:dwarves-design.md. Zero-knowledge:dwarves-for-idiots.md. The forge details:brokkr-guide.md.
dwarves = the brokkr pipeline forge + the sindri scheduler, shipped as a standalone tool AND a library nornir embeds.
Declare a pipeline
A repo declares its CI/build pipeline as data in brokkr.toml (schema: brokkr-guide.md):
[pipeline]
name = "holger-ci"
[[pipeline.trigger]] # fire when the poll sweep sees HEAD move
kind = "poll"
events = ["push"]
[[pipeline.trigger]] # …and nightly
kind = "cron"
cron = "0 3 * * *"
[[pipeline.stage]]
name = "build"
[[pipeline.stage.step]]
name = "compile"
run = { variant = "server" } # real cargo build (zero shell) under --real
[[pipeline.stage]]
name = "test"
[[pipeline.stage.step]]
name = "unit"
needs = ["compile"]
run = { container = { name = "rust:1.85", cmd = ["cargo", "test"] } }
The standalone dwarves tool
cargo build -p dwarves --bin dwarves # build the tool
dwarves # bare: startklar banner + readiness + menu
dwarves doctor # one-shot readiness (store reachable? exit!=0 if not)
dwarves --help # the verbs
dwarves validate # validate ./brokkr.toml
dwarves plan --matrix # topo-ordered step DAG + matrix cells
dwarves run # run through the demo runner, historize
dwarves run --real # `variant` steps build via cargo (zero shell)
dwarves run --matrix # fan over every matrix cell
dwarves trigger push --branch main # which triggers an event would fire
dwarves history --project holger # historized runs (JSONL under ./.nornir)
dwarves badge holger-ci # a status SVG badge for the latest run
dwarves agent ls # the build-agent registry
dwarves tune probe <row> # Heimdall tuner: measure a row's seccomp profiles
dwarves tune approve <row> # record the review (approved.toml)
dwarves tune check <row> [--json] # drift + review gate (exit 1 on findings)
dwarves serve — the scheduler daemon
serve runs the sindri loop: cron → poll-enqueue → drain → dispatch.
# One deterministic tick (demos + CI), injecting a HEAD-move for project "holger":
dwarves serve --once --project holger --poll holger
# sindri tick: 0 cron, 1 enqueued, 1 dispatched (1 green), 0 blocked
# ✓ holger-ci passed — 2/2 steps
# The real daemon: loop every 30s (cron + drain each tick):
dwarves serve --interval 30
Add --json to any verb for machine-readable output.
dwarves tune — the Heimdall tuner verbs
dwarves tune probe|gen|check|approve <artifact> [--workload <driver>], where <artifact> is a row or <repo>:<row>. dwarves tune --help lists the verbs and --json gives the document the UI reads. The README section "Dwarves tune" has the whole workflow.
Callable from nornir
nornir depends on dwarves by path and delegates: the nornir brokkr … verbs run through dwarves's re-exported host wiring (nornir::brokkr::{RealRunner, DemoRunner, run_pipeline_job, store} are now dwarves::…). Nothing about the nornir brokkr surface changed.
nornir brokkr validate
nornir brokkr run --real
nornir brokkr history
Embedded in the server (the running server IS a CI server)
nornir-server's poll sweep drives sindri automatically: when a monitored member that ships a poll-triggered brokkr.toml gets a HEAD-move, the server enqueues, drains and runs its pipeline (sindri_sweep), historizing to the served warehouse and tracking each run as a CI_BUILD job. Watch the server log for nornir-dwarves: <ws> sindri tick: ….
Call it as a library
use dwarves::{Sindri, RegisteredPipeline, MonitorReport, DemoRunner};
let pipeline = dwarves::discover_pipeline(repo_root)?.expect("a brokkr.toml");
let mut sindri = Sindri::new(vec![RegisteredPipeline::new("holger", pipeline)]);
let report = sindri.tick(
chrono::Utc::now(),
Some(&MonitorReport::changed(["holger"])),
&DemoRunner,
None, // Option<&nornir_jobs::JobSink>
warehouse,
);
assert_eq!(report.dispatched.len(), 1);
Watch the UI move
The Brokkr tab drives a pipeline run and lights the DAG stations green:
# Headless robot proof (stations transition dark → green):
cargo test --features viz --test viz_brokkr_robot -- --nocapture
# Watch it live on the desktop:
DISPLAY=:0 cargo run --features viz --bin nornir -- viz # open the ⚒ Brokkr tab
Launch the desktop client (dwarves-ui) — HOST-ONLY
dwarves-ui is a THIN client that knows exactly ONE server address: the dwarves forge. nornir is embedded in the dwarves server (the host); the UI defers to that host and never dials nornir itself — the nornir :7878 port is for EXTERNAL nornir consumers only.
# The ONE launch command — DWARVES_SERVER is the sole knob (no NORNIR_SERVER):
DWARVES_SERVER=http://oden:7900 dwarves-ui
Do NOT set NORNIR_SERVER for the UI — the client ignores it. Warehouse/Mímir panes are served THROUGH the host: the dwarves forge re-serves the nornir.v1 READ surface (Warehouse.{Tables,Scan} · Workspaces.* · the Mimir.* lens folds) on its OWN :7900 socket and forwards each call to its embedded nornir (crate::warehouse_proxy). So the Warehouse navigator · Data view · Dupes · Coverage · 🔍 Lenses panes render LIVE rows through the host with no :7878 anywhere. A standalone (non-fat) server has no embedded nornir, so a proxied read returns an honest Unavailable — never a silent empty, never a direct :7878 socket. (Proven end-to-end by robot_warehouse_proxy_live.rs / robot_lenses_live.rs and their dead-upstream twin robot_warehouse_dead_upstream.rs.)
The ⚒ Build-Thing tab — select & build anything, and watch korp from Windows
The ⚒ Build Thing tab reads every side-by-side checkout's .nornir/build-variants.toml (under $DWARVES_CONSTELLATION, else the parent of the repo root) through build-thing's own model and shows an HONEST roster of build cells + launch cells — never a blank/false-green pane; an absent constellation reads as a named "NO CHECKOUTS" state.
The words this tab uses. Build Thing is the SYSTEM — the declarations plus the gate — and never a name for one artifact. A product is what you click first (gunnar · holger · korp). A suite is a tag (ORIGINAL · MVP) that filters which atoms are real deliverables. An atom is what you click second: ONE declared buildable. Its kind is its shape — exe · service · container · iso · vm · installer. A param is a declared input (--param hub_key=…) and renders as a field in the pane — a kind = "text" param landing as a staged_file (a hub.conf) renders as a MULTI-LINE editor, and a param whose cell declares a fixture ([build.installer.fixtures] hub_conf = "wg-hub") gets an "använd fixtur (wg-hub)" checkbox: ticked, the pane supplies the CLI's own @fixture word and the forge MINTS the fixture at build time (a userspace WireGuard hub, no root, no /etc/wireguard); the result head then prints param hub_conf ← fixture wg-hub (pub=… endpoint=… key=…) beside the receipt. The pane knows no fixture by name — it renders whatever label the recipe declares, and nothing where it declares none. [[build.variant]] stays the TOML section name for the exe/service kinds — it declares one kind of atom, not the concept, which is why the UI says atom and the declaration says variant.
- Build an atom. Pick any declared
ArtifactKind × shape × target × featuresatom and press ▶ Build: it runsnornir_build_thing::build_variant_observedon one named worker thread (never the UI thread) and reports the concrete outcome — artifact path + size, exit status, elapsed, the compiler's diagnostics verbatim on failure, and the zero-egress verdict. - Launch cells + the
bootable_onlyfilter. Container / vm / iso launch cells show under the "endast bootbara" toggle; ▶ Run fires the real launcher and the pane reflects the true phase (passed/failed), never a fabricated green. - Boot cell → ↻ Reboot + 🖥 RDP viewer. A booted
rdp-waylandguest exposes an in-pane RDP viewer (rdp_viewer.status: unavailable → idle → connecting) and a live ↻ Reboot action — no fabricated frame. - Build appliance → watch korp from Windows. Select tunnr → the appliance picker → the
rdp-korp-thin-containercell (the korp-thin-rdp-viewer target: a thinkorp-uiruns as/appinside an RDP desktop, reaching a REMOTE korp-server on oden). ⚒ Build appliance composes the cell's cargo payload layers into one OCI image and reports the two-part honest status —build: staged(real, armed) andboot: needs-runtime— never a faked boot. From Windows:mstsc /v:<host>:3389.
# Point the roster at the paved constellation and open the tab:
DWARVES_CONSTELLATION=$HOME/git DWARVES_SERVER=http://oden:7900 dwarves-ui
Test it
cargo test -p dwarves # the lib (25 tests incl. sindri + cache)
cargo run -p dwarves --bin dwarves -- validate # the standalone tool
cargo test --features viz --test viz_brokkr_robot # the moving robot scenario
cargo build # the whole workspace