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

nordisk/nornir

.nornir/workspace-add.md

nornir workspace register — eager populate (server ≡ user)

register is EAGER by default: it records + fetches + builds a workspace inline, so it's populated now — no waiting for a poll tick. The whole point: a fat user and the server do the same thing. Decided + built with Rickard, 2026-06-12; made the default for register (was add-only) 2026-06-14.

add is the explicit alias. nornir workspace add is kept as a habit-name for the eager path; it does exactly what the default register now does. There is no behavioural difference — reach for whichever reads better.

TL;DR

# --descriptor is OPTIONAL — stand IN the workspace dir and just name it:
cd workspace_<name>            # contains nornir-workspace.toml
nornir workspace add <name> --monitored   # discovers ./nornir-workspace.toml

# or be explicit (path or git URL):
nornir workspace register <name> --descriptor workspace_<name>/nornir-workspace.toml --monitored

# opt OUT to the old record-only behaviour (population deferred to the poll loop):
nornir workspace register <name> --descriptor … --monitored --lazy

Optional --descriptor — "just stand in the workspace dir" (2026-06-15)

--descriptor is OPTIONAL. When omitted, nornir discovers nornir-workspace.toml by walking UP from the current directory (check the cwd, then its parent, repeating to the filesystem root) — the same cwd→workspace UX as viz/test/arch, which already infer the workspace from a workspace_<name>/ cwd via the nornir-workspace header. If nothing is found, it errors clearly: "no --descriptor given and no nornir-workspace.toml found walking up from …".

Thin/server mode SHIPS a discovered LOCAL descriptor's CONTENT

In thin mode the server is a different host/user and cannot read the client's local path. So when the resolved descriptor is a local file (not a git URL, not an explicit server path), the client reads its content and sends it over the RPC in RegisterWorkspaceRequest.descriptor_content (proto field 6). The server stores that content IN the redb registry row — the workspace's descriptor_content field (registry::Workspace::from_content) — not a sibling <home>/.nornir/descriptors/<name>.toml file. The redb row is the single source of truth: the poll loop and every later republish re-read the descriptor TOML straight from the row (WorkspaceDescriptor::from_content(&ws.descriptor_content)), never a sibling descriptors/*.toml on disk.

Runs three phases and leaves a ready-to-view warehouse:

① registered  <name> (N member(s))          # seeds members from the descriptor's [repos.*]
② fetching    N member(s)…                   # clones each into ~/.nornir/workspaces/<name>/git/<member>/
③ building    warehouse…                     # knowledge scan + code index + per-member snapshot
✓ added       <name> → populated (snapshot …) at ~/.nornir/workspaces/<name>/builds

Why it exists — server ≡ user

register is eager by default: it records the workspace AND runs the same record → fetch → build pipeline the poll loop runs on a change, inline, so the members populate + the warehouse snapshot exists now. The poll loop then handles updates (re-fetch + rebuild on a git HEAD change). Nothing differs in kind from what the server does — only who pushes the button (you, vs. the daemon).

what it does fat (you) server (nornir user)
workspace register (default) / add record + fetch + build = populate now ✅ same command ✅ same command (RPC + poll loop)
poll loop automatically re-fetch + rebuild on a git HEAD change — the update only ✗ (no daemon) ✅ the only server-extra

So the poll loop is just the automated register. The lazy primitive is still there as an opt-out — nornir workspace register … --lazy (CLI), eager:false/lazy:true (RPC + MCP workspace_register) — for scripted/daemon flows that explicitly want record-only and defer population to the first poll tick. Default is eager; --lazy is the escape hatch, not the norm.

It writes to exactly the path the server reads: ~/.nornir/workspaces/<name>/builds (the server uses /home/nornir/.nornir/workspaces/<name>/builds; same code, config::Loaded::warehouse_root() == monitor::republish's target == the served builds/). See data-root.md, viz-modes.md.

Modes

The one setup difference: SSH members need a per-user deploy key

Member sources are cloned pure-Rust (gix over HTTPS, russh over SSH — no git subprocess). For SSH remotes (git@codeberg.org:…) the key is read from a per-user location, deliberately NOT your ~/.ssh:

This is the same mechanism, one per identity — so "fat does what the server does" holds for auth too: the server got its key at install; you generate yours once. HTTPS-public members need no key and clone directly; if you skip the key, fat add of SSH members stops at ③ with "SSH-only members can't be cloned yet" — the fix is nornir key generate + add the deploy key (or use HTTPS descriptors).

After register / add

nornir viz (standing in the workspace, or --config it) reads the local warehouse; or just nornir viz from anywhere → thin client to a server that has it. Re-run register (or add) any time to update (it's idempotent: re-fetches changed members + rebuilds) — the manual version of the poll loop.