# AGENTS.md — dots Kieran's NixOS/nix-darwin/home-manager dotfiles and homelab infrastructure. ## Machine Inventory | Name | Type | Platform | Purpose | |---|---|---|---| | `atalanta` | nix-darwin | aarch64-darwin | Primary macOS workstation (Apple Silicon) | | `terebithia` | NixOS | aarch64-linux | Main homelab server (ARM VPS, runs most services) | | `prattle` | NixOS | x86_64-linux | Media + CI server (nixarr, garage, tangled spindle; root on bcachefs SSD) | | `beef` | nix-darwin | aarch64-darwin | Build/compute server (M3 Max, Determinate Nix) | | `iso-*` | NixOS ISO | x86_64 + aarch64 | Bootable install media | ## Apply Commands ```bash # Darwin (atalanta, beef — run locally on that machine) darwin-rebuild switch --flake .#atalanta darwin-rebuild switch --flake .#beef # NixOS (run locally on target or via deploy-rs) nixos-rebuild switch --flake .#terebithia nixos-rebuild switch --flake .#prattle # Remote deploy via deploy-rs (from dev shell, uses Tailscale) nix develop # enters shell with deploy-rs deploy .#terebithia deploy .#prattle # nh helper (preferred on NixOS hosts themselves) nh os switch nh home switch # Format all Nix files nix fmt # Build ISO nix build .#packages.x86_64-linux.iso nix build .#packages.aarch64-linux.iso # Build and serve docs nix build .#packages.aarch64-darwin.docs nix run .#packages.aarch64-darwin.docs.serve # Inspect services manifest (JSON) nix eval --json .#services-manifest ``` ## Repo Layout ``` flake.nix — single source of truth: all configurations, overlays, deploy nodes machines// — per-machine entry points (default.nix + home-manager.nix + home/) modules/ nixos/ system/ — NixOS system modules (authentication, machine, wifi) services/ — service modules, mostly using mkService factory apps/ — NixOS-level app configs (tuigreet) darwin/ defaults.nix — macOS defaults, the Lix pin, Darwin-only overlays nix-cache.nix — prattle attic substituter (server Macs only) home/ system/ — home-manager system (shell) aesthetics/ — theming (Catppuccin), wallpapers apps/ — home-manager app configs (helix, git, ghostty, etc.) wm/ — window manager configs (yabai/skhd, hyprland) shared/ — options shared across platforms (atelier.machine) lib/ mkService.nix — service factory (see below) lib/ services.nix — utilities for extracting service metadata services-manifest.nix — generates the JSON manifest consumed by the docs/infra dashboard secrets/ secrets.nix — agenix public key declarations *.age — encrypted secrets packages/ — custom derivations (bore-auth, docs, zmx, ghrpc) ghrpc/ — Go source for the repo-creation CLI (cobra + fang + huh). templates/ is embedded in the binary, so a new directory there is a new scaffold; its template.json declares the project type and one licenseSlot per thing that needs licensing (hardware/firmware/docs), which the form asks about and LICENSE.md is composed from dots/ — raw config files (hyprland, yabai/skhd, wallpapers, scripts) docs/ — mdBook source; built by nix build .#packages.*.docs ``` ## The `atelier` Namespace All custom options live under `atelier.*`. This is the house namespace — not upstream NixOS options. **Machine metadata** (`modules/shared/machine.nix`): ```nix atelier.machine = { enable = true; type = "server"; # or "client" tailscaleHost = "hostname"; }; ``` Setting `enable = true` makes the machine visible in the services manifest/dashboard. **Services** (`modules/lib/mkService.nix`): ```nix atelier.services. = { enable = true; domain = "app.dunkirk.sh"; port = 3000; secretsFile = config.age.secrets..path; repository = "https://github.com/..."; # cloned once on first start data.sqlite = "/var/lib//data/app.db"; # triggers WAL checkpoint + stop on backup data.files = [ "/var/lib//uploads" ]; }; ``` The factory auto-creates: systemd service, dedicated user/group, `/var/lib//{app,data}` dirs, Caddy virtualHost with Cloudflare DNS TLS, port-conflict assertion, sudo rules so the service user can restart itself (for CI deploys), and restic backup integration. Services that don't fit the factory pattern (herald, bore/frps, tangled knot/spindle, n8n, minio) are handled as custom service modules with manual wiring, but they still appear in the manifest via hardcoded entries in `lib/services.nix`. ## Adding a New Service 1. Create `modules/nixos/services/.nix` — either use `mkService` or write a custom module. 2. Register the secret in `secrets/secrets.nix` (add the `.age` entry with `kierank` pubkey). 3. Create the encrypted secret: `cd secrets && agenix -e .age` (agenix resolves `secrets.nix` from the working directory). 4. In the target machine's `default.nix`, add: - `age.secrets. = { file = ../../secrets/.age; owner = ""; };` - `atelier.services..enable = true;` and remaining options 5. Import the module (or rely on `import-tree` if it auto-discovers from `modules/nixos/`). 6. If Caddy is the proxy: the factory handles TLS automatically using Cloudflare DNS. Caddy needs `EnvironmentFile` pointing to the cloudflare secret (already wired in terebithia's `default.nix`). ## Secrets Management - **Tool**: agenix, run from inside `secrets/`: `cd secrets && agenix -e .age` - **Identity**: `/Users/kierank/.ssh/id_rsa` (Darwin) or `/home/kierank/.ssh/id_rsa` + `/etc/ssh/id_rsa` (NixOS) - **All secrets** encrypted to the single `kierank` RSA key in `secrets/secrets.nix` - Secret files are referenced as `config.age.secrets..path` — this is a runtime path, not a store path; only available after activation ## Module Import Patterns Two patterns in use — know which machine uses which: **`import-tree`** (terebithia, atalanta): Automatically imports all `.nix` files under a directory tree. Used as `inputs.import-tree ../../modules/nixos` or `inputs.import-tree ../../../modules/home`. Any `.nix` file dropped in those trees is automatically included — no manual import list needed. **Explicit imports** (prattle): Individual `imports = [ ... ]` lists. Adding a module requires editing the machine's `default.nix`. Home modules are not imported per machine. `homeManagerModule` in `flake.nix` puts `import-tree ./modules/home` (plus the catppuccin and spicetify home modules) into `home-manager.sharedModules`, so every user on every host has all of them available. They just won't activate without `atelier..enable = true`. A machine's `home/*.nix` only sets user details and enables features. ### Modules under `modules/` never take `inputs` Shared modules only see `pkgs`, `lib`, `config` and options, so they stay portable (see ysun's "`specialArgs` considered harmful"). Anything that needs a flake input is wired in `flake.nix` instead: - **A package from an input**: add it to `overlays/default.nix` and use `pkgs.`. - **An upstream module** (catppuccin, spicetify, tangled knot/spindle): import it in `flake.nix` (`homeManagerModule`'s `sharedModules`, or the host's `modules` list). - **Flake plumbing** (registry, `nixPath`): `registryModule` in `flake.nix`. `specialArgs.inputs` is still passed to the four real hosts, but only machine entry files (`machines//default.nix`) should use it. The ISOs no longer get it at all. ## Overlays and Custom Packages `overlays/default.nix` is the one overlay for every platform. `overlaysModule` in `flake.nix` applies it (with NUR) to every configuration, Darwin included. It: - Adds `pkgs.unstable` (nixpkgs-unstable) — access unstable packages via `pkgs.unstable.` - Adds `pkgs.zmx-binary`, `pkgs.drift-diff`, `pkgs.terminal-wakatime`, `pkgs.wakatime-ls`, `pkgs.spicePkgs`, `pkgs.impure` everywhere - Linux-only: `pkgs.bore-auth`, `pkgs.pear`, `pkgs.herald`, `pkgs.potluck`, `pkgs.lard`, `pkgs.tangle-of-trust`, and the patched nix - Darwin-only: disables the `direnv` test suite and swaps in the patched `yabai` ## Formatting `nix fmt` runs `nixfmt-tree` (RFC-style formatter). This is the formatter; don't use `nixfmt-rfc-style` manually on individual files when the tree formatter is available. The formatter is set per-system in `flake.nix`: ```nix formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-tree; formatter.aarch64-darwin = nixpkgs.legacyPackages.aarch64-darwin.nixfmt-tree; ``` ## CI/CD **Servers (terebithia, prattle)**: `.github/workflows/deploy.yaml` — triggers on push to `main`, gated on which paths changed. Builds on an arch-matched GitHub runner (`ubuntu-24.04-arm` for terebithia), substituting our own packages from prattle's attic cache, then deploy-rs copies only the closure diff over Tailscale. Captures pre-deploy generations and rolls back the failing node in its own matrix leg. Docs build/publish follows a successful deploy. **Application services**: `.github/workflows/deploy-service.yml` — SSH over Tailscale to service user, git pull, `bun install`, `sudo systemctl restart `. Service users can restart their own service (sudoers rule from mkService). **ISOs**: `.github/workflows/build-iso.yml`. `deploy.yaml` uses the `tag:deploy` Tailscale tag; the reusable service workflow uses `tag:ci`. CI needs `TS_OAUTH_CLIENT_ID`, `TS_OAUTH_SECRET`, `ATTIC_TOKEN`, `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` as repo secrets. ## Theming Global theme is **Catppuccin Macchiato** with green accent. The `atelier.theming.enable` option (from `modules/home/aesthetics/theming.nix`) wires catppuccin-nix across GTK, Qt (kvantum), qutebrowser, and cursor. The flavor and accent are hardcoded in that module — change there, not per-app. ## Nix Version Note The Darwin machines run **Lix** (`nix.package = pkgs.lixPackageSets.latest.lix`, set in `modules/darwin/defaults.nix`) — except beef, which sets `nix.enable = false` and runs Determinate Nix. The NixOS servers run upstream CppNix, patched for NixOS/nix#12871 by the overlay in `flake.nix`. ## Gotchas - **`import-tree` is greedy**: every `.nix` file in `modules/nixos/` or `modules/home/` is evaluated on machines that use it. A syntax error anywhere in those trees breaks the whole configuration for those machines. - **Port conflicts are asserted**: `mkService` checks for port collisions across all enabled services and will fail `nixos-rebuild` with a clear message. Check existing ports before picking a new one. - **`ExecStartPre` with `!` prefix**: `!` only lifts `User=`/`Group=`/`SupplementaryGroups=`; the mount-namespace sandbox (`ProtectSystem`, `ReadWritePaths`) still applies. `+` is the prefix that bypasses namespacing — don't reach for it. The script works because `ReadWritePaths` covers `dataDir` and tmpfiles created it at activation. - **agenix secrets are runtime paths**: `config.age.secrets..path` resolves to something like `/run/agenix/`. It's not a store path. Don't try to use it at build time. - **Caddy TLS**: all virtualHosts use Cloudflare DNS challenge (`dns cloudflare {env.CLOUDFLARE_API_TOKEN}`). The token comes from `age.secrets.cloudflare`, loaded via `systemd.services.caddy.serviceConfig.EnvironmentFile`. This is already wired in terebithia — new services just need their virtualHost configured. - **CI builds, the target does not**: `--remote-build` was dropped once runners were arch-matched, so the runner needs real build capacity. The dev shell (via `nix develop`) provides `deploy-rs`. - **`services.nix` nixdoc format**: functions in `lib/services.nix` use nixdoc-compatible `/**` docstring syntax. Keep that format when adding functions there — it feeds the auto-generated docs. - **zmx hosts and one-shot SSH**: hosts with `atelier.ssh.hosts..zmx = true` (and the `t.*`/`p.*`/`e.*` patterns) carry a `RemoteCommand` that auto-attaches a zmx session, so `ssh `, `scp` and `rsync` would normally fail with `Cannot execute command-line and remote command`. A `Match` block cancels it whenever `ZMX_OFF`, `AI_AGENT`, `CLAUDECODE` or `CRUSH` is set, so agent shells already get plain SSH. Prefix with `ZMX_OFF=1` if you ever see that error. For long remote work use `ssh zmx run ` and read it back with `ssh zmx history `.