Add a third auto-loaded kind beside the host and the module: the guest, a reusable definition under guests/ that a host enables like a module and that realizes its interior as a systemd-nspawn nested container. Split the shared base config so a guest can stand on it. base.nix now holds the substrate both bases share — the primary user, home-manager, and the unstable/stable overlays. system.nix keeps the host-only machinery, and a new guest.nix is the slim guest-base: it imports the full modules tree, pins the interior release, and auto-enables the toolkit bundle and SSH so any guest is workable on sight. Give modules.ssh a guest flavor. A host restores its host keys from secrets as before, while a guest sets hostKeys.restore = false, names no sops files, and self-generates a host key, so it holds no age key of its own. The lib grows a guest helper that declares the guests.<path> namespace with an enable and a backend field. Only the container backend is built; microvm is a reserved value that trips a clear build-time assertion rather than silently building nothing. A sample guest exercises the whole path, and neogaia enables it, so the guest interior builds through the existing nix flake check seam.
5.4 KiB
spec, blocked-by
| spec | blocked-by |
|---|---|
| guests | 0001-toolkit-bundle |
What to build
The tracer bullet for the Guest concept: the thinnest complete path from discovery to a running nested container.
The Auto-loader gains a third kind, discovering every Guest under guests/ the way it already discovers Modules and Hosts.
The shared base config splits into a host base (system.nix, carrying host-only machinery — bootloader, hardware profile, host identity, boot and garbage-collection timers) and a slim guest-base that every nested Guest stands on.
Both keep the primary user, home-manager, and the shared overlays; the guest-base additionally auto-enables the toolkit bundle and modules.ssh, and imports the full modules/ tree so any Module is available inside a Guest.
A minimal sample Guest is Module-shaped: it declares its own guests.<path> namespace with an enable and a backend field (default container, microvm reserved but not built), and guards its body per the Enable convention.
Its body's payload — the one difference from a Module — realizes a nested container running the Guest's interior on the guest-base.
A Host enables the sample Guest exactly as it enables a Module, and the whole thing builds through the existing nix flake check seam.
Acceptance criteria
- The Auto-loader discovers and wires every Guest under
guests/as a third kind, with no manualimportsedits, and a Guest's option path mirrors itsguests/location per the Namespace convention (including the index-node and file-or-folder rules). - The shared base config is split into a host base and a slim guest-base; the existing Host still builds via
nix flake checkwith its host-only machinery intact. - The guest-base includes the primary user, home-manager, and the shared overlays, auto-enables
toolkitandmodules.ssh, and imports the fullmodules/tree. - A sample Guest declares
guests.<path>.enableplus abackendfield defaulting tocontainer, guards its body onenable, and realizes a nested container running its interior when a Host enables it. - The
microvmbackend value is accepted as reserved but unimplemented, failing clearly rather than silently building nothing. - A Host enabling the sample Guest builds via
nix flake check, the guest is reachable from its Host bymachinectlwith no per-Guest configuration, and the baseline toolset and SSH access are present inside it.
Implementation Notes
-
The base split is realized as three files, not two.
base.nixis the shared substrate both bases build on — the primary user, home-manager, theunstable/stableoverlays, and flakes.system.nix(the host base) imports it and adds the host-only machinery (bootloader limit, sops decryption and the password, the maintenance timers, the chaotic cache, console keymap).guest.nix(the guest-base) imports it and adds the slim guest layer. Factoring the common substrate out keeps "both include the primary user, home-manager, and the shared overlays" a single fact rather than a duplicated one. -
The guest-base imports
sops-nixandstylixalongside the fullmodules/tree. This is load-bearing, not incidental: the module system pushes anmkIfdown to the leaves it guards, so an option path a module names must be declared even where itsenableis off.modules/ssh.nixnamessops.*and the desktop modules namestylix.*, so those option namespaces have to exist for the tree to evaluate inside a guest that leaves them disabled. -
modules/ssh.nixgained a guest flavor.hostKeys.restore(default on) gates restoring host keys from secrets, and bothhostKeys.sopsFileanduserKey.sopsFileare now nullable. A guest setsrestore = falseand names no sops files, so its daemon self-generates a host key and it carries no age key — verified: the interior'ssops.secretsis empty andservices.openssh.hostKeysfalls back to the generated defaults, whileneogaiastill restores its committed host keys withopenssh.hostKeys = [ ]. -
The namespace mirroring is honored by author discipline, exactly as a Module's is:
my.guest { name = "sample"; }names the option path, andguests/sample.nixplaces it. The Auto-loader change is the same recursivecollectNixFiles, so the index-node and file-or-folder rules a folder-shaped guest would use come for free from the loader that already serves Modules — no folder-shaped guest exists yet to exercise them. -
The guest gets
privateNetwork = trueby default, so its interior sshd never contends with the host's on the shared namespace. It ismkDefault, so the networking foundation can later attach the guest to a VLAN bridge. -
backendis anenum [ "container" "microvm" ]defaulting tocontainer.microvmis built by nothing; choosing it trips a build-time assertion with a clear message rather than silently producing no container, per the acceptance criterion and ADR 0006's reserved-value decision. -
neogaiaenablesguests.sampleto demonstrate the path end to end, the way it demonstratedmodules.toolkit. This means a rebuild starts a live nspawn container on the laptop; it is a minimal smoke test and can be turned off with one line. -
nix flake checkpasses and builds the nestednixos-system-sampleinterior in full. A pre-existing nixvim warning about its ownnixpkgs.followsnow also prints for the guest's reused Neovim config; it is upstream noise, not a defect in this change.