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

nordisk/nornir

.nornir/running-as-a-server.md

Running nornir as a server

nornir runs embedded (the CLI opens the warehouse in-process — the default) or as a long-running server that the CLI talks to over gRPC. Same verbs either way. The two are mutually exclusive per warehouse — one process owns the redb/iceberg write lock (see design.md).

Verified end-to-end on 2026-06-07 against workspace_holger (3 repos).

Install / build

server is a default feature, so a plain install is functional out of the box:

cargo install nornir                 # installs `nornir` + `nornir-server`; client mode works
cargo install nornir --features cli  # + MCP (nornir-mcp) + docs PDF/HTML (docs-export)
cargo install nornir --features cli,embed-ort  # + semantic search embedder, CUDA→CPU
                                               #   (GPU needs cuDNN discoverable — see vector.md)

From source: cargo build already includes the server (→ target/debug/{nornir, nornir-server}). Library/bench consumers that want nornir lean use nornir = { …, default-features = false }.

The server is just an authenticated port (for idiots)

Read this first; the rest is detail.

The whole flow at a glance

Start the server

One command — nornir serve execs nornir-server for you. The only things it needs are a token and (optionally) a bind address:

NORNIR_SERVER_TOKEN=<≥16-char-secret> \
  nornir-server --addr 127.0.0.1:7878

The server has no boot config file. It does NOT read a nornir.toml, and there is no --config / NORNIR_CONFIG / NORNIR_WORKSPACES / --monitor / --poll / --warehouse ingress (all removed). Workspaces come ONLY from the redb registry at <home>/.nornir/workspaces/registry.redb, mutated at runtime via nornir workspace register (→ the Workspaces.Register RPC). A server with an empty registry (zero workspaces) is a valid resting state: it starts cleanly and serves 0.

On a fresh box it prints:

nornir-server: serving 0 workspace(s) (empty registry — register one with `nornir workspace register <name> --descriptor <toml>`)
nornir-server: listening on 127.0.0.1:7878

…and once you've registered workspaces (which persist in the registry across restarts) it re-opens and serves them:

nornir-server: serving monitored workspace `njord` (builds/ warehouse)
nornir-server: serving 1 workspace(s); default `njord`
nornir-server: listening on 127.0.0.1:7878

The entire server config surface is two flags, each with a matching env-var fallback — precedence --flag > env > default — so a deployed systemd unit (env) and a hand-run binary (flags) both work:

flag env default
--token NORNIR_SERVER_TOKEN <home>/.nornir/token (required, ≥16 chars)
--addr NORNIR_SERVER_ADDR 127.0.0.1:7878 (loopback)

