feat: introduce guests as nested-container definitions #28

Merged
alexion merged 1 commits from task-0002-guest-walking-skeleton into main 2026-07-25 14:37:36 -04:00
Owner

Summary

Introduces the guest — a third auto-loaded kind beside the host and the module.
A guest is a reusable definition under guests/ that a host enables like a module and that realizes its interior as a systemd-nspawn nested container.

  • Auto-loader now discovers guests/ with the same recursion it uses for modules/ and hosts/, so a guest wires itself in with no manual imports edits.
  • Base split. The shared base config becomes three files: base.nix (the substrate both bases share — primary user, home-manager, the unstable/stable overlays), system.nix (the host base, keeping bootloader, sops decryption, the password, and the maintenance timers), and a new guest.nix (the slim guest-base). The guest-base imports the full modules/ tree, pins the interior release, and auto-enables the toolkit bundle and modules.ssh.
  • modules.ssh guest flavor. A hostKeys.restore flag (default on) and nullable hostKeys.sopsFile/userKey.sopsFile. A host restores its committed host keys as before; a guest self-generates a host key and carries no age key.
  • The guest helper in lib.nix declares guests.<path>.{enable,backend}, guards the body on enable, and builds the nested container. backend is an enum defaulting to container; microvm is reserved and 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.

nix flake check passes and builds the nested nixos-system-sample interior in full.
Verified by evaluation: the interior has the toolkit and all five members on, SSH in guest flavor with self-generated host keys and the operator's authorized key, and an empty sops.secrets, while neogaia still restores its own host keys with openssh.hostKeys = [ ].

Deviations

  • The base split is three files, not two: base.nix holds the common substrate so "both bases include the primary user, home-manager, and the overlays" stays a single fact rather than a duplicated one.
  • The guest-base imports sops-nix and stylix alongside the module tree. This is load-bearing: the module system pushes an mkIf down to its leaves, so an option path a module names must be declared even where its enable is off, and the tree names sops.* and stylix.*.
  • The guest defaults to privateNetwork = true (as mkDefault) so its interior sshd never contends with the host's; the later networking foundation can attach it to a VLAN bridge.

Review

Risk

Overall: HIGH

  • Blast radius: High — refactors the shared base every host carries, rewrites mkHost, and restructures modules/ssh.nix, so all hosts re-evaluate.
  • Reversibility: Low risk — pure Nix config, no migrations or state deletions; a git revert and rebuild fully restores the prior system.
  • Test coverage: Medium — nix flake check builds each toplevel plus the nested guest interior, but nothing exercises runtime SSH auth or container isolation/networking.
  • Sensitive domain: High — touches SSH daemon config, sops-managed host/user private keys, wheel/root lockout, and the guest's network namespace.
  • Size & complexity: Medium — moderate diff, with non-trivial control flow in lib.guest (option-path metaprogramming) and the mkMerge/mkIf SSH split.
  • Runtime criticality: High — the SSH refactor governs remote access to neogaia; a regression in the restore/sopsFile branching could break host-key restore.

Note on the SSH risk: verified that neogaia is unchanged — its sops secrets still include ssh-host-ed25519-key/ssh-host-rsa-key and services.openssh.hostKeys = [ ] (restored, not generated).

Standards — unaddressed

  • Shotgun Surgery (judgement call): sops-nix and stylix are imported in both guest.nix and mkHost. Left explicit — the two sites differ in intent (active on the host, declared-but-inert in the guest), and extracting a shared two-item list would be premature abstraction; the guest.nix comment documents why the guest needs them.
  • File-top purpose line (judgement call): guest.nix's header describes what it imports and that it stands on the shared base. Kept — a one-line purpose statement is the permitted file-top form and aids navigation.

Spec — unaddressed

  • Namespace mirroring is by author discipline, not enforced: the guest helper takes name as a literal, so the option path matching the file's location is convention, not machinery. By design — this is exactly how a module declares its own namespace, and the loader change is the same recursion, so the index-node and file-or-folder rules a folder-shaped guest would use come for free (none exists yet to exercise them).

— Claude

