← the Atlas

Host Isolation, in Plain Words: Lock 1 and Lock 3

Analysis·budding·proposed·

What the two low-risk locks of the #1334/#1375 fix actually do — why a bare dev command can no longer sit in production's chair, and why the test suite can no longer fork-bomb or even see your real kolu.

Twice, working on kolu killed the kolu being worked in: a dev run disrupted the production service (#1334), and a bare vitest forked enough real daemons that the OOM killer reaped the production kaval — every terminal lost (#1375). The first fix attempt (#1911, closed after adversarial review 2026-07-21) framed three independent locks. Of them: Lock 3 shipped (#1921, closed #1375); Lock 1 was superseded — the live plan is state isolation: no badge, no guard; the state-root default is deleted outright and the production wrapper supplies the path, so the Lock-1 mechanism below is kept only as the #1911-era design record; Lock 2 was rejected to #1912. This note explains the two low-risk ones — Lock 1 and Lock 3 — in plain words, as #1911 designed them. They are boot checks and test plumbing; the third, Lock 2, is the delicate one (it guards the daemon take-over/kill machinery itself) and is summarized at the end.Lock 2 puts a role check inside the supervisor endpoint so an adopt, kill, or drain first reads the target daemon’s role marker and refuses to touch production. It touches kolu’s most sensitive lifecycle code, which is why it carries most of the merge risk; the residual it deliberately does not cover (daemon wire verbs like killAll from interactive tools) is tracked in #1912.

LOCK 1 — the folder is the identity ~/.local/state/padi production's folder (its identity) your real kolu badge: KOLU_ROLE=production binds — allowed bare pnpm dev / padi no badge, no own folder ✗ CRASH at bind: "set KOLU_PADI_STATE_DIR" LOCK 3 — tests forked real daemons, uncapped, seeing prod bare vitest 19 real-spawn suites describeDaemon gate skips: KOLU_DAEMON_TESTS unset CI: ci::daemon node gate ON · leashed · capped 0 forks + every test worker's env is scrubbed of KAVAL_SOCKET / PADI_SOCKET — tests cannot even see the real daemons.
Lock 1: a process without the production badge that reaches for production's state folder crashes at bind time. Lock 3: a bare test run spawns no real daemons at all, and test processes have the real daemons' addresses scrubbed from their environment.

Lock 1 — a dev process can’t use production’s folder

A kolu daemon’s identity is its state folder — the path is hashed and that digest names its sockets and its gate. Your real kolu lives at the well-known default (~/.local/state/padi). The hole: a bare dev command — pnpm dev, a plain padi — that isn’t told otherwise defaults to the same folder. Same folder, same digest, same daemon: the dev process is now entitled to take over or restart your kolu. Nothing refused the crossing.

The lock, in three parts:

  1. Production wears a badge. The production launcher (the nix wrapper that starts your daily kolu) sets KOLU_ROLE=production. Nothing else ever sets it.
  2. No badge → the default folder is off-limits. A process without the badge that tries to bind production’s folder — by default, by explicit override, or a folder persistently marked as productionThe persistent marker covers a relocated production root (the #1414 second-instance case): a production daemon stamps its state folder, and a dev process refuses to bind a stamped folder even when pointed at it explicitly, even when no daemon is running there.crashes immediately, with a one-line remedy: set KOLU_PADI_STATE_DIR to a folder of your own.
  3. First-party dev flows already comply. pnpm dev now sets a per-worktree private folder automatically, and the e2e harness always did. Day-to-day you notice nothing; only a truly bare launch that would have collided now refuses loudly instead.

The design law behind it: crash loudly rather than silently share — the same fail-fast shape KOLU_STATE_DIR already had on the server side. The asymmetry (server refused, padi silently defaulted) was the hole.

Lock 3 — tests can’t fork-bomb the box, and can’t see real daemons

Nineteen test files spawn real daemon processes — real padi, real kaval, real PTYs. Two separate dangers: a bare vitest ran them all, uncapped (the incident left 182 abandoned daemon dirs and an OOM-reaped production kaval); and test processes inherit your shell’s environment, which carries the socket addresses of your real daemons — so a buggy test could dial your actual kolu.

The lock, in four parts:

  1. Daemon tests are off by default. Every real-spawn suite is wrapped in describeDaemon (from the new zero-dep leaf @kolu/daemon-test-gate), which skips unless KOLU_DAEMON_TESTS=1. A plain vitest on a workstation spawns nothing.
  2. CI still runs everything. A dedicated ci::daemon DAG node sets the flag, so coverage is a distinct required check — it cannot silently disappear, and the fork-free unit lane keeps one meaning everywhere.
  3. Opted-in runs are leashed. just test-daemon binds every spawned daemon to the run’s lifetime (it dies with the run) and caps worker parallelism — the fork-bomb shape is unrepresentable even when the gate is open.
  4. Tests are blinded. A per-worker scrub deletes the daemon-locator environment (KAVAL_SOCKET, PADI_SOCKET, …) before any test runs, and a runtime guard at the spawn spine throws if an ungated test tries to fork — so helper indirection can’t smuggle a spawn past the gate, and even a badly written test cannot see your real kolu, let alone kill it.

The three ways of running kolu — and which identity each gets

The locks only make sense against how kolu actually starts. There are three front doors; below, every path each one uses, before → after the fix. Two facts to read them with: a daemon’s identity is the digest of its padi state-root path (that digest names its sockets and gates), and the runtime dirs live under $XDG_RUNTIME_DIR and are wiped on reboot — the state roots persist.

nix run github:juspay/kolu — the production front door

Role badge
before: (no such concept) → after: KOLU_ROLE=production, set by the wrapper — the only place that sets it.
Server state (settings, remembered hosts, prefs)
/.config/kolu/ (state.json) — unchanged. An inherited KOLU_STATE_DIR still wins (the relocated-second-instance case, #1414).
Padi state-root (saved session, padi.log) — the identity anchor
/.local/state/padi/
— unchanged path; after, production also stamps ~/.local/state/padi/role (the persistent marker dev refuses to cross, even with the daemon down).
Daemon runtime (sockets · pid gates · manifests; boot-wiped)
$XDG_RUNTIME_DIR/padi-<digest>/ and kaval-<digest>/, where <digest> = sha256 of the state-root path — so production’s digest is fixed by the path above. After: each dir also carries an ephemeral role file, written before the socket serves.

This runs the flake’s default package, which is a thin production wrapper around the real binary. The wrapper does exactly two things: it exports KOLU_STATE_DIR, defaulting to ~/.config/kolu (an inherited value wins, so a second, relocated production instance stays possibleThe #1414 case: a second production instance sets its own KOLU_STATE_DIR before launch and the wrapper honors it via its :- fallback instead of hijacking $HOME. Lock 1’s persistent marker is what keeps such a relocated root refusable by dev processes.), and — after Lock 1 — it sets the badge: KOLU_ROLE=production. Nothing else in the repo sets that variable. The server then binds the default padi state root, which is exactly what the badge entitles it to. Notably, the base binary underneath the wrapper deliberately has no state-dir default: anything that reaches it without going through this wrapper (tests do) crashes rather than silently landing in ~/.config/kolu.

The home-manager module — the same door, supervised

services.kolu doesn’t invent a different kolu — it execs the package you point it at (normally that same production wrapper) as a user service (systemd unit on Linux, LaunchAgent on macOS) with your --host/--port, Restart=on-failure, and the TUI CLIs on PATH. So every path is identical to nix run above — production folder, production badge, same padi root, same runtime digest. One thing worth its own line:

What the service manager owns vs. what outlives it
systemd/launchd supervises only the server. The padi + kaval daemons it spawns live in the boot-wiped runtime dirs and outlive service restarts by design (the kaval-survivors feature) — which is exactly why a stray dev process able to adopt or kill them was so dangerous: they are long-lived and hold your terminals.

just dev — a private universe per worktree

just dev (and just dev-auto, which picks two random free ports so a second worktree never collides) runs the dev server via the kolu-cli dev script.

Role badge
none — and none needed: nothing here ever reaches for production’s folder.
Server state
before: set<worktree>/.kolu-dev/ (this half was already isolated). After: unchanged.
Padi state-rootthe fix’s crux
before: UNSET → silently defaulted to ~/.local/state/padi (production’s root) — the #1334 hole. After: <worktree>/.kolu-dev/padi/, set explicitly by the dev script. A private root ⇒ a private digest ⇒ a wholly separate daemon identity.
Daemon runtime
$XDG_RUNTIME_DIR/padi-<dev-digest>/, kaval-<dev-digest>/ — namespaced to the worktree, structurally unable to collide with production’s sockets/gates.

The missing second export (KOLU_PADI_STATE_DIR) was the whole of #1334; Lock 1 turns any future recurrence — a bare launch that resolves production’s root without the badge — into a crash instead of a takeover.

What you feel afterwards (Lock 3 today; Lock 1 once its rebuild lands)