That is all the server reads at boot — bind + auth, nothing else. There is no workspace/warehouse/poll boot flag. The warehouse is $HOME-derived per-workspace at <home>/.nornir/workspaces/<name>/builds/warehouse (no override flag), and the poll cadence is the per-workspace registry poll field set at register time (the server's monitor loop ticks at a fixed 60s default).

Add a workspace at runtime (this is how the server learns anything)

The server is launched empty; you populate it while it runs:

# Against a running server (NORNIR_SERVER + token set), register a monitored
# workspace from its descriptor. The descriptor's TOML is stored IN the registry
# row — there is no `descriptors/*.toml` sibling file the server depends on.
export NORNIR_SERVER=http://127.0.0.1:7878
export NORNIR_SERVER_TOKEN=<the-secret>

# `--descriptor` is EITHER a git ssh URL (the server clones the workspace repo and
# reads its nornir-workspace.toml) OR a local path to an already-checked-out repo
# (its dir, or the .toml itself). Either way the content lands in the redb row.
nornir workspace register nordisk \
  --descriptor git@codeberg.org:nordisk/workspace_nordisk.git \
  --monitored --poll 60s

# …or from a local checkout:
nornir workspace register njord \
  --descriptor /path/to/workspace_njord/nornir-workspace.toml \
  --monitored --poll 60s

What happens:

After register, the server's consumer loop keeps it current: on each tick (fixed 60s default) it fetches each monitored member's git HEAD (the poll field you set at register is that fetch's per-workspace cadence) and consumes every commit a member is behind into the workspace's code room — see The code room.

An empty registry is fine. A server with zero workspaces is healthy, not broken — it is an authenticated port waiting for you to register something. You never have to seed workspaces at launch.

The code room (Mímir v2): what the server builds from git

Since Mímir v2 the server does not "populate" a warehouse with scans. It consumes each member's git history into a code graph — the workspace's code room — one atomic release per commit:

<root>/<ws>/git/<member>/                      the server's clone (the fetch door)
<root>/<ws>/builds/mimir_v2/lake/              the event log: its OWN skade catalog
<root>/<ws>/builds/mimir_v2/clones/<member>/   the consumer's own clone
<root>/<ws>/builds/mimir_v2/target/<member>/   the nightly driver's target dir
<root>/<ws>/builds/mimir_v2/out/<member>/      driver dumps (removed once committed)
<root>/<ws>/builds/mimir_v2/open-set.bin       cache, rebuilt from the lake
<root>/<ws>/builds/mimir_v2/std/<toolchain>/   the toolchain's std dump (once per toolchain)
<root>/<ws>/builds/mimir_v2/artifacts/<member>/<sha>/<profile>/<bin>
                                               linked artifacts of a commit — put there by
                                               whoever BUILT it; the server only reads them

Every 60 s the consumer loop asks, per monitored workspace: is a member behind? (No offset in the lake, or the offset is not the commit its clone is at.) Boot, a wiped lake and a server that was away for a week are that same question. What is behind is consumed, at most 8 commits per member per tick.

History: a backfill is a background job

nornir workspace populate <ws> --from <sha|YYYY-MM-DD> [--member <m>]   # over --server
#   ✓ znippy-zoomies: backfill from 8242a30f1c — queued, 20 commit(s) to walk, at seq 1 @ 66a2891d0e
#   ✓ `ws`: 1 backfill(s) queued on the server — one commit per turn, in the background
nornir workspace repopulate <ws>
#   · in flight: znippy-zoomies: backfill from 8242a30f1c — 7 of 20 commit(s), at seq 8 @ 5c2033e9aa

The call returns at once. What it left is ONE row in the lake (backfill_runs, requested), and the server walks it on the workspace's background lane:

Progress: the unit's own row in nornir jobs (workspace_populate, backfill:<member>: commits done / total, the commit it is on; one child run per turn), and repopulate's in flight lines, read from the lake.

The request rides Telemetry.SubmitJobs — a queued workspace_populate job row with {"backfill": {"from": …, "member": …}} — because the workspace RPCs have no field for from (the proto is edda's nornir-wire). Without a server (nornir workspace populate --from on the box, server stopped) the walk runs inline, as before.

The binary hook: linked artifacts are READ, never built

The binary half of the graph (linked symbols, sizes, monomorphised instances — what generic_cost and binary_drift read) comes from the artifacts of a commit. The server does not build them. After a member's commit landed the binary hook looks in

<root>/<ws>/builds/mimir_v2/artifacts/<member>/<sha>/<profile>/<bin>
<root>/<ws>/builds/mimir_v2/artifacts/<member>/<sha>/<profile>/deps/   (optional: exact crate ids)

— a cargo target dir of exactly that commit, e.g. what cargo build --release --target-dir <root>/<ws>/builds/mimir_v2/artifacts/<member>/<sha> leaves. The sha in the path is the claim that the artifact is of that commit. Every executable there that is a bin the workspace declares is joined against the lake's own source rows of the commit (+ the ring) and written as one release of the binary stream.

Nothing there: the ledger row is no_artifact and names the directory, the binary lenses say the same in their evidence, and the hook looks again whenever the consumer asks what is behind — put the artifacts there later and the same commit gets its binary half, no new commit needed. Artifacts of an EARLIER commit stay in the room until newer ones arrive (the lenses say that the two halves are of two commits).

The driver — and what "driver not available" means

The graph comes from a rustc driver on ONE pinned nightly (crates/mimir-driver/rust-toolchain.toml). The stable workspace never builds it; a box that should consume needs the pair cargo-mimir-dump + mimir-driver and that toolchain with rustc-dev.

setting meaning
NORNIR_MIMIR_DRIVER path of cargo-mimir-dump (its sibling mimir-driver is the wrapper). Unset: <nornir home>/mimir-driver/, beside the server binary, then PATH
NORNIR_MIMIR_DRIVER_HOST a wrapper command line every tool is started through — the pinned toolchain image (containers/mimir-driver/Containerfile), e.g. podman run --rm -v /home/nornir/.nornir:/home/nornir/.nornir -v /home/nornir/.nornir/cargo-registry:/usr/local/cargo/registry nordisk-mimir-driver@sha256:<digest>. It must show the same absolute paths inside. The driver path is then a path in the image (cargo-mimir-dump is on its PATH)
NORNIR_MIMIR_TOOLCHAIN the nightly the driver was built against, when it is not the tree's pin
MIMIR_CFG_SCAN the driver's own knob (all / off), passed through untouched

The server probes the driver at boot and before every run: the binaries, the toolchain, its compiler libraries, and the driver itself started against it. When that fails nothing is consumed and nothing pretends: the boot log says NOT CONSUMING [driver_not_available]: … with the step, nornir workspace repopulate <ws> fails with the same sentence, Workspaces.Fetch carries it in errors, readers answer not_consumed. The feeds ride the consumer's offsets, so they do not run either.

Build it on a dev box: cd crates/mimir-driver && cargo build --release --bins (rustup installs the pinned nightly from the toolchain file). For a server box: podman build -t nordisk-mimir-driver -f containers/mimir-driver/Containerfile . from the repo root — the image holds the pinned nightly (rustc-dev, rust-src), a linker and the two binaries built against it, and asserts in its own build that the driver starts. Run it by digest.

Asking what state it is in

nornir workspace repopulate <ws>            # over --server / NORNIR_SERVER
#   ✓ behind, consuming: skade: seq 41 @ 0b07d30f37 → head 9c1e22aa01 (ok)
#   · in flight: znippy seq 12 @ 77aa01bc9d (41 s, 3 behind)
#   ✓ repopulate `nordisk` — lake 3f9a1c2e · skade@41 0b07d30f37 · znippy@11 … — 1 member(s) behind, 1 in flight
nornir workspace repopulate <ws> --purge    # also drops the orphaned v1 tables (once)
nornir workspace ls --status                # red when the consumer could not consume a member
nornir jobs                                 # one row per (member, commit) and per feed run;
                                            # a backfill's unit with its progress; the std dump

The registry's current_snapshot is the lake's stamp: member@seq,….

One process holds a lake

A lake is a single-process skade catalog (a redb file lock). While the server runs, no other process can open it — not the fat CLI, not an embedded MCP: they answer lake_unavailable and are told to go through the server's RPCs. There is no copy-aside read of a held lake. Inside the server any number of readers share the consumer's handle.

A new schema: the lake is evolved in place

The lake is derived from git, but it is not thrown away for a new column. When a new build ADDS nullable columns to the graph's schema (the usual mimir-schema bump), the server evolves the lake when it opens it: one metadata commit per table adds the columns (Iceberg schema evolution, no data file is rewritten) and moves the table's mimir.schema stamp. Nothing to do, nothing is lost:

Any OTHER difference is still refused on open — a column this build does not have (dropped or renamed, or the lake was written by a NEWER build), a column of another type, another sort order — and the server says what to do:

code room of `nordisk`: this lake was written with another schema: mimir_v2.code_node_events
has […] this build does not, and lacks […] — only added nullable columns are evolved in
place; a lake that differs otherwise is not migrated: delete the lake directory and the
consumer rebuilds it from each repo's tip. Stop the server, remove
<root>/nordisk/builds/mimir_v2/lake and start it again (the clones and the driver's
target dir beside it can stay)

Build / runtime requirement: SuiteSparse:GraphBLAS 7.x

The code room's engine links the system GraphBLAS. To build anything that serves a code room: apt-get install libgraphblas-dev. To run it: libgraphblas7 (+ libgomp1) in the runtime image. The gates that link the workspace check first and fail with one line (SuiteSparse:GraphBLAS 7.x not found — apt-get install libgraphblas-dev). The thin GUI atom (nornir-ui) does not link it.

Use the CLI against it (client mode)

Watch it in the viz — one line, from any machine:

nornir viz --server http://<host>:7878 --token <the-secret>

--server makes the viz a thin client (shows all the server's workspaces). The token also resolves from $NORNIR_SERVER_TOKEN or a user-readable ~/.nornir/token — drop the secret there once and bare nornir viz auto-connects. Full flow + token resolution: viz-modes.md.

For the CLI verbs: set NORNIR_SERVER (+ the same token) and any verb routes to the server:

export NORNIR_SERVER=http://127.0.0.1:7878
export NORNIR_SERVER_TOKEN=<same secret>

nornir repos                 # the server's configured repos
nornir guard status          # the server's guard report
nornir index stats           # the server's index
nornir index search "query"
nornir knowledge query <repo> lookup <name>
nornir release gate all <repo>
nornir bench run <repo>      # runs locally, ships the BenchRun via Bench.Submit
nornir introspect symbol-lookup <bin> <pat>   # `<bin>` resolves on the SERVER
nornir docs render <repo>    # rendered on the server

Unset NORNIR_SERVER and the exact same commands run embedded against the local warehouse.

What to expect (the lock rule, by design)

Coverage

Every everyday verb is wired CLI→server: repos, guard, bench run, index {stats,search,snapshot,upload}, knowledge query, release gate, introspect {symbols,lookup,defined-in,callers,callees,path}, docs {init,render,check,history}. index upload streams a locally-built index up to the server (client-streaming Index.UploadSnapshot), the live-refresh path for a server's index — the server reconstructs the blobs under its write lock and snapshots them. Notes: introspect's binary path and docs rendering resolve on the server's filesystem.

Install as a systemd service

Two scopes — pick by who runs it

The lifecycle is three verbs — add (install), update (swap in a new binary), remove (tear down) — across two scopes, auto-picked by privilege (override with --scope user|system):

user (default when NOT root) system (default when root)
systemd systemctl --user system (sudo systemctl)
runs as you the nornir system user
ExecStart your ~/.cargo/bin/nornir-server in place /usr/local/bin/nornir-server (copied in)
state your ~/.nornir /home/nornir/.nornir
token ~/.nornir/token (== client's) /home/nornir/.nornir/token
boot loginctl enable-linger you WantedBy=multi-user.target

One law for both: there is no /etc/nornir and no env file. The bearer token is the 0600 <home>/.nornir/token (read by the server and the CLI client); the bind addr is an inline Environment= line in the unit.

No monitor/poll at install time (done). The --monitor/--poll CLI flags are removed, and the systemd-unit generator (render_server_unit) writes no NORNIR_MONITOR/NORNIR_POLL Environment= lines — unit tests assert the generated unit carries neither. The unit's only env lines are HOME and NORNIR_SERVER_ADDR (the server reads just --token/NORNIR_SERVER_TOKEN and --addr/NORNIR_SERVER_ADDR). Workspaces are added at runtime with nornir workspace register — never seeded from the unit. (--config remains an optional explicit pin that, only when passed, emits a single NORNIR_CONFIG line. The unit renderer still accepts a warehouse pin, but no install flag passes one — both live install paths hand it None, so no NORNIR_WAREHOUSE line is ever written.)

TL;DR — developer (user scope, no root)

cargo install nornir --features viz                  # → nornir, nornir-server, nornir-ui
nornir install add                                   # NO sudo → user scope (token + unit only)
systemctl --user enable --now nornir-server          # start now (serves 0 workspaces yet)
loginctl enable-linger "$USER"                        # (optional) start at boot, no login session

# Add the workspace at RUNTIME (the server learns it; persists in the registry):
export NORNIR_SERVER=http://127.0.0.1:7878
export NORNIR_SERVER_TOKEN="$(cat ~/.nornir/token)"
nornir workspace register njord \
  --descriptor /path/to/workspace_njord/nornir-workspace.toml --monitored --poll 60s

# Same-box client just works — token + endpoint live in your ~/.nornir, no sync:
nornir --server 127.0.0.1:7878 --workspace njord repos
# Stop = fat again; start = served. One warehouse (~/.nornir/warehouse), flip with:
systemctl --user stop nornir-server
nornir install update                                # restart after `cargo install` (no sudo)
nornir install remove                                # UNINSTALL: unit + ~/.nornir DATA erased; ssh key + token KEPT

TL;DR — server box (system scope, root)

cargo install nornir --features viz
sudo nornir install add \                            # sudo → system scope (the `nornir` user)
  --addr 0.0.0.0:7878                                # 0.0.0.0 = reachable over the tailnet/LAN
sudo systemctl enable --now nornir-server            # serves 0 workspaces until you register

# Register the workspace at RUNTIME against the running server (token from nornir's home):
export NORNIR_SERVER=http://127.0.0.1:7878
export NORNIR_SERVER_TOKEN="$(sudo cat /home/nornir/.nornir/token)"
nornir workspace register njord \
  --descriptor /abs/path/workspace_njord/nornir-workspace.toml --monitored --poll 60s
# (descriptor must be readable by the SERVER process, or ship its content via thin mode)

# UPDATE on a new release. Run YOUR build BY FULL PATH: bare `sudo nornir` hits
# sudo's secure_path → the already-installed /usr/local/bin/nornir (a no-op).
cargo install nornir --features viz
sudo "$(command -v nornir)" install update           # copies your bins → /usr/local/bin, restarts

# The token is in the `nornir` user's home, not /etc. Read it / hand it to clients:
sudo cat /home/nornir/.nornir/token
# A client on THIS box has a DIFFERENT home, so copy the token across once:
sudo cat /home/nornir/.nornir/token > ~/.nornir/token

sudo nornir install remove                           # UNINSTALL: unit + bins + DATA erased; ssh key + token KEPT

install update never touches a registry. It does not run cargo install and does not fetch from crates.io / holger. It is a pure local binary swap: you build/cargo install the new binaries, then (system scope) update copies them onto the box and restarts; (user scope) cargo install already replaced ~/.cargo/bin and update just restarts. Producing the new binary is your step; update only moves it the last mile.

nornir install add writes a hardened nornir-server.service unit. In system scope (root) it also creates the nornir system user; in user scope it writes ~/.config/systemd/user/nornir-server.service and needs no root:

# Install the service — it needs nothing but (optionally) a bind addr. The server
# starts EMPTY and you register workspaces against it at runtime (see above).
sudo nornir install add --addr 0.0.0.0:7878

Re-running install add is idempotent (refreshes binaries, preserves token + key, restarts). Use it when you change --addr; use install update when you only shipped a new binary.

No workspace flags are needed (or used) at install time. The server is launched with just bind + auth and learns workspaces at runtime via nornir workspace register (which persists them in the registry across restarts). View a registered workspace with nornir viz --workspace njord (no separate nornir-ui invocation needed; it's a thin wrapper).

No workspace flags at install time (done, see above): install add has no --monitor/--poll flags and writes no NORNIR_MONITOR/NORNIR_POLL env lines. Register workspaces at runtime instead.

The installer copies the binaries onto the system — nornir-server (and the nornir CLI + nornir-ui viz when present) → /usr/local/bin/ — and the unit's ExecStart points there. The service never execs from the build tree (a removable drive the nornir user can't execute from gives 203/EXEC). Re-run the install after a rebuild to refresh the installed binaries (atomic replace).

This binary-copy is system scope only. In user scope the service runs as you and execs your ~/.cargo/bin/nornir-server in place — nothing is copied, so update is just a restart after cargo install.

In system scope the service runs as the nornir system user, whose home is /home/nornir. That user can't read arbitrary paths (e.g. a removable drive mounted 0700 for your login user) — so do not change --user. What the install sets up for that user:

There is no descriptor staging anymore. You do not drop a descriptor file into a /home/nornir/descriptors/ dir — that staging model is gone. Instead, register the workspace at runtime with nornir workspace register <name> --descriptor <toml>; the server reads the descriptor at register time and stores its content inside the redb registry row (descriptor_content), so the registry is fully self-contained (a re-add re-populates from the row's git URLs, no sibling file needed). The descriptor source must be readable by the server process at register time, or — for a thin client whose path the server can't see — its content is shipped over the wire and persisted the same way.

Install behavior: install add has no --monitor/--poll flags and writes no NORNIR_MONITOR/NORNIR_POLL env lines — workspaces come from the registry (added at runtime), and the poll cadence is per-workspace. (--root is deprecated/ignored — the registry root is home-derived as <home>/.nornir/workspaces; relocate by setting the service user's $HOME.) Re-running install add preserves token + key and restarts. (Shipped only a new binary? install update is the lighter path — no flags.)

A private monitored repo is cloned/fetched over SSH with the server's deploy key — use a git@host:owner/repo.git (or ssh://) URL in the descriptor and register sudo nornir key show as a deploy key on the repo. The SSH transport is pure-Rust (russh runs git-upload-pack; gix drives the wire protocol + pack indexing over it — no ssh/git subprocess). In server mode the transport is effectively SSH-only: the installed server always holds a deploy key, so it tries SSH FIRST for every member — an https:// descriptor URL is coerced to its scp-like SSH form (git@host:owner/repo.git) before each fetch (gitio::clone_or_fetch). So every monitored member, public or private, needs the server's key (nornir key show) registered as a deploy key, or it won't sync — a public https:// URL does not exempt it. (The keyless HTTPS path only applies to the FAT/no-key client, which has no key to try first.) (SSH push / git-receive-pack is not implemented yet — monitoring only fetches.)

It (system scope): creates the nornir system user (idempotent), writes the 0600 /home/nornir/.nornir/token (a generated bearer secret, owned by nornir), and writes /etc/systemd/system/nornir-server.service (User=nornir, inline Environment= lines, NoNewPrivileges, Restart=on-failure), then runs systemctl daemon-reload. There is no /etc/nornir and no env file — the token is the file, the rest are Environment= lines in the unit. Then:

sudo systemctl enable --now nornir-server     # start at boot + now
journalctl -u nornir-server -f                # follow logs

Want the service as YOU instead of the nornir user? Drop sudo (user scope) — see the developer TL;DR above. --scope user|system forces either explicitly.

Remove:

sudo nornir install remove            # system scope. NO FLAGS NEEDED: stop+disable,
                                      #   remove the unit AND the /usr/local/bin binaries
                                      #   AND erase the service account's data — but SPARE
                                      #   its .ssh deploy key, the token, and the `nornir`
                                      #   account that owns them
nornir install remove                 # user scope (no sudo): remove the --user unit and
                                      #   erase ~/.nornir's data, KEEPING ssh/ + token

sudo nornir install remove --wipe-identity   # ⚠ the one opt-in: ALSO destroys the deploy
                                      #   key, the token, the home and the `nornir` user

(Everything the service writes lives under its own home, home-derived from HOME=/home/nornir: the warehouse at /home/nornir/.nornir/warehouse, the workspace registry at /home/nornir/.nornir/workspaces, and the Tantivy index cache under /home/nornir/.nornir/cache — so the service writes only inside /home/nornir.)

It also creates the server's git SSH identity: an ed25519 keypair at /home/nornir/.ssh/id_ed25519 (mode 600, owned by the nornir user), generated in pure Rust (the ssh-key crate — no ssh-keygen subprocess). Idempotent and preserved across re-installs like the token. This is the identity the server uses to fetch/push member repos in server-monitored mode. The state home /home/nornir doubles as the workspace root (<root>/workspaces/<name>/{git,builds} — see mode-matrix.md).

Print the public key to register as a deploy key on each monitored repo:

nornir key show       # public key (paste as deploy key)
nornir key path       # path to the .pub
nornir key generate   # for non-systemd/dev: writes ~/.nornir/ssh, never ~/.ssh

THE UNINSTALL LAW — always kill the data, always keep the key

Suite-wide and non-negotiable (workspace_nordisk/.nornir/suite-uninstall-law.md):

uninstall ALWAYS kills the DATA and ALWAYS keeps the SSH KEYS. Not behind a flag. Not opt-in. Not --purge. Plain install remove does both halves, every time.

So nornir install remove — with no flags at all — removes the unit, the installed binaries (system scope) and everything under the state root: warehouses, workspace registry, caches, funnel, jobs. A reinstall gets a genuine clean slate. What SURVIVES, byte-identical, is the install identity: the ssh/ (system scope: .ssh) deploy key that the git remotes have already registered, and the server bearer token. System scope also keeps the nornir account, because that account owns the spared key — an uninstall must never orphan or destroy the key as a side effect of erasing data.

Destroying the identity is the one explicit opt-in:

flag effect
(none) erase all DATA (+ binaries in system scope); KEEP the ssh deploy key + token
--wipe-identity additionally DESTROY the ssh deploy key + token (system scope: the home and the nornir user too)
--purge DEPRECATED, no effect — erasing the data IS the uninstall; still accepted so existing scripts/pipelines keep working
--keep-identity DEPRECATED, no effect — keeping the key is unconditional; still accepted

After --wipe-identity a reinstall mints a brand-new keypair, which you must register again as a deploy key on every monitored remote before the server can fetch or push.

Two more rules the law spells out, both of them from real incidents:

Each individual delete goes through skidbladnir's guarded primitive (skidbladnir::systemd::guarded_remove_dir_all, the authority behind purge_service_data_keep_keys), which refuses any path containing a .ssh component, a real user's home, a system directory, or a symlink pointing out of the tree, and pins the delete to the resolved state root. Proof: src/bin/nornir.rs::purge_law_tests — a fake data home is erased by a FLAGLESS uninstall and the deploy key must come out byte-identical.

Update an existing install by simply re-running the same command — it's overwrite/idempotent: keeps the nornir user, preserves the existing token (so configured clients keep working), rewrites the unit (bind addr + binary path), daemon-reloads, and try-restarts the service if it's running (so the update takes effect; a no-op when stopped). Output says updated (and token preserved) vs installed.

Where compute happens (today) — important

No source code crosses the wire (a remote client never uploads its tree). The server operates on repos on its own filesystem: path= local, or git= which it clones/fetches itself — pure-Rust, SSH-only in practice for a server (it holds a deploy key, so it tries SSH first for every remote, https:// URLs included; see "Git transport" in design.md). Facts are Iceberg rows keyed by git SHA read from those checkouts; the warehouse never stores source.

Work Runs on Notes
Bench client/local needs the machine's hardware; ships only the BenchRun via Bench.Submit
Producers — knowledge scan, index build, vector index, dwarf persist server for monitored workspaces (the poll loop clones → scans → indexes → republishes); client/local for the embedded path a running server's warehouse is single-writer, so an ad-hoc client producer can't target it — but the monitor IS the server producing for itself
Introspect extraction, docs render/check, gates server (client mode) computed on the server's own sources/binaries
Reads — search, knowledge/dwarf query, bench history, gates server (client mode) query the already-built warehouse

Consequence: heavy indexing/embedding happens where the source is — on the server for monitored workspaces (it cloned them), or client/local for the embedded path. A live server's warehouse is kept current by the monitor poll loop, and can also be fed via Bench.Submit / Index.UploadSnapshot. Paths you pass in client mode (introspect <bin>, docs) resolve on the server's filesystem, not yours.

Done since: network git-clone (HTTPS + SSH) — monitored workspaces already clone + produce server-side. Future (plan.md §"Nornir-as-a-service"): expose it as on-demand, client-triggered build repo@<sha> producer RPCs (Index.Build, Vector.Index, Knowledge.Scan, Introspect.Persist) so a thin client can request a build of any SHA without local sources. Bench stays client-side (real hardware).

Monitor a git repo + watch it live (server-monitored + viz)

A server can monitor git repos and republish their warehouse on change, and the nornir-ui viz can watch that warehouse live over gRPC — all Rust binaries, no scripts.

# Server: launch it empty (token + addr only). It seeds NO workspaces from env.
NORNIR_SERVER_TOKEN=<≥16 chars>  nornir-server --addr 127.0.0.1:7878

# Add the monitored workspace at RUNTIME (descriptor = a server-readable
# nornir-workspace.toml or its members' git URLs). Its content is stored in the
# registry row; the per-workspace poll loop fetches + republishes on git change.
export NORNIR_SERVER=http://127.0.0.1:7878
export NORNIR_SERVER_TOKEN=<same>
nornir workspace register njord \
  --descriptor /path/to/workspace_njord/nornir-workspace.toml --monitored --poll 60s

# Viz: pick the workspace from the top-bar dropdown (it lists the server's
# registered workspaces and switches live); auto-refresh is on by default.
NORNIR_SERVER=http://127.0.0.1:7878 NORNIR_SERVER_TOKEN=<same>  nornir viz

How it fits together:

Worked example: [../../workspace_njord/MONITOR-DEMO.md] (monitors the private Njord repo over SSH). See mode-matrix.md for the full self-sync loop.

Embed the holger crates cache in the server (the marriage)

The daemon can run holger's /cache (crates.io mirror) + /sparring (release rehearsal registry) in-process — no separate holger-server. It's linked in via the default-on embed-holger feature; turn it on at boot with an env var:

# in the unit, or your shell before `nornir serve`:
NORNIR_HOLGER=1                    # enable it (presence of NORNIR_HOLGER_HTTP also enables)
NORNIR_HOLGER_HTTP=127.0.0.1:18464 # sparse HTTP cargo talks to (default)
NORNIR_HOLGER_GRPC=127.0.0.1:18443 # control plane (default)
NORNIR_HOLGER_DATA=<dir>           # znippy archives (default <home>/.nornir/holger-cache)

The listeners run on the server's own tokio runtime (same process, owned by the service user). Non-fatal: if holger can't bind, the nornir server still comes up. The everyday foreground equivalent is nornir cache up. See nornir-for-dummies.md.

Not yet

The three deployment modes (fat / server-monitored / thin) and the verb×mode test matrix that the items below feed into are specified in mode-matrix.md. Server-monitored = the server playing thin client against itself, fed by a git poll loop; the single server-user is nornir.

Now works (was "not yet"): multiple monitored workspaces from one server, selected per-request by the nornir-workspace header (CLI/MCP) or the viz's top-bar picker — verified with njord/holger/knut/nornir-catalog live. And pure-Rust SSH clone/fetch for private repos (see "Git transport" above).