## Summary Introduces the **guest** — a third auto-loaded kind beside the host and the module. A guest is a reusable definition under `guests/` that a host enables like a module and that realizes its interior as a systemd-nspawn nested container. - **Auto-loader** now discovers `guests/` with the same recursion it uses for `modules/` and `hosts/`, so a guest wires itself in with no manual `imports` edits. - **Base split.** The shared base config becomes three files: `base.nix` (the substrate both bases share — primary user, home-manager, the `unstable`/`stable` overlays), `system.nix` (the host base, keeping bootloader, sops decryption, the password, and the maintenance timers), and a new `guest.nix` (the slim guest-base). The guest-base imports the full `modules/` tree, pins the interior release, and auto-enables the `toolkit` bundle and `modules.ssh`. - **`modules.ssh` guest flavor.** A `hostKeys.restore` flag (default on) and nullable `hostKeys.sopsFile`/`userKey.sopsFile`. A host restores its committed host keys as before; a guest self-generates a host key and carries no age key. - **The `guest` helper** in `lib.nix` declares `guests.<path>.{enable,backend}`, guards the body on `enable`, and builds the nested container. `backend` is an enum defaulting to `container`; `microvm` is reserved and 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. `nix flake check` passes and builds the nested `nixos-system-sample` interior in full. Verified by evaluation: the interior has the toolkit and all five members on, SSH in guest flavor with self-generated host keys and the operator's authorized key, and an empty `sops.secrets`, while `neogaia` still restores its own host keys with `openssh.hostKeys = [ ]`. ### Deviations - The base split is **three** files, not two: `base.nix` holds the common substrate so "both bases include the primary user, home-manager, and the overlays" stays a single fact rather than a duplicated one. - The guest-base imports `sops-nix` and `stylix` alongside the module tree. This is load-bearing: the module system pushes an `mkIf` down to its leaves, so an option path a module names must be *declared* even where its `enable` is off, and the tree names `sops.*` and `stylix.*`. - The guest defaults to `privateNetwork = true` (as `mkDefault`) so its interior sshd never contends with the host's; the later networking foundation can attach it to a VLAN bridge. ## Review ### Risk **Overall: HIGH** - Blast radius: High — refactors the shared base every host carries, rewrites `mkHost`, and restructures `modules/ssh.nix`, so all hosts re-evaluate. - Reversibility: Low risk — pure Nix config, no migrations or state deletions; a `git revert` and rebuild fully restores the prior system. - Test coverage: Medium — `nix flake check` builds each toplevel plus the nested guest interior, but nothing exercises runtime SSH auth or container isolation/networking. - Sensitive domain: High — touches SSH daemon config, sops-managed host/user private keys, `wheel`/root lockout, and the guest's network namespace. - Size & complexity: Medium — moderate diff, with non-trivial control flow in `lib.guest` (option-path metaprogramming) and the `mkMerge`/`mkIf` SSH split. - Runtime criticality: High — the SSH refactor governs remote access to `neogaia`; a regression in the `restore`/`sopsFile` branching could break host-key restore. Note on the SSH risk: verified that `neogaia` is unchanged — its sops secrets still include `ssh-host-ed25519-key`/`ssh-host-rsa-key` and `services.openssh.hostKeys = [ ]` (restored, not generated). ### Standards — unaddressed - **Shotgun Surgery (judgement call):** `sops-nix` and `stylix` are imported in both `guest.nix` and `mkHost`. Left explicit — the two sites differ in intent (active on the host, declared-but-inert in the guest), and extracting a shared two-item list would be premature abstraction; the `guest.nix` comment documents why the guest needs them. - **File-top purpose line (judgement call):** `guest.nix`'s header describes what it imports and that it stands on the shared base. Kept — a one-line purpose statement is the permitted file-top form and aids navigation. ### Spec — unaddressed - **Namespace mirroring is by author discipline, not enforced:** the `guest` helper takes `name` as a literal, so the option path matching the file's location is convention, not machinery. By design — this is exactly how a module declares its own namespace, and the loader change is the same recursion, so the index-node and file-or-folder rules a folder-shaped guest would use come for free (none exists yet to exercise them). — Claude
alexion added 1 commit 2026-07-25 13:20:10 -04:00
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.
alexion merged commit e7d7eb14e1 into main 2026-07-25 14:37:36 -04:00
alexion deleted branch task-0002-guest-walking-skeleton 2026-07-25 14:37:36 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alexion/dotfiles#28