Collectivenix fleet

30 machines · 4 platform classes · 1 registry

Devices

Platforms

The same modules land on very different hosts. Each platform has its own reasons to be awkward: WSL distributions shut down when idle, macOS scans every file, handhelds dock and undock, and some containers have no root at all. These are the parts of Collective that exist to absorb that.

WSL devboxes

Nine machines are NixOS under WSL on Windows cloud devboxes. They do the fleet's heavy lifting: CI runners, the six Attic caches and their prebuilds, Azure token brokering and long-running agent services. The devbox module makes one of them from a few lines.

devbox = {  enable = true;  hostName = "name";  windows = {    enable = true;    winget.userPackages = [ "Microsoft.PowerShell" "Microsoft.WindowsTerminal" "Git.MinGit" ];    winget.adminPackages = [ "Google.Chrome" ];    power.disableSleep = true;    power.disableHibernate = true;  };};

Converging the Windows host from inside

The Windows side is declared too, rendered as an idempotent PowerShell script that runs through WSL interop. The Windows account is not an administrator, so convergence is split in two:

HalfRunsCovers
HeadlessEvery switch and on a timer, no promptUser-scope winget installs (portable installers where they exist), per-user power timeouts, extra PowerShell
AdminOnce, devbox-windows-admin, one UAC consentMachine-scope installs, hibernation off, and a boot-time SYSTEM task that re-applies never-sleep after Windows Update or an image reset

Keeping WSL alive

WSL stops a distribution when no Windows-side client is attached, and systemd services inside it do not count. A Windows-side watchdog, running as the distribution's owner and never as SYSTEM, holds a client open on a pipe it owns, so closing it ends only that client and a dead parent leaves no orphans. It probes health with bounded commands and logs failures, including whether the distribution's own SSH server still answers from Windows, but by default never restarts the distribution: a busy host can stall WSL's control channel while everything inside keeps running, and restarts mostly killed healthy work. A maintenance marker pauses it. Liveness has a Windows reporter of its own that works without WSL at all (see Operations).

The Mac and its Linux VM

The Mac is a nix-darwin machine with Determinate Nix, a tiling window manager and hotkey daemon, a PulseAudio server and its share of the CI runner pool. Its most useful module runs a Linux machine.

macOS endpoint security scans files as they are written, which makes Git checkouts, Nix builds and Cargo builds slow. So the Mac hosts an aarch64 NixOS guest under Apple's Virtualization framework, and that work happens on the guest's ext4 disk, which the host sees as one large file. The guest is an ordinary registry machine: prebuilt on a cache node, switched with --cached, reachable over vsock SSH without networking and registered as a remote builder for aarch64-linux. How the host runs it is on the Fleet page.

Desktops, laptops and handhelds

ModuleHandles
wayland/hyprland, sway, x11/i3Window managers with shared keybindings and bars. Hyprland also runs nested inside Android (see Android)
displayNamed outputs as kanshi profiles, laptop panels, and a headless output so hot-plugging works
wayland.hyprland.internalDisplayTurn the built-in panel off while a physical external output is active, per machine
inputOne touchpad and gesture configuration, matched to devices at run time
formServer, laptop and clamshell behaviour: whether idle or a closed lid suspends
gfxAMD (with an opt-in ROCm toolkit that changes nothing else), NVIDIA, monitoring
gameSteam, and on handhelds the Jovian Steam Deck session with Decky. x86 only; elsewhere a stub
handheld-daemon (public)Handheld controls and power profiles, with exactly one power-profile service
keyboardQMK and Vial keyboard access through the generated udev rules

Behaviour that is hard to see on a screen is tested from what the modules generate: display toggling, the internal-display policy, gesture configuration and handheld power each have a test in tests/.

Sound across the fleet

Every NixOS and macOS machine bootstrapped by Collective has a silent 48 kHz stereo sink named cacophony and its monitor source, reachable over the PulseAudio protocol on TCP 4713. Producers such as text-to-speech write to it explicitly, and any number of listeners subscribe to its monitor. No default device or loopback changes.

PlatformServerWho may connect
NixOSpipewire-pulseLoopback, and the tailnet interface through the host firewall; no global port opening
macOSPulseAudio backed by CoreAudioAn address ACL of loopback and tailnet ranges, with cookie and anonymous admission disabled, and the binary allowed in the application firewall
AndroidThe PulseServer appSinks are created in the app; Nix-on-Droid is a client

A wildcard bind is not protected by the tailnet on its own; each platform's boundary is what keeps the port off the LAN.

A microVM

A minimal NixOS guest, with no host-specific hardware or secrets, runs locally or under microvm.nix:

nix build .#nixosConfigurations.micro.config.microvm.declaredRunnernix run .#nixosConfigurations.micro.config.microvm.declaredRunner

The QEMU runner forwards host port 2222 to the guest's SSH and keeps persistent volumes in the directory it is started from. A browser-bootable variant of the same guest is built in CI.

Nix without root

Some hosts offer a container, a non-root user and nothing else: no writable /nix, no CAP_SYS_ADMIN, no init system, and a root filesystem recreated on every restart. Five probes decide what is possible:

id; sudo -n true && echo SUDO_OKgrep CapBnd /proc/self/status                 # CAP_SYS_ADMIN is bit 21sudo mount --bind ~/store /mntunshare -Ur true                               # user namespaces?unshare -Urm --propagation=unchanged true      # mount namespace?
RouteNeedsVerdict
A real /nix through sudoPasswordless sudoBest: native speed, single-user install
Bind mount from homeCAP_SYS_ADMINIdeal, but absent from the stock container capability set
nix-user-chrootUser namespaces with bindsFails where namespaces are allowed but every bind is denied
prootNothing (ptrace)Always runs; substitution works, local builds that copy store paths do not
Symlink /nix into homeNever: Nix refuses a symlinked store or parent

Under proot, access(2) resolves against the real path, and a copy's final permission step resolves outside the binding. So builders that copy store paths fail, while NAR extraction, which never changes symlink permissions, works. When /nix is real but wiped on restart, publish the closure to a binary cache in $HOME and use it as a substituter, rather than copying the store and its database. Such hosts take a home-only configuration tagged as a VM, run services through supervisord (the agnostic daemon module) and set sandbox = false with an empty build-users-group.