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.
addis the explicit alias.nornir workspace addis kept as a habit-name for the eager path; it does exactly what the defaultregisternow 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 …".
- Symbols:
resolve_descriptor_arg()(cwd discovery) +nornir::workspace::discover_descriptor()(the walk-up) insrc/bin/nornir.rs/src/workspace/mod.rs.
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.
- Pass-through (no content sent): an explicit
--descriptorthat is a git URL (https://…,git@…,ssh://…) or an already-server-readable path —descriptor_contentis empty and today's path-passing behaviour is preserved. - Fat/local mode: uses the discovered/given path directly (no shipping).
- Symbols:
descriptor_is_local_file()(client gate) insrc/bin/nornir.rs;resolve_descriptor_content()+registry::Workspace::from_content+ theregisterhandler insrc/bin/nornir-server.rs.
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
- Fat (no
NORNIR_SERVER): builds the warehouse on your machine, inline. - Thin (
NORNIR_SERVER/--server):addroutes register + fetch over RPC; the server republishes from the fetched change (its build, its warehouse). A discovered LOCAL descriptor's content is shipped so the server can stage + re-read it (see Optional--descriptorabove).
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:
- server →
/home/nornir/.ssh/id_ed25519(created bynornir install) - fat user →
~/.nornir/ssh(create withnornir key generate, then register the printed public key as a deploy key on each repo)
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.