diff --git a/.agents/skills/nix-config-desktop/SKILL.md b/.agents/skills/nix-config-desktop/SKILL.md
index fcdfaf60..b23f9727 100644
--- a/.agents/skills/nix-config-desktop/SKILL.md
+++ b/.agents/skills/nix-config-desktop/SKILL.md
@@ -36,20 +36,20 @@ back with git, and needs no rebuild. Only the store layer needs one.
## 1. Pick the layer
-| Change | Where it lives | Applies |
-| --------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------ |
-| Niri compositor (binds, layout, window rules) | `home/linux/gui/niri/conf/*.kdl` | live |
-| Niri per-host outputs | `hosts/
/niri-hardware.kdl` | live |
-| Noctalia shared baseline | `home/linux/gui/base/noctalia/config/config.toml` | live |
-| Noctalia per-host override | `home/hosts/linux//noctalia.toml` (`host-.toml`) | store: needs `just niri` |
-| Noctalia value saved by the Settings UI | `~/.local/state/noctalia/settings.toml` (not in the repo) | live, wins over the rest |
-| Mozc dictionary | `home/linux/gui/base/fcitx5/mozc-config1.db` | live |
-| fcitx5 profile and addons | `home/linux/gui/base/fcitx5/` | store: needs `just niri` |
-| vinput (voice input) | `home/linux/gui/base/vinput/` (tag-pinned: the `fcitx5-vinput` row in WORKAROUNDS.md §Pins) | store: needs `just niri` |
-| Interface a net-speed widget reads | `home/hosts/linux//noctalia.toml` (`[widget.net_rx]`/`[widget.net_tx]` `interface`) | store: needs `just niri` |
-| `$HOME` state that must survive | `hosts//preservation.nix` (tmpfs root) | store: needs `just niri` |
-| Theme (catppuccin), fonts | `home/base/core/theme.nix`, `modules/nixos/desktop/fonts.nix` | store: needs `just niri` |
-| Session, portal, and systemd wiring | `modules/nixos/desktop/**`, `home/linux/gui/base/xdg/` | store: needs `just niri` |
+| Change | Where it lives | Applies |
+| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------ |
+| Niri compositor (binds, layout, window rules) | `home/linux/gui/niri/conf/*.kdl` | live |
+| Niri per-host outputs | `hosts//niri-hardware.kdl` | live |
+| Noctalia shared baseline | `home/linux/gui/base/noctalia/config/config.toml` | live |
+| Noctalia per-host override | `home/hosts/linux//noctalia.toml` (`host-.toml`) | store: needs `just niri` |
+| Noctalia value saved by the Settings UI | `~/.local/state/noctalia/settings.toml` (not in the repo) | live, wins over the rest |
+| Mozc dictionary | `home/linux/gui/base/fcitx5/mozc-config1.db` | live |
+| fcitx5 profile and addons | `home/linux/gui/base/fcitx5/` | store: needs `just niri` |
+| vinput (voice input) | `home/linux/gui/base/vinput/` (tag-pinned: the `fcitx5-vinput` row in WORKAROUNDS.md §Pins) | store: needs `just niri` |
+| Interface a net-speed widget reads | `home/hosts/linux//noctalia.toml` (`[widget.net_rx]`/`[widget.net_tx]` `interface`) | store: needs `just niri` |
+| `$HOME` state that must survive | `hosts//preservation.nix` (tmpfs root; `12kingdoms-shoukei` imports `hosts/idols-ai/preservation.nix`) | store: needs `just niri` |
+| Theme (catppuccin), fonts | `home/base/core/theme.nix`, `modules/nixos/desktop/fonts.nix` | store: needs `just niri` |
+| Session, portal, and systemd wiring | `modules/nixos/desktop/**`, `home/linux/gui/base/xdg/` | store: needs `just niri` |
Noctalia's merge order and `[include]` are in
[home/linux/gui/base/README.md](../../../home/linux/gui/base/README.md). A per-host difference goes
diff --git a/SECURITY.md b/SECURITY.md
index f43956d5..f3c10fa3 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -200,8 +200,8 @@ checks remain necessary after deployment.
`root_squash` is deferred: the existing PVC subdirectories are root-owned (observed
`drwxr-xr-x root root` and `drwxrwsr-x root 107`), so enabling it needs a one-time ownership
migration and a maintenance window. See WORKAROUNDS.md.
-- **Remote consoles:** the libvirt VNC consoles listen on `127.0.0.1` only, and the shared firewall
- drops 5900-5910 on every host. Reach a console through an SSH tunnel.
+- **Remote consoles:** the libvirt VNC consoles listen on `127.0.0.1` only, and the k3s firewall
+ drops 5900-5903 from pod sources. Reach a console through an SSH tunnel.
- **Pod-to-host firewall:** two independent layers deny pod access to the credential-bearing host
ports (SSH, NFS, Postgres, restic, VNC, Immich, exporter/monitoring). The
`deny-pod-to-node-admin-ports` `CiliumClusterwideNetworkPolicy` in
diff --git a/WORKAROUNDS.md b/WORKAROUNDS.md
index 1096893f..f57160b6 100644
--- a/WORKAROUNDS.md
+++ b/WORKAROUNDS.md
@@ -44,6 +44,7 @@ A setting that works around a bug or a hardware quirk.
| WA-019 | programs.steam.extraCompatPackages = [ pkgs.dwproton-bin pkgs.proton-ge-bin ] — modules/nixos/desktop/gaming.nix | ACE games fail on GE/mainline Proton, work with DW-Proton | Drop once mainline or GE-Proton runs these games (GE-Proton#433) | 2026-10-07 | on Proton/GE bump | active |
| WA-024 | Wi-Fi power saving disabled (`networking.networkmanager.wifi.powersave = false`) — `hosts/12kingdoms-shoukei/apple-silicon.nix` | On this BCM4378/brcmfmac laptop, powersave is a common source of intermittent ASSOC-REJECT / authentication-timeout failures; NetworkManager then mistakes the failure for a bad key, re-prompts, and the client (Noctalia, nmtui, nmcli) saves the answer as a duplicate ` 1` profile | Drop once the kernel carries the brcmfmac `port_authorized` fix (WA-025) and no association failures remain | 2026-10-07 | watch `journalctl -u wpa_supplicant` for ASSOC-REJECT / authentication timeout | active |
| WA-026 | guest deploys run `installOnHost` then `systemctl restart microvm@` on the physical host — `Justfile`, `lib/genMicrovmGuestModule.nix` | the sshSwitch step runs `nix-store --load-db` against the guest's read-only virtiofs `/nix/store` and dies on `cannot remount "/nix/store" writable` (nix 2.35) | Drop the manual path once microvm.nix or nix handles a read-only store switch. A host activation re-links `/var/lib/microvms//current` to the runner baked into the host's system, so switch the host after installing a guest from a newer tree | 2026-10-09 | on microvm.nix or nix bump | active |
+| WA-028 | GitHub SSH pinned to IPv4 (`AddressFamily = "inet"`) — `home/base/tui/ssh.nix` | a v6 fake AAAA for `ssh.github.com` from the `2001:2::/64` pool (WA-029) is not proven on the 443 SSH tunnel yet | Drop once the v6 fake-ip pool is proven for GitHub SSH | 2026-10-10 | on a mihomo bump or a v6 pool change | active |
| WA-029 | mihomo `fake-ip-range6` is `2001:2::/64`, not the shipped ULA default — `modules/nixos/desktop/networking/mihomo/generate.nu` | the default `fdfe:dcba:9876::/64` sits in `fc00::/7`, which Local Network Access calls local, so a public page loaded over the v4 fake pool and pulling a dual-stack subresource prompts for local-network access (Chrome 154, Firefox). The same range also stalled ssh: a session to a fake AAAA authenticated and then never closed, on both `gvisor` and `mips`. `2001:2::/48` is RFC 5180 benchmarking -- the v6 twin of the `198.18.0.0/15` v4 default, absent from that table -- and both symptoms went with the move | Go back to the upstream default once mihomo ships a pool outside the LNA non-public table; the ULA default brings the ssh stall back | 2026-10-09 | on a browser or mihomo bump -- that table already grew to cover `2001:db8::/32` and `198.18.0.0/15` | active |
If WA-008 freezes recur, the fallback is to restore
@@ -72,7 +73,7 @@ An unfinished feature or gap.
| ID | What & where | Why | Removal condition | Added | Revisit | Status |
| ------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------- | ----------------------- | ------ |
| WA-013 | NixOS tests — `outputs/README.md` | full-host tests still rebuild custom packages (no private cache) and need the host ssh key to decrypt agenix; the shared-firewall security-exporters VM test now runs in CI (KVM) | Resolve when a private cache exists and tests can run per-service | 2024-03-13 | quarterly | active |
-| WA-020 | NFS golden store `no_root_squash` — `hosts/12kingdoms-youko/nfs-golden.nix` | the existing PVC subdirectories are root-owned, so enabling `root_squash` would break running workloads until they are migrated; the CSI node plugin is hostNetwork and sources from the allowlisted node IPs | Drop when the PVC subdirectories are migrated and `root_squash` is enabled | 2026-10-07 | next maintenance window | active |
+| WA-027 | NFS golden store `no_root_squash` — `hosts/12kingdoms-youko/nfs-golden.nix` | the existing PVC subdirectories are root-owned, so enabling `root_squash` would break running workloads until they are migrated; the CSI node plugin is hostNetwork and sources from the allowlisted node IPs | Drop when the PVC subdirectories are migrated and `root_squash` is enabled | 2026-10-07 | next maintenance window | active |
## Related
diff --git a/docs/nixos-modernization-options.md b/docs/nixos-modernization-options.md
index ce9fdf44..85a99eb5 100644
--- a/docs/nixos-modernization-options.md
+++ b/docs/nixos-modernization-options.md
@@ -1,8 +1,8 @@
# NixOS Modernization — Options and Decision Record
> **Purpose:** let the maintainer decide, per candidate, whether it is worth adopting — by comparing
-> it against what this repository does **today**. The plan documents under `docs/plans/` are
-> downstream of this file: a plan item is only approved work once its card here says **adopt**.
+> it against what this repository does **today**. A plan derived from this file is only approved
+> work once its card here says **adopt**.
>
> Researched against nixpkgs `nixos-unstable` on 2026-10-07. Option names and upstream wording are
> quoted from the module sources cited in each card.
diff --git a/home/linux/gui/i3/README.md b/home/linux/gui/i3/README.md
index 4a377e78..be27b9dd 100644
--- a/home/linux/gui/i3/README.md
+++ b/home/linux/gui/i3/README.md
@@ -41,7 +41,7 @@ tree the agent reads, so it stays.
services: `xvfb` → `i3` → (`x11vnc`, `autocutsel-primary` / `autocutsel-clipboard`,
`cua-driver serve`).
- `modules/nixos/desktop/computer-use.nix` provides the system side (`linger`, AT-SPI, fonts, Mesa,
- Clash Verge proxy, proxy-region timezone).
+ and a `proxyRegion` option that pins the timezone to the proxy's exit region).
- the generated `computer-use-init` script (exec'd by i3) publishes `DISPLAY`/`XDG_SESSION_TYPE` to
the systemd user manager, clears any stale `WAYLAND_DISPLAY`, and enables the AT-SPI bridge via a
gsettings key.
diff --git a/hosts/idols-ruby/README.md b/hosts/idols-ruby/README.md
index 89449ccd..5e7666ac 100644
--- a/hosts/idols-ruby/README.md
+++ b/hosts/idols-ruby/README.md
@@ -2,7 +2,7 @@
NixOS VM running under libvirt on `shoryu`, used to run AI agents. It owns the shared agent and
computer-use modules (`packages.nix`, `computer-use.nix`, `oci-containers/`) that
-[`idols-kana`](../idols-kana/) imports, and is the only host with a `*-nixos-tests` output.
+[`idols-kana`](../idols-kana/) imports.
- Deploy from the VM with `just local`.
- Host inventory and role context: [`../README.md`](../README.md).
diff --git a/modules/nixos/desktop/networking/mihomo/README.md b/modules/nixos/desktop/networking/mihomo/README.md
index 61b19689..89094431 100644
--- a/modules/nixos/desktop/networking/mihomo/README.md
+++ b/modules/nixos/desktop/networking/mihomo/README.md
@@ -12,9 +12,11 @@ just mihomo-gen # -> ~/.config/mihomo/config.yam
sudo systemctl restart mihomo.service # dashboard: http://127.0.0.1:9090/ui
```
-`~/.config/mihomo` holds only `sources.yaml` and the generated `config.yaml` (plus geodata and the
-cache); the generator, `policy.yaml` and `gateway-config.nu` are run from here and never copied
-over. `just mihomo-gen` renders the config and validates it with the core the service runs.
+`~/.config/mihomo` holds `sources.yaml` and the generated `config.yaml`, which the service loads
+through systemd `LoadCredential`; the core's own state — geodata and cache — is in
+`/var/lib/private/mihomo` (`StateDirectory`, `DynamicUser`). The generator, `policy.yaml` and
+`gateway-config.nu` are run from here and never copied over. `just mihomo-gen` renders the config
+and validates it with the core the service runs.
`sources.yaml` holds everything private (subscription URLs, secret, private domains) and stays out
of the repo and the Nix store; the module loads `config.yaml` via `LoadCredential`. Generate the
diff --git a/outputs/README.md b/outputs/README.md
index bbb828fa..5a399154 100644
--- a/outputs/README.md
+++ b/outputs/README.md
@@ -46,10 +46,10 @@ suite passes when every `expr` equals its `expected`.
Some tests iterate over every configuration, so a new host is covered automatically and must satisfy
them. Others name specific hosts and only cover a new host if you add it.
-| Kind | Tests |
-| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Every configuration | `hostname`, `kernel`, `nix-system-features`, `security-apparmor`, `security-container-groups`, `security-exporters`, `security-firewall`, `security-k3s-kubeconfig`, `security-kernel`, `security-ssh-x11` |
-| Named hosts only | `btrbk`, `home-manager`, `home-manager-xdg`, `computer-use-headless`, `k3s-api-endpoint`, `k3s-master-home-manager`, `idols-ai-gpu`, `youko-metrics`, `ups-metrics`, `shoukei-logind` |
+| Kind | Tests |
+| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Every configuration | `hostname`, `kernel`, `nix-system-features`, `security-apparmor`, `security-container-groups`, `security-exporters`, `security-firewall`, `security-k3s-kubeconfig`, `security-k3s-pod-firewall`, `security-kernel`, `security-ssh-x11` |
+| Named hosts only | `btrbk`, `home-manager`, `home-manager-xdg`, `computer-use-headless`, `k3s-api-endpoint`, `k3s-master-home-manager`, `idols-ai-gpu`, `idols-ai-voice-input`, `security-postgres-auth`, `youko-metrics`, `ups-metrics`, `shoukei-logind` |
`hostname` expects each configuration's `networking.hostName` to equal its name. Niri desktop
configurations are the exception (`ai-niri` → `ai`, `shoukei-niri` → `shoukei`) and are listed in
@@ -71,15 +71,8 @@ Problems:
agenix secrets.
- [ ] Maybe it's better to test every service separately, not the whole host?
-How to run a host's NixOS tests:
-
-```bash
-# Format: nix build .#-nixos-tests
-
-nix build .#ruby-nixos-tests
-```
-
-Only `ruby` currently defines a `*-nixos-tests` output.
+No host defines a NixOS integration test yet: full-host tests are tracked as WA-013 in
+[WORKAROUNDS.md](../WORKAROUNDS.md). What runs today is the eval set above, with `just test`.
## Overview