refactor: host profiles; give ruby, kana and aquamarine the TUI home (#512)

- Add the agent profile (modules/nixos/agent.nix: rootless Podman, linger,
  nix-ld) with home/linux/tui.nix as its home role; ruby, kana and
  aquamarine use it, so hosts no longer import from hosts/idols-ruby.
- Gate nushell's ~/.secrets source behind modules.shell.secrets.enable,
  off on agent hosts.
- Persist the TUI home state on aquamarine's tmpfs root.
- Document host profiles in hosts/README.md; add tests/agent-profile and
  drop the x86_64 colmena-parity test, which needs mysecrets.

Assisted-by: Claude Code (claude-opus-5-5)
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-11-03 04:57:06 -06:00
1 parent eb6f6c34f0
commit 9a307b313d
31 files changed
+301 -147

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ home/
│ │ └── ...
│ └── ...
├── hosts/ # Host-specific home manager entry modules
│ ├── linux/ # Linux host home modules (idols-ai, 12kingdoms-shoukei, idols-kana, idols-ruby)
│ ├── linux/ # Linux host home modules (idols-ai, 12kingdoms-shoukei, idols-aquamarine, idols-kana, idols-ruby)
│ └── darwin/ # macOS host home modules (fern, frieren)
└── darwin/ # macOS-specific home manager configurations
├── proxy/ # Proxy configurations
+12 -3
View File
@@ -1,17 +1,26 @@
{
config,
lib,
nu_scripts,
...
}:
{
programs.nushell = {
options.modules.shell.secrets.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Source ~/.secrets/nushell-secrets.nu, which only the desktops sync.";
};
config.programs.nushell = {
# the file must exist, otherwise nushell will complain about it!
#
# currently, nushell does not support conditional sourcing of files
# https://github.com/nushell/nushell/issues/8214
extraConfig = ''
# decrypted secret: AI agents must not read it
source ${config.home.homeDirectory}/.secrets/nushell-secrets.nu
${lib.optionalString config.modules.shell.secrets.enable ''
# decrypted secret: AI agents must not read it
source ${config.home.homeDirectory}/.secrets/nushell-secrets.nu
''}
# Directories in this constant are searched by the
# `use` and `source` commands.
+3 -2
View File
@@ -10,8 +10,9 @@ This directory contains host-specific Home Manager entry modules.
## Conventions
1. Each host output should reference only one file under `home/hosts/...`.
2. Shared home module imports should be handled in the host file itself.
- Linux hosts usually import `../../linux/core.nix` or `../../linux/gui.nix`.
2. The host file imports its profile's home role, see
[Host Profiles](../../hosts/README.md#host-profiles).
- `desktop` hosts import `../../linux/gui.nix`; `agent` hosts import `../../linux/tui.nix`.
- Darwin hosts import `../../darwin`.
3. Host-specific overrides (SSH keys, desktop toggles, host-local config links) live in the same
host file.
+5
View File
@@ -0,0 +1,5 @@
{
imports = [
../../linux/tui.nix
];
}
+1 -2
View File
@@ -1,7 +1,6 @@
{
imports = [
../../linux/core.nix
../../linux/gui/i3
../../linux/tui.nix
];
# Headless X11/i3 session for computer use.
+1 -2
View File
@@ -1,7 +1,6 @@
{
imports = [
../../linux/core.nix
../../linux/gui/i3
../../linux/tui.nix
];
# Headless X11/i3 session for computer use.
+3 -2
View File
@@ -46,8 +46,9 @@ tree the agent reads, so it stays.
the systemd user manager, clears any stale `WAYLAND_DISPLAY`, and enables the AT-SPI bridge via a
gsettings key.
- Drivers come from the nur-ryan4yin overlay (`overlays/nur-ryan4yin.nix`).
- Host wiring: `home/hosts/linux/idols-ruby.nix`, `home/hosts/linux/idols-kana.nix`,
`hosts/idols-ruby/computer-use.nix`.
- Wiring: the `agent` profile imports both sides, off by default
([Host Profiles](../../../../hosts/README.md#host-profiles));
`hosts/idols-{ruby,kana}/default.nix` and `home/hosts/linux/idols-{ruby,kana}.nix` turn them on.
## The i3 config
+7
View File
@@ -1,3 +1,5 @@
# Home role of the `agent` profile (hosts/README.md#host-profiles): the full
# TUI stack for a headless host.
{
imports = [
../base/core
@@ -5,5 +7,10 @@
../base/home.nix
./base
# off by default; a host turns it on with modules.desktop.computerUse.enable
./gui/i3
];
# ~/.secrets is synced to the desktops only.
modules.shell.secrets.enable = false;
}
+47
View File
@@ -5,6 +5,53 @@ This directory contains all host-specific configurations for my NixOS and macOS
Manual firmware settings are recorded in [`docs/bios.md`](../docs/bios.md), and the shared
Wake-on-LAN workflow is in [`docs/wake-on-lan.md`](../docs/wake-on-lan.md).
## Host Profiles
Every host is exactly one profile. The profile fixes the system module that
`outputs/<system>/src/<host>.nix` imports and the Home Manager role that
`home/hosts/<platform>/<host>.nix` imports. A host directory only adds hardware, networking,
persisted paths and feature switches on top.
| Profile | System module (in outputs) | Home role (in home/hosts) | Hosts |
| --------- | --------------------------------- | ------------------------- | ------------------------------------------- |
| `desktop` | `modules/nixos/desktop.nix` | `home/linux/gui.nix` | `ai`, `shoukei` |
| `agent` | `modules/nixos/agent.nix` | `home/linux/tui.nix` | `aquamarine`, `ruby`, `kana` |
| `server` | `modules/nixos/server/server.nix` | none | `shoryu`, `shushou`, `youko`, k3s, `akane`¹ |
| `darwin` | `modules/darwin` | `home/darwin` | `fern`, `frieren` |
¹ `akane` (aarch64) uses `server-aarch64.nix`; the k3s nodes import `server.nix` from their host
directory.
- **desktop**: Niri session and GUI apps on top of the full TUI stack.
- **agent**: headless, with the full TUI stack from Home Manager (shell, editors, dev toolchains,
cloud CLIs, AI agents with their rules and skills), and nix-ld, rootless Podman and linger from
the system. Computer use (Xvfb + i3, VNC) is part of the profile but off by default.
- **server**: system services only, no Home Manager.
The `agent` hosts differ only in:
| | `aquamarine` | `ruby` | `kana` |
| ------------ | ------------------------------------------ | ------------------- | ------------- |
| Computer use | off | on, with cua-driver | on |
| Hardware | physical; 2x R9700, NPU speech recognition | libvirt guest | libvirt guest |
| Root | tmpfs; `preservation.nix` keeps home state | disk | disk |
`outputs/x86_64-linux/tests/agent-profile` checks the `agent` rows.
### Where a new piece of configuration goes
1. A tool you run from a shell goes to the Home Manager role (`home/base/core`, `home/base/tui`),
never to a host's `environment.systemPackages`.
2. Anything that needs root or a system service (nix-ld, Podman, drivers, systemd units) goes to the
profile's system module, or to a `modules/nixos` module the profile imports.
3. Hardware, networking, the hostname, persisted paths and feature switches go to the host
directory.
A host does not import files from another host's directory; shared code moves under `modules/` or
`home/` instead. Existing exceptions, between machines on identical hardware: `shushou` and `youko`
reuse `12kingdoms-shoryu`'s hardware and preservation files, and `shoukei` reuses `idols-ai`'s
preservation file.
## Current Host Inventory
### Physical Machines
-3
View File
@@ -15,9 +15,6 @@ in
{
imports = (mylib.scanPaths ./.) ++ [
disko.nixosModules.default
../idols-ruby/packages.nix
../idols-ruby/oci-containers
];
# supported file systems, so we can mount any removable disks with these filesystems
+3 -8
View File
@@ -67,14 +67,9 @@ in
# VictoriaMetrics.
services.smartd.enable = true;
# The inference engine ships as an OCI image and is meant to run rootless, so
# `podman` is what this host uses to build and start it from the `~/src` checkout
# that ./preservation.nix keeps.
virtualisation.podman.enable = true;
# Keep the user systemd manager running after logout and start it at boot,
# so enabled user services can run without an active login session.
users.users."${myvars.username}".linger = true;
# The inference engine ships as an OCI image and runs under the agent
# profile's rootless Podman (modules/nixos/agent.nix), built from the `~/src`
# checkout that ./preservation.nix keeps.
# Speech recognition runs on the NPU, see ./asr.nix. The driver and the
# compiler it loads come from NixOS/nixpkgs#570230, which is unmerged, so they
+93
View File
@@ -85,6 +85,84 @@ in
".config/systemd/user"
".config/containers/systemd"
# ======================================
# Home Manager TUI role (home/linux/tui.nix); the same paths `ai` keeps
# ======================================
".local/state/home-manager"
".local/state/nix/profiles"
".local/share/nix"
".config/nushell" # history
# ai agents
".agents" # skills for all agents
".config/agents"
".codex"
".dsh" # DeepSeek Harness
".pi"
".config/opencode"
".local/share/opencode"
".local/state/opencode"
# ctx7 docs lookup login
{
directory = ".config/context7";
mode = "0700";
}
# editors
".local/share/nvim"
".local/state/nvim"
".local/share/jupyter"
# tuios session state, project-tape trust store and layout templates
{
directory = ".local/state/tuios";
mode = "0700";
}
{
directory = ".local/share/tuios";
mode = "0700";
}
".config/tuios"
# cloud native
{
directory = ".aws";
mode = "0700";
}
{
directory = ".aliyun";
mode = "0700";
}
{
directory = ".config/gcloud";
mode = "0700";
}
{
directory = ".config/gh";
mode = "0700";
}
{
directory = ".kube";
mode = "0700";
}
".terraform.d/plugin-cache"
# language package managers
".npm"
"go"
".cargo"
".local/bin"
".local/share/uv"
{
directory = ".gnupg";
mode = "0700";
}
{
directory = ".ssh";
mode = "0700";
}
];
};
};
@@ -123,6 +201,21 @@ in
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.local/state".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.local/state/nix".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
"/home/${username}/.terraform.d".d = {
user = username;
group = lib.mkForce username;
mode = lib.mkForce "0750";
};
};
systemd.tmpfiles.rules = [
+2 -2
View File
@@ -1,7 +1,7 @@
# Idols - Kana
NixOS VM running under libvirt on `shoryu`, used to run AI agents. It imports the agent and
computer-use modules from [`idols-ruby`](../idols-ruby/).
NixOS VM running under libvirt on `shoryu`, used to run AI agents. Profile: `agent`, see
[Host Profiles](../README.md#host-profiles).
- Deploy from the VM with `just local`.
- Host inventory and role context: [`../README.md`](../README.md).
+2 -5
View File
@@ -16,11 +16,8 @@ let
ipv4WithMask = "${ipv4}/24";
in
{
imports = [
../idols-ruby/packages.nix
../idols-ruby/computer-use.nix
../idols-ruby/oci-containers
];
# Headless computer-use environment (Xvfb + i3 + desktop automation + browser).
modules.desktop.computerUse.enable = true;
# supported file systems, so we can mount any removable disks with these filesystems
boot.supportedFilesystems = [
+2 -3
View File
@@ -1,8 +1,7 @@
# Idols - Ruby
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.
NixOS VM running under libvirt on `shoryu`, used to run AI agents. Profile: `agent`, see
[Host Profiles](../README.md#host-profiles).
- Deploy from the VM with `just local`.
- Host inventory and role context: [`../README.md`](../README.md).
-4
View File
@@ -1,4 +0,0 @@
{
# Headless computer-use environment (sway + desktop automation + browser).
modules.desktop.computerUse.enable = true;
}
+2 -5
View File
@@ -16,11 +16,8 @@ let
ipv4WithMask = "${ipv4}/24";
in
{
imports = [
./packages.nix
./computer-use.nix
./oci-containers
];
# Headless computer-use environment (Xvfb + i3 + desktop automation + browser).
modules.desktop.computerUse.enable = true;
# Enable binfmt emulation of aarch64-linux, this is required for cross compilation.
boot.binfmt.emulatedSystems = [
@@ -1,17 +0,0 @@
{
mylib,
lib,
pkgs,
...
}:
{
imports = mylib.scanPaths ./.;
virtualisation = {
docker.enable = true;
oci-containers = {
backend = "docker";
};
};
}
+1
View File
@@ -43,6 +43,7 @@ modules/
│ ├── trash.nix # Trash retention cleanup
│ ├── user-group.nix # User and group management
│ └── zram.nix # ZRAM swap configuration
├── agent.nix # `agent` profile: headless host with the TUI home role
├── desktop.nix # Desktop environment configuration
├── desktop/ # Desktop-specific modules
│ ├── canokey.nix # CanoKey hardware token
@@ -1,12 +1,29 @@
{
pkgs,
llm-agents,
myvars,
...
}:
let
agentPackages = llm-agents.packages.${pkgs.stdenv.hostPlatform.system};
in
#############################################################
#
# The `agent` profile (hosts/README.md#host-profiles): a headless
# host whose user tools come from the Home Manager TUI role
# (home/linux/tui.nix). This module only carries what needs root.
#
#############################################################
{
imports = [
./server/server.nix
# off by default; a host turns it on with modules.desktop.computerUse.enable
./desktop/computer-use.nix
];
# Rootless Podman for agents and the images they build.
virtualisation.podman.enable = true;
# Keep the user systemd manager running after logout and start it at boot,
# so agent and computer-use user services run without a login session.
users.users."${myvars.username}".linger = true;
# https://github.com/Mic92/nix-ld
#
# nix-ld will install itself at `/lib64/ld-linux-x86-64.so.2` so that
@@ -32,60 +49,4 @@ in
stdenv.cc.cc
];
};
environment.systemPackages =
with pkgs;
[
nodejs_24
pnpm
#-- python
uv # python project package manager
(python313.withPackages (
ps: with ps; [
pandas
requests
pyquery
pyyaml
numpy
# model downloaders
huggingface-hub
]
))
rustc
cargo # rust package manager
go
# cryptography
age
sops
rclone
gnupg
# cloud-native
kubectl
istioctl
kubernetes-helm
fluxcd
terraform
# db related
pgcli
mongosh
sqlite
yt-dlp # youtube/bilibili/soundcloud/... video/music downloader
]
# AI Agent Tools
++ [
# Agents
agentPackages.codex
agentPackages.opencode2
agentPackages.pi
# dsh from nur-packages: llm-agents.nix's dsh cannot boot on nixpkgs node
# (node-addon-require-builtin probe), this one shims it.
pkgs.dsh
];
}
+4 -4
View File
@@ -47,10 +47,10 @@ Some tests iterate over every configuration, so a new host is covered automatica
them. Others name specific hosts — or a name prefix, like `k3s-api-endpoint`, which covers every
`k3s-test-1-*` configuration — 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-k3s-pod-firewall`, `security-kernel`, `security-ssh-x11` |
| Named hosts only | `btrbk`, `cpu-power`, `home-manager`, `home-manager-xdg`, `computer-use-headless`, `k3s-api-endpoint`, `k3s-node-home-manager`, `idols-ai-gpu`, `idols-ai-net`, `idols-ai-voice-input`, `idols-aquamarine`, `security-postgres-auth`, `youko-metrics`, `ups-metrics`, `shoukei-logind`, `mihomo-dns`, `colmena-parity`, `microvm-guest-parity` |
| 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`, `cpu-power`, `home-manager`, `home-manager-xdg`, `computer-use-headless`, `agent-profile`, `k3s-api-endpoint`, `k3s-node-home-manager`, `idols-ai-gpu`, `idols-ai-net`, `idols-ai-voice-input`, `idols-aquamarine`, `security-postgres-auth`, `youko-metrics`, `ups-metrics`, `shoukei-logind`, `mihomo-dns`, `colmena-parity`, `microvm-guest-parity` |
`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
@@ -3,4 +3,7 @@
inputs,
}:
# Colmena must deploy the system that nixosConfigurations evaluates and builds.
# Every node comes out of the same rawColmenaHive code in outputs/default.nix, so
# akane covers the x86_64 nodes too; those all read the private mysecrets input,
# which CI cannot fetch.
lib.genAttrs [ "akane" ] (name: inputs.self.colmenaHive.toplevel.${name}.drvPath)
@@ -22,8 +22,8 @@ let
modules = {
nixos-modules =
(map mylib.relativeToRoot [
# common
"modules/nixos/server/server.nix"
# profile: agent (hosts/README.md#host-profiles)
"modules/nixos/agent.nix"
# host specific
"hosts/idols-${name}"
])
@@ -32,6 +32,10 @@ let
# until its new host key is a recipient in nix-secrets. Enabling one is
# the last step in hosts/idols-aquamarine/README.md.
];
home-modules = map mylib.relativeToRoot [
# host specific
"home/hosts/linux/idols-${name}.nix"
];
};
systemArgs = modules // args;
+2 -3
View File
@@ -24,10 +24,9 @@ let
(map mylib.relativeToRoot [
# common
"secrets/nixos.nix"
"modules/nixos/server/server.nix"
# profile: agent (hosts/README.md#host-profiles)
"modules/nixos/agent.nix"
"modules/nixos/server/qemu-guest-hardware-configuration.nix"
# headless computer-use environment (sway + desktop automation)
"modules/nixos/desktop/computer-use.nix"
# host specific
"hosts/idols-${name}"
])
+2 -3
View File
@@ -24,10 +24,9 @@ let
(map mylib.relativeToRoot [
# common
"secrets/nixos.nix"
"modules/nixos/server/server.nix"
# profile: agent (hosts/README.md#host-profiles)
"modules/nixos/agent.nix"
"modules/nixos/server/qemu-guest-hardware-configuration.nix"
# headless computer-use environment (sway + desktop automation)
"modules/nixos/desktop/computer-use.nix"
# host specific
"hosts/idols-${name}"
])
@@ -0,0 +1,24 @@
{ ... }:
let
common = {
agentsFromHome = true;
devToolsFromHome = true;
nushellSecrets = false;
nixLd = true;
rootlessPodman = true;
docker = false;
linger = true;
agentsInSystem = false;
};
in
{
aquamarine = common // {
computerUse = false;
};
ruby = common // {
computerUse = true;
};
kana = common // {
computerUse = true;
};
}
@@ -0,0 +1,41 @@
# The `agent` profile (hosts/README.md#host-profiles): every agent host gets
# the full TUI home role, and the system side only carries what needs root.
{
myvars,
lib,
outputs,
}:
let
username = myvars.username;
hosts = [
"aquamarine"
"ruby"
"kana"
];
hasPackage = name: lib.any (package: (package.pname or "") == name);
in
lib.genAttrs hosts (
name:
let
system = outputs.nixosConfigurations.${name}.config;
home = system.home-manager.users.${username};
in
{
# home role: home/linux/tui.nix
agentsFromHome = hasPackage "codex" home.home.packages;
devToolsFromHome = hasPackage "kubectl" home.home.packages;
# ~/.secrets is synced to the desktops only; sourcing it would break nushell.
nushellSecrets = home.modules.shell.secrets.enable;
# system module: modules/nixos/agent.nix
nixLd = system.programs.nix-ld.enable;
rootlessPodman = system.virtualisation.podman.enable;
docker = system.virtualisation.docker.enable;
linger = system.users.users.${username}.linger;
# User tools stay out of the system profile.
agentsInSystem = hasPackage "codex" system.environment.systemPackages;
# host switch
computerUse = system.modules.desktop.computerUse.enable;
}
)
@@ -1,7 +0,0 @@
{
lib,
outputs,
}:
lib.genAttrs [ "kana" ] (
name: outputs.nixosConfigurations.${name}.config.system.build.toplevel.drvPath
)
@@ -1,6 +0,0 @@
{
lib,
inputs,
}:
# Colmena must deploy the system that nixosConfigurations evaluates and builds.
lib.genAttrs [ "kana" ] (name: inputs.self.colmenaHive.toplevel.${name}.drvPath)
@@ -21,7 +21,7 @@
raplPermissionsExposed = true;
collectorFlagsUnique = true;
inferenceUserCanRender = true;
noHomeManager = true;
homeManagerStatePersistent = true;
pythonInstalled = true;
staticAddressMatchesWiredInterface = true;
rootIsTmpfs = true;
@@ -92,8 +92,18 @@ in
== builtins.length (lib.unique cfg.services.prometheus.exporters.node.enabledCollectors);
inferenceUserCanRender = builtins.elem "render" cfg.users.users.${myvars.username}.extraGroups;
# Headless: no desktop session, no Home Manager.
noHomeManager = !cfg ? home-manager.users;
# Home Manager's own state has to survive the tmpfs root; the agent profile
# itself is covered by ../agent-profile.
homeManagerStatePersistent =
let
dirs = cfg.preservation.preserveAt."/persistent".users.${myvars.username}.directories;
in
builtins.all (path: builtins.any (d: d.directory == "/home/${myvars.username}/${path}") dirs) [
".local/state/home-manager"
".local/state/nix/profiles"
".codex"
".config/opencode"
];
pythonInstalled = builtins.any (
package: lib.hasPrefix "python3-" (package.name or "")
) cfg.environment.systemPackages;