Gunnar

nordisk/nornir

.nornir/data-root.md

<!-- nornir:class: history -->
# Data root — `<home>/.nornir`

## TL;DR (the whole thing in four lines)

- nornir keeps **everything in `~/.nornir`**. That's it.
- **One folder per workspace** under `~/.nornir/workspaces/<name>/`.
- The **server is identical**, just under `/home/nornir/.nornir` (because the
  daemon runs as the `nornir` user).
- Run **`nornir root`** to print where it is. No env-var to set; containers just
  set `$HOME`.

## The layout

nornir's data root is **derived from the running user's home directory**. ONE
rule for everyone, **no path env-vars**. Everything is **per-workspace** — there
is no global warehouse or funnel; a workspace is one self-contained folder:

```
<home>/.nornir/
  workspaces/
    registry.redb             # the ONLY global file (the index: which workspaces exist)
    <name>/                   # e.g. holger, korp, matrix — one folder per workspace
      git/                    # member source: server clones here / fat = your external checkout
      builds/
        warehouse/            # THIS workspace's Iceberg warehouse
      funnel/                 # THIS workspace's idea→plan funnel
```

The payoff: **fat and server modes are the identical layout** — the only
difference is who fills `git/` (you check it out, or the server clones it). A
`workspace_<name>` bundles as many repos as you want, so breadth lives *inside* a
workspace, not in a global pile.

`<home>` is the **running user's** home directory:

| who runs it | `$HOME` | data root |
| --- | --- | --- |
| a login user `rickard` | `/home/rickard` | `/home/rickard/.nornir` |
| the server (system user `nornir`) | `/home/nornir` | `/home/nornir/.nornir` |

The **same code** computes both — there is no special case for the server.

## How home is resolved

`config::nornir_home()` calls the [`home`](https://crates.io/crates/home) crate's
`home_dir()`, which reads `$HOME` and falls back to `getpwuid` for the current
uid. That makes it correct for:

- a normal interactive login (`$HOME` set);
- the systemd service (`Environment=HOME=/home/nornir` in the unit);
- `sudo -H -u nornir …` (the `-H` resets `$HOME` to nornir's, and getpwuid backs
  it up).

The canonical resolvers live in `src/config.rs`:

```rust
pub fn nornir_home_from(home: &Path) -> PathBuf  // pure: home.join(".nornir")
pub fn nornir_home() -> PathBuf                   // nornir_home_from(home::home_dir())
pub fn registry_root() -> PathBuf                 // nornir_home().join("workspaces")
// per-workspace, on `Loaded` (keyed by the workspace_<name> in the config path):
pub fn Loaded::workspace_name(&self) -> String   // "matrix" from …/workspace_matrix/…
pub fn Loaded::warehouse_root(&self) -> PathBuf   // registry_root()/<name>/builds/warehouse
pub fn Loaded::funnel_root(&self)   -> PathBuf    // registry_root()/<name>/funnel
```

Every root/warehouse/funnel default routes through these. An explicit
`[storage].local_path` still overrides the per-workspace default (a deliberate
pin). `nornir root` prints the resolved `nornir_home()` — "where does nornir keep
its data?".

## Why no path env-vars

There used to be `NORNIR_ROOT` (registry), `NORNIR_WAREHOUSE_ROOT`, and
`NORNIR_FUNNEL_ROOT`. They are **gone**. The home rule subsumes them: a process
inherits its user's `$HOME`, so there is exactly one place data lives per user
and nothing to forget to set in sync.

**Containers** get the right path the standard way — **set the user's `$HOME`**
(or run as a user whose passwd home is what you want). No nornir-specific env var
is needed or honored.

### What's still configurable (and what isn't)

- An **absolute `[storage].local_path`** in `nornir.toml` still pins a warehouse
  to an explicit path (e.g. a fast local disk) for the **embedded CLI/MCP** — that's
  a deliberate per-config override, not a filesystem-root env-var.
- **The server has no warehouse override.** `NORNIR_WAREHOUSE` is **no longer read
  by `nornir-server`** — the server's warehouse is `$HOME`-derived per-workspace at
  `<home>/.nornir/workspaces/<name>/builds/warehouse`. (There is no install flag
  left that could pin one: `install add` has no `--warehouse`, and both live
  install paths hand the unit renderer no warehouse — so no
  `Environment=NORNIR_WAREHOUSE=` line is written; unit tests assert its absence.
  No generator cleanup is pending — the old-flag wiring was removed in
  `0c240f9`/`9ea97c4`.)
- **Non-path** env-vars the *server* honors are exactly two: `NORNIR_SERVER_TOKEN`
  (auth) and `NORNIR_SERVER_ADDR` (bind). `NORNIR_SERVER` is the *client*'s endpoint.
  `NORNIR_POLL` / `NORNIR_MONITOR` are **no longer read** (poll cadence is the
  per-workspace registry field; workspaces are registered at runtime). `NORNIR_CONFIG`
  remains an **embedded-CLI/MCP** config-resolution var (the server has no boot
  config). Behavior vars like `NORNIR_TEST_STALL_SECS`, `NORNIR_VIZ_*` are unaffected.

## Erasing it — THE UNINSTALL LAW

`nornir install remove` — **no flags** — erases the data root and **keeps the
identity**:

```
<home>/.nornir/
  workspaces/ …            ← ERASED by every uninstall
  cache/, funnel/, jobs/ … ← ERASED by every uninstall
  ssh/                     ← KEPT (the deploy key the git remotes have registered)
  token                    ← KEPT (the server bearer token)
```

(System scope: the key is at `<service home>/.ssh`, so the uninstall spares that
directory *and* the `nornir` account that owns it, while erasing the data root and
any other state in the home.)

**The root is resolved by scope, never hardcoded** — the same
`nornir_home_from($HOME)` rule above, with system scope reading the service
account's home out of `/etc/passwd`. Two sibling apps once erased a hardcoded
`/home/<app>` regardless of scope: on a per-user install that path did not exist,
so the uninstall wiped nothing and *printed success anyway*. An eraser and a
resolver that disagree about where state lives means one of them is lying.

That is the suite rule (`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`. Destroying the identity is the one explicit
opt-in `--wipe-identity`; afterwards a reinstall mints a **new** key you must
register on every remote again. `--purge` and `--keep-identity` are deprecated
no-ops kept only so old scripts still parse. Deletes run through skidbladnir's
guarded primitive `systemd::guarded_remove_dir_all`, which refuses `.ssh`
components, real users' homes, system directories, and symlinks out of the tree.
An uninstall that removed nothing prints `NOTHING REMOVED`, never success.

## Proof

`tests/root_resolution.rs::root_is_home_derived_and_same_for_any_user` asserts
the in-process resolution equals `$HOME/.nornir`, and — gated on passwordless
`sudo -n -H -u nornir` — runs the built `nornir root` **as the server user** and
asserts it prints `/home/nornir/.nornir`. Same code, same rule, two users.