Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 4 additions & 26 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,7 @@
# From running nixos-rebuild build-vm
*.qcow2

# Config files that are not suitable to add to version control:
link/nix/config/.mono/
link/nix/config/asciinema/
link/nix/config/configstore/nodemon.json
link/nix/config/configstore/snyk.json
link/nix/config/configstore/update-notifier-nodemon.json
link/nix/config/configstore/update-notifier-npm.json
link/nix/config/configstore/update-notifier-yo.json
link/nix/config/filezilla
link/nix/config/fluidkeys/
link/nix/config/fontforge/
link/nix/config/gcloud/
link/nix/config/grv/
link/nix/config/gtk-2.0/
link/nix/config/htop/
link/nix/config/hub
link/nix/config/iterm2/
link/nix/config/Microsoft\\VisualStudio Services/
link/nix/config/NuGet/nuget.config
link/nix/config/powershell/powershellget/
link/nix/config/puppet/
link/nix/config/QtProject/
link/nix/config/rclone/
link/nix/config/tmuxinator/
link/nix/config/wireshark/
link/nix/config/linode-cli
.claude/*
!.claude/skills/
!.claude/skills/**

318 changes: 318 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
# AGENTS.md β€” dots

This is the authoritative agent guide for this repository. It is self-contained
β€” do not assume any parent or global AGENTS.md is available. For a human-readable
overview of the repo's purpose and layout, see `README.md`. This file covers what
agents need to know that the README does not.

---

## Who You're Working With

The owner is an experienced infrastructure engineer (SRE) who manages Linux
fleets, runs a NixOS homelab, and is comfortable in a terminal. He is **not an
application developer**. When working on application code:

- Comment generously β€” future maintenance may be done by an agent without full
context, or by the owner returning to code he didn't write
- Prefer explicit over implicit β€” avoid patterns that require deep framework
knowledge to maintain
- Prefer simple over clever β€” the best solution is the one that's easiest to
understand six months later
- If something is non-obvious, explain it in a comment at the point of use

---

## What This Repo Is

A multi-system Nix flake that fully manages NixOS hosts, macOS hosts (via
nix-darwin), and home-manager-only Linux machines. Sensitive configuration lives
in a companion **private-flake** at `~/repos/private-flake` β€” never commit
secrets or private config here.

---

## Infrastructure Preferences

When making architectural or configuration decisions:

- **Self-hosted over cloud** β€” prefer infrastructure the owner controls over
third-party SaaS or cloud services
- **Open-source over proprietary** β€” all else being equal, prefer open-source
- **Self-sovereign data** β€” avoid patterns where the owner's data is in third-party
custody unless there is no self-hosted alternative
- **Simple over clever** β€” the least complex solution that meets the requirements

These are preferences, not absolute rules. External services are fine for
read-only lookups or when self-hosting is genuinely impractical.

---

## Repo Layout (agent-relevant summary)

```
flake.nix β€” inputs, outputs, host wiring
lib/ β€” mkNixosHost / mkDarwinHost / mkHomeConfig helpers
modules/
hosts/ - host-specific modules
nixos/<hostname>/ β€” per-host NixOS config + hardware + secrets.yaml
darwin/<hostname>/ β€” per-host nix-darwin config
home-manager-only/ β€” config for hosts running Home Manager only
shared/ - reusable modules
nixos/ β€” NixOS modules imported by multiple hosts
home/general/ β€” Home Manager defaults for all systems and config for all GUI systems
home/linux/ β€” Home Manager config for Linux-specific apps (NixOS and Home Manager only such as Ubuntu)
files/ β€” raw config files managed via xdg.configFile etc.
.claude/skills/ β€” project-specific Claude Code skills (committed)
```

---

## Host Inventory

| Hostname | Type | Arch | Notes |
|---|---|---|---|
| bigboy | NixOS | x86_64 | ThinkPad P52, daily driver |
| nixnuc | NixOS | x86_64 | Home server, runs most services |
| hetznix01 | NixOS | x86_64 | Hetzner VPS, runs email + matrix, primary VPS |
| hetznix02 | NixOS | aarch64 | Hetzner VPS, build host for Raspberry Pi's |
| kiosk-entryway | NixOS | x86_64 | Lenovo Q190, headless kiosk, WiFi only (`wlp3s0`) |
| kiosk-gene-desk | NixOS | aarch64 | Raspberry Pi 4, headless kiosk, WiFi only (`wlan0`) |
| AirPuppet / Blue-Rock | macOS | x86_64 | nix-darwin managed |
| mightymac | macOS | aarch64 | nix-darwin managed |
| rainbow-planet | home-manager only | x86_64 | Ubuntu, old NixOS config currently commented out in flake |

---

## Build and Deploy Commands

The `nixup` and `nixdiff` aliases abstract away the differences between NixOS,
nix-darwin, and home-manager-only hosts β€” use them everywhere rather than
invoking the underlying tools directly. Their definitions live in:
- NixOS / home-manager-only Linux: `modules/shared/home/linux/default.nix`
- macOS (nix-darwin): `modules/hosts/darwin/home.nix`

### Check what will change before deploying

```bash
nixdiff # builds the new config and diffs it against the currently running system
```

### Apply locally

```bash
nixup # NixOS: sudo nixos-rebuild switch --flake ~/repos/dots
# nix-darwin: darwin-rebuild switch --flake ~/repos/dots
# home-manager only: home-manager switch --flake ~/repos/dots#gene-x86_64-linux
```

There is also `nixboot` on NixOS hosts to stage a change for the next reboot
without switching immediately.

### Build here, deploy to a remote NixOS host

`nixos-rebuild` is available natively on NixOS hosts but not on home-manager-only
or nix-darwin hosts. When deploying from one of those, bring it in via `nix shell`:

```bash
nix shell nixpkgs#nixos-rebuild -c nixos-rebuild switch \
--flake .#<hostname> \
--target-host ssh://<ip-or-hostname> \
--sudo
```

To only build the closure without activating (useful for checking before committing):

```bash
nix build .#nixosConfigurations.<hostname>.config.system.build.toplevel
```

### Build SD card image (kiosk-gene-desk)

```bash
nix build .#packages.aarch64-linux.kiosk-gene-desk-sdImage
```

---

## Secrets Management

All secrets use **sops-nix**. Encrypted `secrets.yaml` files live per-host under
`modules/hosts/nixos/<hostname>/` and shared secrets at `modules/shared/secrets.yaml`.

- Age key derivation: `ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt`
- Key registration: `.sops.yaml` at repo root controls which age keys decrypt which files
- Edit a secret: `sops modules/hosts/nixos/<hostname>/secrets.yaml`

When a secret must be readable by a non-root service user, set `owner` on the
`sops.secrets` entry (e.g. `owner = "wpa_supplicant"` for wifi credentials).
The default is `root:root 0400`.

---

## Private Flake

Sensitive host modules (wifi networks, email accounts, Tailscale keys, etc.) live
in `~/repos/private-flake`. It is a flake input and its modules are passed via
`additionalModules` in `flake.nix`. When diagnosing a host issue, always check
both repos β€” the private flake often holds the relevant config.

---

## NixOS Module Pattern

When a service has its own repo, the NixOS module lives there β€” not here:

- The service repo's `flake.nix` exports `nixosModules.default`
- This repo adds the service repo as a flake input and imports the module in the
relevant host's `additionalModules`
- This keeps deployment config co-located with the code it deploys

When writing NixOS modules:
- Expose typed options with `lib.mkOption` β€” domain, ports, data directories,
secrets file path, enable flag
- Use `DynamicUser = true` for systemd services where possible
- Pass secrets via `EnvironmentFile` pointing to a sops-managed path β€” never
hardcode secrets
- Add `systemd.tmpfiles.rules` entries to create required directories

### Port Registry

Service ports are managed through a two-level registry rather than scattered
magic numbers:

- `modules/shared/nixos/ports.nix` β€” defines the `dots.ports` option type and
declares fleet-wide ports (ssh, http, https, shared service ports). All entries
default to `openFirewall = false`.
- `modules/hosts/nixos/<hostname>/ports.nix` β€” host-specific ports plus any
fleet-wide overrides (e.g. setting `openFirewall = true` for a port only that
host exposes).

The `openFirewall` flag is not just documentation β€” hosts that use the registry
wire it directly to `networking.firewall` via `lib.pipe`:

```nix
networking.firewall = {
allowedTCPPorts = lib.pipe config.dots.ports [
builtins.attrValues
(builtins.filter (e: e.openFirewall && e.protocol == "tcp"))
(map (e: e.port))
];
allowedUDPPorts = lib.pipe config.dots.ports [
builtins.attrValues
(builtins.filter (e: e.openFirewall && e.protocol == "udp"))
(map (e: e.port))
];
};
```

Reference ports in config as `config.dots.ports.<name>.port` rather than
hardcoding numbers β€” this applies everywhere: service configs, nginx proxy
targets, container definitions, and the firewall. When the surrounding attrset
attribute name is already `port`, use `inherit` β€” statix rules W03/W04
(`manual_inherit` / `manual_inherit_from`) enforce this and will fail the build
if you use the verbose form where `inherit` would work:

```nix
# rejected by statix W04
port = config.dots.ports.grafana.port;

# correct
inherit (config.dots.ports.grafana) port;
```

When adding a service that other hosts need to know about (e.g. a shared API
endpoint referenced across hosts), declare it in the shared registry. When
adding a service local to one host, declare it in that host's `ports.nix`.

---

## Code Quality Gates

Before pushing any `.nix` change:

```bash
nix fmt . # nixfmt-tree β€” formats all Nix files
nix run .#deadnix # finds dead code
nix run .#statix # lints for common issues
```

Pre-commit hooks enforce this automatically after `pre-commit install`. CI
(`.github/workflows/validate.yml`) mirrors the same checks.

**Also run `nix build .#nixosConfigurations.<hostname>.config.system.build.toplevel`**
for the affected host before pushing whenever `flake.nix`, `flake.lock`, or a
host module changes. The Nix sandbox is authoritative; remote CI failures are
slow to diagnose.

---

## Commit and PR Hygiene

- Write commit messages as descriptions of what the code **is**, not what changed.
For non-trivial commits, include a body: one short paragraph per major component
describing what it does, key decisions, and any non-obvious constraints. A
subject line alone is not sufficient for features. Reserve before/after framing
for bug fixes only.
- Incremental local commits are fine as a working tool, but squash before pushing.
What reaches the remote should reflect the final, complete state of the work.
Multiple commits in a pushed PR are only justified for genuinely independent
logical concerns.
- If a follow-up fix is caught after committing (even after pushing), amend
immediately and silently β€” use `--force-with-lease` if already pushed.
- After every push that changes what a PR does, update the PR description:
`gh pr edit <N> --body "..."`. Base it entirely on `git log main..HEAD`.
- When writing `gh pr create` or `gh pr edit` bodies containing backticks, use
`PREOF` (not `EOF`) as the heredoc delimiter β€” backticks inside
`$(cat <<'EOF' ... EOF)` are interpreted as command substitution by the outer
shell; `PREOF` prevents this.
- Do not bundle unrelated changes in a single commit.

---

## Git Branch Workflow

- Start new work from a fresh main:
`git checkout main && git pull`, then `git checkout -b <branch-name>`
- Delete merged branches locally after the PR merges: `git branch -d <old-branch>`
- Only rebase when there is actual divergence from main. Check
`git log --oneline origin/main..HEAD` before opening a PR β€” if main has not
moved ahead of your branch base, rebasing rewrites SHAs for no reason.

---

## Skills

Project-specific Claude Code skills live in `.claude/skills/` and are committed
to this repo (`.gitignore` tracks `skills/` but ignores the rest of `.claude/`).
Add new skills there when a workflow is worth automating.

---

## What NOT to Change Without Asking

- `flake.lock` β€” only update intentionally with `nix flake update [input]`
- `.sops.yaml` β€” changing key rules requires re-encrypting secrets; confirm before touching
- `system.stateVersion` β€” must stay at the NixOS version the host was first installed on
- This `AGENTS.md` file itself β€” propose changes rather than silently editing

---

## NixOS 26.05 β€” Known Breaking Changes

All hosts are on 26.05 except kiosk-gene-desk (Pi β€” deployment method TBD).
These notes apply when completing that upgrade or when rebuilding from scratch.

**wpa_supplicant hardening** (affects all WiFi hosts):
- Set `networking.wireless.interfaces = ["<iface>"]` explicitly β€” auto-detection
reads `/sys/class/net` which is not mounted in the new hardened sandbox
- Set `owner = "wpa_supplicant"` on the sops wifi_creds secret β€” the daemon now
runs unprivileged and cannot read root-owned files
- Service name changes from `wpa_supplicant.service` to
`wpa_supplicant-<iface>.service` β€” update `restartUnits` and any `wants`
- Interface names: kiosk-entryway uses `wlp3s0`, kiosk-gene-desk uses `wlan0`

**simple-nixos-mailserver API changes**:
- `mailserver.certificateScheme` removed β†’ use `mailserver.x509.useACMEHost`
- `mailserver.loginAccounts` renamed β†’ `mailserver.accounts`
- `config.services.dovecot2.user` no longer exists β†’ hardcode `"dovecot2"`
Loading