diff --git a/.claude/tasks/0009-guest-nesting.md b/.claude/tasks/0009-guest-nesting.md new file mode 100644 index 0000000..ab41286 --- /dev/null +++ b/.claude/tasks/0009-guest-nesting.md @@ -0,0 +1,37 @@ +--- +spec: guests +blocked-by: 0002-guest-walking-skeleton +--- + +## What to build + +The Host-side placement that makes the OCI fallback a plain Guest, so image-only software has a declarative home without a separate mechanism. +A Host sets `nesting` to grant the nested-container prerequisites so the Guest's interior can run Podman and other OCI containers. +The Skeleton emits the nested-container cgroup-delegation and capability prerequisites once, so the operator flips one boolean and the interior's `oci-containers` runtime works, with Podman as the default runtime. + +## Acceptance criteria + +- [x] A Host setting `guests..nesting` grants the nested-container cgroup-delegation and capability prerequisites, off by default. +- [x] With `nesting` on, the Guest's interior can define `oci-containers` running Podman as the default runtime. +- [x] With `nesting` off, those prerequisites are absent and the Guest is unaffected. +- [x] A Host with a nesting Guest that defines an OCI container builds via `nix flake check`. + +## Implementation Notes + +- `nesting` is a Host-side placement field on the guest, a bool defaulting off, alongside the other placement fields in `lib.nix`. +On, it grants the guest's container `CAP_NET_ADMIN` and the `/dev/net/tun` and `/dev/fuse` device nodes, the capability and devices an OCI runtime reaches for to network its containers and back their overlay storage. +Off, both `additionalCapabilities` and `allowedDevices` are empty, matching the NixOS defaults, so a non-nesting guest is untouched. + +- The capability prerequisite is `CAP_NET_ADMIN` alone. +A container-backend guest runs privileged (`privateUsers = "no"`), so it already retains the broad nspawn capability set including `CAP_SYS_ADMIN`; the one addition an OCI runtime needs is network administration for its bridges and firewall rules. + +- cgroup delegation is not toggled by `nesting`, a deliberate deviation from the criterion's wording that the flag "grants" it and that it is "absent" when off. +The NixOS container backend sets `Delegate = true` on every container's unit unconditionally, so the delegated cgroup subtree an OCI runtime manages is always present. +Re-emitting it under `nesting` would be redundant, and forcing it off for non-nesting guests to make it literally "absent" would remove a harmless, useful default for no gain. +The Skeleton records the prerequisite as satisfied-elsewhere with an absence pointer comment, so a reader does not think delegation was forgotten. + +- A new `guests/nesting-sample.nix` carries an interior that defines an `oci-containers` workload, the payload the criteria exercise, and `neogaia` enables it with `nesting = true`. +This follows the walking-skeleton's precedent of proving a guest path through the one Host's `nix flake check`: the flake check builds the nested `nixos-system-nesting-sample` in full, pulling in `podman` and the generated `podman-hello.service` unit, which is how criteria two and four are verified on the build seam. +The sample carries the same modest caps as the walking-skeleton guest so an interior container cannot starve the laptop. + +- The two behaviors the build seam cannot prove — that the interior Podman actually starts a container and that its networking works — are left to manual verification on the target Host and to the VM integration test of task 0010, per the spec's testing decisions. diff --git a/guests/nesting-sample.nix b/guests/nesting-sample.nix new file mode 100644 index 0000000..5941932 --- /dev/null +++ b/guests/nesting-sample.nix @@ -0,0 +1,11 @@ +args@{ my, ... }: +# A sample guest whose interior runs an OCI container on Podman. +# The image is pulled at runtime, so the guest builds with no build-time fetch. +my.guest { + name = "nesting-sample"; + interior = { + virtualisation.oci-containers.containers.hello = { + image = "docker.io/library/hello-world"; + }; + }; +} args diff --git a/hosts/neogaia/default.nix b/hosts/neogaia/default.nix index 460db7b..0819da9 100644 --- a/hosts/neogaia/default.nix +++ b/hosts/neogaia/default.nix @@ -54,6 +54,16 @@ tasksMax = 512; }; + # The nesting guest, run with `nesting` on: proves an interior OCI container + # on Podman builds end to end through this host's `nix flake check`. + guests.nesting-sample.enable = true; + guests.nesting-sample.nesting = true; + guests.nesting-sample.limits = { + memory = "1G"; + cpu = "100%"; + tasksMax = 512; + }; + modules.agents.claude-code.enable = true; modules.agents.tools.gitea-axi.enable = true; modules.agents.pi.enable = true; diff --git a/lib.nix b/lib.nix index 92532d2..8f0d51f 100644 --- a/lib.nix +++ b/lib.nix @@ -279,6 +279,19 @@ let ''; }; }; + nesting = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Grant the guest's interior the prerequisites to run Podman or other + OCI containers of its own. Off by default, so a guest cannot nest + containers. On, the guest's container gains the network-administration + capability its container runtime uses to build bridges and firewall + rules, along with the tun and fuse device nodes such a runtime reaches + for, so the interior's `virtualisation.oci-containers` works with + Podman as its default runtime. + ''; + }; autoStart = lib.mkOption { type = lib.types.bool; default = true; @@ -335,6 +348,25 @@ let # A private-user mapping would shift the ids and reintroduce those errors, so it stays off. privateUsers = lib.mkDefault "no"; + # A nesting guest runs Podman or other OCI containers in its interior. + # The network-administration capability lets that runtime build its + # bridges and firewall rules. + # The tun and fuse device nodes are what it reaches for to network + # those containers and back their overlay storage. + # The remaining prerequisite, a delegated cgroup subtree for the + # runtime to manage, the container backend already grants every guest. + additionalCapabilities = lib.optionals cfg.nesting [ "CAP_NET_ADMIN" ]; + allowedDevices = lib.optionals cfg.nesting [ + { + node = "/dev/net/tun"; + modifier = "rwm"; + } + { + node = "/dev/fuse"; + modifier = "rwm"; + } + ]; + bindMounts = userMounts // secretMounts; inherit specialArgs;