System
Fleet
Every machine is one entry in machines/registry.nix. The flake builds configurations from it, modules read it as constants, and scripts read a plain-data copy. No second list has to be kept in step unless it must work before Nix evaluates.
quarry and lantern; counts are from the registry as of this revision.One registry
Only system is required. Everything else has a default, and the type of system chooses the builder, the module tree and the defaults for the rest.
| Field | Default | Meaning |
|---|---|---|
system | required | A typed system from collective-lib's data.nix, optionally tagged: Tagged [VM WSL] System.NixOS.X86. Home-only entries give an architecture instead. |
home | false | A standalone Home Manager configuration with no system layer. |
user | null | Login user; null means the default for the system type. |
sshPort | 22 | Android's sshd listens on 8022. |
groups | [] | Members of the Group enum: Work, Personal, Android. They select fleet lists. |
pending | false | Defined but not reachable yet; excluded from every fleet list. |
prebuild | null | The cache node that builds and publishes this machine's activation closure. |
attic | null | Set on a machine that serves an Attic cache: { publicKey; }. URL and cache name are derived. |
azureTokenBroker | false | This machine's Azure login mints tokens for machines that cannot sign in. |
vm | null | A guest run by another registry machine: host, runner, cpus, memoryMiB, diskGiB, mac, shareHome, autostart, nested, rosetta. |
An entry for a new laptop, prebuilt by a cache node:
quarry = { system = System.NixOS.X86; groups = [Group.Personal]; pending = true; # out of fleet lists until it is live prebuild = "lantern"; # the cache node that builds its closure};
Platform classes
A system's type picks one of four builders. Tags change assumptions within a class rather than creating new ones.
| Class | Builder | Architectures | Machines |
|---|---|---|---|
| NixOS | lib.nixosSystem | x86_64-linux, aarch64-linux | 22 |
| nix-darwin | nix-darwin.lib.darwinSystem | aarch64-darwin | 1 |
| Nix-on-Droid | nix-on-droid.lib.nixOnDroidConfiguration | aarch64-linux | 5 |
| Home Manager | homeManagerConfiguration | x86_64-linux | 2 |
The 22 NixOS machines are 11 bare-metal x86_64 machines, 9 WSL devboxes, one aarch64 board and one aarch64 VM guest of the Mac. Their roles span desktops, laptops, handheld PCs, servers and a microVM.
Typed systems and tags
Systems are values from collective-lib's data.nix, not strings. Each enum value carries a switch that dispatches on it, so code that forgets a platform fails to evaluate instead of quietly doing nothing.
rebuildCmd = system.systemType.switch { NixOS = "sudo --preserve-env=SSH_AUTH_SOCK env NIX_REMOTE=daemon nixos-rebuild"; NixOnDroid = "nix-on-droid"; NixDarwin = "sudo --preserve-env=SSH_AUTH_SOCK /nix/var/nix/profiles/system/sw/bin/darwin-rebuild";};
| Tag | Effect |
|---|---|
WSL | Adds the NixOS-WSL module with wsl.enable and a WSL state version. The devbox module then converges the Windows host too. |
VM | A virtual guest: hardware-specific configuration is skipped. |
HomeOnly | No system layer. Exported to scripts as __COLLECTIVE_HOME_ONLY=1. |
Architecture is typed the same way. Upstream flakes that only work on x86 are masked rather than failing: on aarch64 the gaming module becomes an option-only stub, so unrelated settings such as game.enable = false still resolve.
Groups and fleet lists
The registry exports plain lists for anything that is not a Nix module. The sizes at this revision:
nix eval --json .#registry --apply 'r: builtins.mapAttrs (_: builtins.length) r.lists'{"android":5,"androidNodes":5,"atticPublicKeys":6,"atticSubstituters":6,"azureTokenBrokerSsh":13,"azureTokenBrokers":13,"cacheBuildMap":11,"cacheNodes":6,"linuxNodes":13,"nodes":20,"personal":6,"work":14}
| List | Holds | Read by |
|---|---|---|
nodes | SSH targets of the live work and personal machines, by MagicDNS name (no addresses are recorded) | Fleet just recipes, tmux fan-out |
linuxNodes, androidNodes | Subsets by platform | Platform-specific recipes |
cacheNodes, cacheBuildMap | Cache servers, and which machine each builds | Prebuild service, cache status |
atticSubstituters, atticPublicKeys | Cache URLs and signing keys | Nix configuration on every machine |
azureTokenBrokers | Machines whose Azure login can serve others | Credential recovery |
work, personal, android | Group members | Workspaces and fleet recipes |
A copy of the plain registry is also written to ~/.config/collective/registry.json on every machine.
Lists that stay explicit
A few lists must work before the flake can be evaluated, such as the cache table a fresh machine needs to download its first closure, or the cache a phone activates from. They stay as literal lists in shell scripts and the justfile, between markers that make them easy to find:
# COLLECTIVE_MACHINE_LIST_START prebuild-mappings: keep in sync with machines/registry.nix `prebuild`case "$machine" in quarry) cache=lantern ;; …esac# COLLECTIVE_MACHINE_LIST_END prebuild-mappings
rg -n 'COLLECTIVE_MACHINE_LIST_(START|END)' ~/collective
Adding a machine
Copy the nearest machine directory
machines/name/holds only what is that machine's own:nixos/configuration.nixand its hardware file,nix-darwin/ornix-on-droid/configuration.nix, andhome-manager/home.nix.Add the registry entry
With
pending = trueuntil the machine is reachable. This alone creates its configuration and updates the derived lists, prebuild mappings and broker lists.Keys and secrets
Add its SSH public key and age key to collective-lib's key lists and re-encrypt the secrets it needs.
Evaluate
nix eval --json .#registry.listsnix eval --raw .#nixosConfigurations.name.config.system.build.toplevel.drvPathBuild on its cache node first
So the first switch downloads instead of compiling.
Switch, join the tailnet, go live
Run
cltv switch, thentailscale up, setpending = false, update the explicit lists, and switch the machines whose lists changed.
VM guests
A registry machine with a vm attribute set is run by its host. On the Mac, every guest whose vm.host is the Mac gets:
- a system LaunchDaemon that runs it under
vfkit(Apple's Virtualization framework) as the primary user, so it boots with the Mac without a login; - state in
~/.local/state/linux-vm/name: disk, EFI variables and logs; - SSH over a vsock socket, which works without networking:
vmruns a command in the default guest, andssh name.vmservesscpandrsync; - registration as a Nix remote builder for its platform, over the same socket.
The guest assumes only virtio and EFI, so linux-vm runner name krunkit boots the same disk under another runner. linux-vm stop holds a guest down across switches and reboots until it is started again.
Working across the fleet
The justfile turns the fleet lists into operations. Each fan-out opens a tmux window per machine over SSH, so long commands stay visible and interruptible.
| Recipe | Does |
|---|---|
just nodes, android-nodes, linux-nodes | Print SSH targets: explicit lists kept in step with the registry |
just cluster-tmux-run alias cmd [nodes] | Run a command on each machine in its own tmux window |
just collective-pull [nodes] | Pull the checkout everywhere |
just collective-switch all|nodes [--cached] | Pull, then switch each machine; --cached activates published generations where a machine has a prebuild assignment |
just cache-build-map | Which cache node builds which machine |
just herdr-init-work|personal|android | Open a terminal workspace with a tab per machine in a group |
just bootstrap-ssh-all | On every machine, connect once to every other by short name, address and full name so each records the others' host keys |
just distribute-pi-auth | Copy this machine's coding-agent (Pi) credentials to the remote machines |