Switch the default provider from Anthropic to OpenAI and the default
model to gpt-5.3-codex, so Pi runs on a ChatGPT Plus/Pro (Codex)
subscription. The OAuth login stays unmanaged in ~/.pi/agent/auth.json,
as before.
A guest placed on a VLAN runs its own networkd, which default-enables
systemd-resolved. That conflicts with the nested-container default of
inheriting the host's resolv.conf, so its toplevel failed to build; keep the
guest's own resolver with networking.useHostResolvConf = false.
The same guest's container enslaves its veth to the VLAN bridge at start,
but the container backend orders the unit only after the network is up, not
after that specific bridge exists — a race the veth enslavement could lose.
Order container@<name> after the br-vlan<id> device so the bridge is there
first.
Both were surfaced by a VM integration test that was explored and then
dropped as not worth its regression cost; the task file records the call.
Add a `nesting` placement field to the Host-side guest interface, a bool
off by default. On, it grants the guest's container the prerequisites its
interior needs to run Podman and other OCI containers: the `CAP_NET_ADMIN`
capability an OCI runtime uses to build its bridges and firewall rules,
and the `/dev/net/tun` and `/dev/fuse` device nodes it reaches for to
network those containers and back their overlay storage. Off, both the
capability and device lists are empty, so a non-nesting guest is untouched.
cgroup delegation, the other nested prerequisite, the NixOS container
backend already grants every container unconditionally, so the Skeleton
records it with an absence pointer rather than re-emitting it.
Add a nesting-sample guest whose interior defines an `oci-containers`
workload on Podman, and enable it on neogaia with `nesting` on, so the
path builds end to end through the Host's `nix flake check` — which pulls
in podman and the generated container unit for the nested system.
Give the Host-side placement a `limits` field — `memory`, `cpu`, and
`tasksMax` — that caps the guest's `container@<name>.service` unit via
`MemoryMax`, `CPUQuota`, and `TasksMax`, uncapped by default so an unset
limit contributes no key and systemd keeps its default.
Add an `autoStart` placement option, on by default, driving the
container's boot-start so a Host can define a guest without starting it
at boot.
Give neogaia's skeleton guest modest demonstrative caps.
Add a `secrets` placement option to the guest builder: a list of secret
names. The host declares each as one of its own sops secrets, so the host
is the sole decryptor from the sops files it already holds, and the
decrypted file is bind-mounted into the guest read-only at the same
`/run/secrets/<name>` path it occupies on a host. A service inside the
guest reads its credentials at a predictable location while the guest
carries no age key and decrypts nothing itself.
Ownership needs no new code: the container already runs in the host's uid
and gid space one to one, so the decrypted file's host owner is its owner
inside the guest.
Assert that no in-guest path is claimed by both a mount and a secret, so
the attribute-set merge fails loudly rather than resolving a collision
silently in the secret's favour.
Add a `mounts` placement option to the guest builder: a Host maps guest
interior paths to host paths, each read-write by default or read-only per
mount, realized as the nested container's bind mounts. A guest sees exactly the
data it should, at any granularity from a single folder to a whole pool.
Pin the container to the host's uid and gid space with `privateUsers = "no"`,
so a guest process writing as the shared storage group lands on a bind-mounted
pool as that same group without permission juggling.
Install the grill skill globally from the skills flake, alongside wiki and
consume. It interviews the operator relentlessly about a plan or design,
resolving every branch to an explicit decision before any implementation.
Add `modules.zfs`, the host-level pool import: a host declares its ZFS host id
and the pools to import with their dataset mountpoints, and the module imports
those pools as durable state rather than recreating them, so a service's data
survives any rebuild or reimage.
Add a shared `storage` group with a fixed gid to the base config both the host
base and the guest-base build on, so a host and every guest carry the same
number and an identity-mapped container write lands on the pool as that group
without per-service permission juggling.
No host enables the module: the only host is a laptop with no pools and a
kernel with no ZFS build, so the enabled build was verified by ad-hoc
enablement against a ZFS-supported kernel while the committed tree stays inert.
A guest sets `vlan` to attach to its host's `br-vlan<id>` bridge, `mac`
to reuse an existing address (else a stable one is derived from its
namespace path and read back via `nix eval`), and `address` for a static
IP (else DHCP). The MAC and address are pinned inside the guest by its
own networkd, the only stable MAC pin for a nested container. A guest
naming a VLAN its host has not declared fails the build with an
actionable message.
The `br-vlan<id>` naming moves into a shared `bridgeName` in the lib, so
the bridge a guest attaches to and the bridge the host emits have one
source.
Introduce `modules.network`, the host-level networking foundation a Host
declares once. A Host states its trunk interface and the tagged VLAN ids to
materialize, and the module emits one systemd-networkd bridge per VLAN, named
by the `br-vlan<id>` convention, plus the host's own management address on a
chosen VLAN's bridge.
The trunk and every bridge set `RequiredForOnline = "no"` so wait-online never
blocks boot on a carrier-less link, and the module owns its own NetworkManager
`unmanaged` guard so enabling it is self-sufficient. Two assertions tie the
management VLAN to the declared VLANs and a management address to a VLAN, so an
address can never be silently dropped.
No host commits an enablement: the only host is a wifi laptop that cannot
present guests as L2 citizens and whose DNS the module's networkd/resolved
would disturb, so the standing enablement waits for the first wired server
host. The build and bridge-name evaluation were verified by enabling the
module ad hoc, and stay reproducible through `extendModules`.
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.
Add modules.toolkit, an aggregator that enables fish, tmux, nvim, git,
and direnv as one unit, and makes fish the login shell, so any Host or
Guest shell feels identical. Each member is set via mkDefault, so a Host
can still override any single piece while the one flag brings up the
whole bundle.
neogaia now enables the bundle as a single line in place of its five
individual enables and its explicit default-shell line, keeping the Host
a flat checklist.
Also lands the domain model, ADR 0006, and the guests spec: the toolkit
is the first piece of the wider guests work, and the guest-base will
auto-enable this bundle.
The .claude specs and tasks have been distilled into the notes vault, so
remove the 37 consumed files. Trim the CLAUDE.md gotchas to their
project-specific residue: drop the entries now captured as general notes,
and fix the two references that pointed at the deleted specs.
Source the wiki and consume skills from the skills flake, placing them
globally so they are active in every project. wiki reads the personal
Obsidian vault read-only, and consume mines a source into it, pairing
with the Obsidian vault this branch installs.
Set NIXOS_OZONE_WL in the compositor env so Chromium and Electron
wrappers select native Wayland rather than XWayland, which blurs at this
DPI. Deliver it through Hyprland's env directive because UWSM starts the
session without the shell profile home-manager's sessionVariables rely
on.
Add a standalone modules.desktop.obsidian module and enable it on
neogaia. Give it a waybar window-rewrite icon so its windows show a
recognisable glyph on the workspace indicator.
Apply a codebase-wide comment audit against the comment conventions:
split banned semicolons and multi-sentence lines into one sentence per
line, cut cross-file and history narration, trim file-top headers to a
single purpose line, and drop verbosity that did not earn its place.
Prose docs (CLAUDE.md, install.md) get the same one-sentence-per-line
and no-semicolon treatment.
The greeting always drew the full NixOS logo beside the info block,
which overflows in a narrow terminal. Choose the logo by column count:
the full logo when it fits, the compact one when it does not, and none
below that.
Left at auto, Firefox reads the panel's 1.5x and inflates its whole
chrome while point-sized apps stay put. Pin devPixelsPerPx a shade under
that so the UI and pages match without dropping to a too-small 1:1.
The graphical layer inherited Stylix's default font sizes, which read
oversized on this display. Step them down a point across applications,
desktop, popups, and the terminal.
home.sessionVariables (XCURSOR_THEME/XCURSOR_SIZE from home.pointerCursor)
land in hm-session-vars.sh, which only a login shell sources. UWSM starts
the graphical session without it, so Hyprland never saw the theme and drew
its built-in cursor instead of the configured Bibata-Modern-Ice.
Declare the cursor through Hyprland's own env directive, read at compositor
startup regardless of the shell profile, sourced from config.stylix.cursor
so the theme and size stay defined in one place.
Route the volume and brightness keys through swayosd-client, which raises
a transient on-screen popup that auto-hides a moment after the last press,
so the level is visible while adjusting without a permanent widget. A new
osd module runs the server for the graphical session and makes the
backlight node group-writable so it dims without root.
Supersedes the plain brightnessctl binds, whose control swayosd bundles.
Bind XF86MonBrightnessUp/Down to brightnessctl in the repeat-while-held
block, mirroring the volume keys. A new brightness module installs
brightnessctl's udev rule and adds the user to the video group so the
backlight can be dimmed without root.
Stylix's Nord Firefox Color mapping paints the selected address-bar result in
base04, a near-white grey, against base05 text — light on light, so the text
of the highlighted result is illegible. Override that one highlight to the Nord
selection grey (base03), derived from the same Stylix scheme, restoring
contrast without touching the rest of the theme.
Firefox disables profile-sideloaded extensions by default, so the Firefox
Color add-on that carries the Nord chrome theme installed but stayed disabled
and the chrome kept its default look. Set extensions.autoDisableScopes = 0 so
it enables on detection; the policy-installed functional extensions are
force-enabled by policy and unaffected.
The base Stylix Firefox target themes only fonts and the reader view, leaving
the toolbar and tabs at Firefox's default look. Enable colorTheme so Stylix
recolours the chrome Nord from the shared scheme through its managed Firefox
Color add-on, with no hand-written chrome CSS and no flake input of ours.
Scope ADR 0005's no-pinned-add-on rule to the three functional extensions and
record the signed, Stylix-managed theming add-on as a deliberate exception.
Firefox writes ~/.config/mozilla/firefox/profiles.ini itself on first launch,
pointing at an auto-created random-prefix profile. Home-manager then refuses to
clobber that file and fails the whole activation, so the declarative default
profile — search.json.mozlz4 (DuckDuckGo) and the Stylix user.js — never lands
and Firefox keeps running the stale profile with Google as default and no theme.
Set force = true on the generated profiles.ini home.file entry so home-manager
owns it, deploys the default profile, and Firefox switches to it.
Add Firefox as a single-purpose desktop Module configured entirely through
home-manager's programs.firefox, and fan it out from the desktop aggregator so
any Host with the desktop enabled brings the browser up.
Ship stock mainline Firefox, hardened and de-monetized through locked
enterprise policies (telemetry, studies, Pocket, offer-to-save-logins, the
default-browser check, Firefox accounts and sync, and the sponsored new-tab
surfaces all off). Force-install three extensions by policy, keyed by their
verified add-on ids: uBlock Origin, Proton Pass, and SponsorBlock.
Default search to DuckDuckGo over a lean list: the general-purpose commercial
engines are hidden with metaData.hidden so Firefox does not reconcile them back
in, leaving DuckDuckGo and Wikipedia. Theme the browser Nord from the shared
Stylix scheme against the one profile, and register it as the default handler
for the web-link schemes and HTML.
Record the stock-Firefox-plus-policy-extensions decision as ADR 0005.
The task 0031 commit moved the module files but a mis-staged `git add`
left the option-path rewrites out, so main declared the old namespaces
(`modules.claude-code`, `modules.gitea-axi`, `modules.pi`,
`modules.desktop.hyprlock`/`hypridle`) while CONTEXT.md and ADR 0004
already documented the new ones. Rewrite the paths in the moved and
in-place modules, the neogaia host, and the two live CLAUDE.md gotchas
so the code matches the recorded convention.
Adopt the convention that a Module's option path mirrors its directory
under modules/, with an index file naming the directory's own segment.
- Group agent Modules under modules.agents.*: claude-code (whole
directory), pi (flattened to a file), skills (renamed from
agent-skills), and gitea-axi under an agents/tools/ subgroup. The
agents/ and tools/ folders are pure namespace prefixes with no
aggregator enable.
- Nest hypridle and hyprlock under modules.desktop.hyprland.*, with
hyprland.nix as the index, and update the desktop aggregator.
- Remove the obsolete example Module.
- Record the convention in CONTEXT.md and ADR 0004, and update the
neogaia host, the two live CLAUDE.md gotchas, and the skills Module's
intentional Enable-convention exception comment.