From f3d921bcdc1bb3239013c89897f318ae93147905 Mon Sep 17 00:00:00 2001 From: Jack O'Sullivan Date: Sat, 1 Aug 2026 23:54:14 +0100 Subject: [PATCH] AGENTS.md: Add documentation guidance Keep agent context lean and define documentation conventions for terminology, page structure, ownership, generated content, and commit messages. --- AGENTS.md | 163 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 48 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a5478e3..b061ca5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,18 +6,24 @@ This file provides guidance to coding agents when working with code in this repo tools refuse to write through a symlink and will error on `CLAUDE.md`). **Prefer this file over agent memory.** When you learn something durable about this repo — a -convention, a workflow gotcha, a design rationale — record it here (or in a repo doc this file points -to, e.g. `home-switches.md`), not in agent memory. AGENTS.md is versioned and shared; memory is not. +convention, a workflow gotcha, a design rationale — record it here (or in a repo doc this file +points to, e.g. `docs/sites/home/switches.md`), not in agent memory. AGENTS.md is versioned and +shared; memory is not. + +Keep this file lean: it should contain only rules, workflows, safety constraints and sharp gotchas +an agent needs before starting work. Detailed inventories, topology, service operation and design +rationale belong in the canonical document under `docs/`; summarize only the essential constraint +here and link to that document instead of maintaining a second copy. Claude Code permissions live in two files: `.claude/settings.json` (versioned, shared — the committed allow list of safe-to-auto-approve commands) and `.claude/settings.local.json` (personal, gitignored — where interactive "always allow" grants accumulate). Put durable, generally-safe -commands in the shared file; leave one-off or machine-specific grants in the local one. +commands in the shared file; leave one-off or workstation-specific grants in the local one. ## Overview Personal Nix flake managing NixOS systems and home-manager configurations for a set of -machines — always called **"boxes"**, never "fleet". It is built around a **custom module +**boxes**, never a "fleet". It is built around a **custom module system** layered on top of NixOS/home-manager, not the stock flake `nixosConfigurations` pattern. ## Commands @@ -54,9 +60,14 @@ Common ones: - `repl` — `nix repl .#`. - `update-nixpkgs` / `update-home-manager` — bump pinned inputs. -Check everything (what CI runs): `nix flake check --no-build`. +Use the narrowest relevant evaluation while iterating: `check-system ` for a box config, +`nix eval .#nixfiles.config.nixos.allAssignments --json` for assignment generation, or +`nix build --no-link .#nixfiles.config.nixos.optionsDoc` for the option reference. Reserve +`nix flake check --no-build` for final broad validation or reproducing CI. CI builds each attr of `.#ci.x86_64-linux` (systems, homes, packages, shell) and pushes to the -Harmonia binary cache; see `.gitea/workflows/ci.yaml` and `ci/push-to-cache.sh`. +Harmonia binary cache; see `.gitea/workflows/ci.yaml` and `ci/push-to-cache.sh`. A separate +workflow (`.gitea/workflows/update-docs.yaml`) regenerates the network-assignment tables and NixOS +option reference via `nix run .#update-docs-{assignments,options}`. For DNS lookups use **`drill`** (ldns) — `dig` isn't installed in this environment (it fails with exit 127, which is easy to miss if stderr is redirected). E.g. `drill -Q @ A`. @@ -64,8 +75,17 @@ exit 127, which is easy to miss if stderr is redirected). E.g. `drill -Q @ up`. +For ad-hoc packages prefer **`pkgs#`** over `nixpkgs#` (e.g. `nix run pkgs#python3`): +`pkgs` is a system registry alias for the same pinned nixpkgs the boxes are built from, so it +resolves from the local store instead of fetching a different channel. + ## Architecture +The mechanics in this section have expanded human-readable write-ups under `docs/`: +`docs/architecture.md` (module system), `docs/networking.md` (assignments, topology, meshes) and +`docs/deployment.md` (deploy-rs, devshell, secrets, CI). This section stays the terse agent +version; consult those for depth. + ### The custom module system `flake.nix` does **not** call `nixosSystem` per host directly. Instead it `evalModules` over `./nixos`, `./home-manager`, `./deploy-rs.nix`, and the per-host files listed in the `configs` @@ -115,50 +135,33 @@ Per-host configs live under `nixos/boxes/` (some are single `.nix` files, with nested VMs/containers under e.g. `colony/vms`). Many "systems" are VMs or containers managed via the `vms` / `containers` modules and the `l2mesh` VXLAN module. -### Home routers (`nixos/boxes/home/routing-common`) -The two home routers, `river` and `stream`, share `routing-common`, which is a **function of an -`index`** (`import ../../routing-common 0` for river, `1` for stream). The index derives per-box -addresses, keepalived VRRP priorities/state, DNS `ns` numbering, etc., so the two boxes are an -active/backup HA pair from one definition. They differ where hardware/uplink differ: `stream` has a -DHCP WAN, `river` runs PPPoE (`services.pppd`, Digiweb) — box-specific bits live in the respective -box file, not `routing-common`. +For a human-readable map of what is actually deployed (per-box roles, services and networking), +see `README.md` and `docs/` (index at `docs/README.md`; box pages under `docs/sites/`, +`docs/remote/`, `docs/mobile/`). Keep these in sync when adding, removing or repurposing a box or +service. The network-assignment tables are consolidated in `docs/networking.md` (one per site, +CI-generated from `allAssignments` between `` markers); box pages +link to that section. Write the prose and let the updater refresh the tables. -- **HA is VRRP (`keepalived`).** Per-VLAN floating **VIPs** (`lib.my.c.home.vips`) are what clients - use as both gateway *and* DNS server. `kea` (DHCP) and `radvd` (RAs; started only on the master) - hand out the VIP, and `pdns-recursor` binds the VIPs (with `net.ipv*.ip_nonlocal_bind` so the - backup can pre-bind). Point client-facing services at the VIP, not a box's real address, so - failover follows the master instead of relying on client resolver timeouts. -- **`wan-online.target`** is a shared abstract target meaning "the public WAN/IPv4 route is up". - `routing-common` only declares it; each box wires *how it is reached* (`stream`: a oneshot that - waits for the DHCP default route; `river`: the pppd `ip-up`/`ip-down` hooks). Services that need - the WAN attach **to** it via `wantedBy` + `partOf` + `after` (not `requires`/`wants`), so an empty - target is never pulled in and prematurely activated, and they re-load on WAN flap. -- networkd helpers used heavily here: `lib.my.networkdAssignment` and `lib.my.mkVLAN` live under - **`lib.my`**, while networkd snippet constants like `networkd.noL3` live under **`lib.my.c`** — - easy to mix up. Set an interface MTU via the `.network`'s `linkConfig.MTUBytes` (`[Link]`), not - `netdevConfig` (`[NetDev]` rejects `MTUBytes`). +### Home routers (`nixos/boxes/home/routing-common`) +`river` and `stream` share `routing-common`, a function of an `index`; keep common HA behavior there +and box-specific WAN behavior in the respective box file. Client-facing gateway and DNS services +must use the floating VIPs. `routing-common` only declares the inert `wan-online.target`; each box +wires its own reachability mechanism. See `docs/networking.md#router-ha` and the router box pages +for the design and operational detail. Keep pair-wide behavior in `docs/networking.md`; router +pages should document only their WAN, platform and other box-specific responsibilities. + +The easy-to-mix-up implementation details are that `lib.my.networkdAssignment` and +`lib.my.mkVLAN` live under `lib.my`, networkd snippet constants such as `networkd.noL3` live under +`lib.my.c`, and interface MTU belongs in the `.network`'s `linkConfig.MTUBytes`. ### Home switches (`jim` / `dave` / `brian`) -The home boxes and the Digiweb WAN hang off hand-configured switches that are **not** managed by -this flake: `jim` and `dave` (MikroTik, RouterOS) and `brian` (Ubiquiti, UniFi). The full topology, -VLAN map, and the ONT/WAN path live in **`home-switches.md`** at the repo root — read it before -touching anything WAN/VLAN-related, and update it when the switch layout changes. -- **Access:** the switches resolve by **short hostname** on the home network (the routers serve - their records in the home zone — `routing-common/dns.nix`: `jim`/`dave`/`brian`). From a home box, - SSH to the MikroTiks as `admin`/`admin` (e.g. `ssh admin@jim`); `brian` is configured via the - UniFi controller, not a CLI. -- **Changing switch config is out-of-band and hard to revert — always confirm before applying:** - print the affected menu, make the change, then re-verify. The nix config and the switches must - agree on VLAN numbering (e.g. `lib.my.c.home.vlans`), so a switch-side change usually pairs with a - box change; `home-switches.md` documents the switch layout and per-switch config for the WAN design. +These switches are not managed by the flake, and changing them is out-of-band and hard to revert. +Read `docs/sites/home/switches.md` before WAN or VLAN work, always confirm before applying a switch +change, and keep the documented layout and `lib.my.c.home.vlans` in sync. ### Home wireless APs (`vibe` / `wave`) -The home Wi-Fi APs are also **not** managed by this flake: `vibe` (MikroTik cAP ax, RouterOS) and -`wave` (Cudy AX3000 running OpenWrt/UCI). They are dumb APs — bridge clients onto the right VLAN, -routers do DHCP/RA/firewall. The trunk/VLAN design, the OpenWrt flash + config for `wave`, and the -per-AP management addressing live in **`home-aps.md`** at the repo root — read it before touching AP -config, and update it when an AP changes. Only the DNS records live in the flake -(`routing-common/dns.nix`). +These APs are not managed by the flake; only their DNS records are. Read and update +`docs/sites/home/aps.md` when changing an AP or its VLAN layout. ## Secrets @@ -171,7 +174,8 @@ private keys) is required for editing secrets, deploying, and running dev VMs. ## Conventions -- Format with `nixpkgs-fmt` (`fmt`). 2-space indent, `inherit (...)` blocks at the top of `let`. +- Format with `nixpkgs-fmt` (`fmt`). 2-space indent, `inherit (...)` blocks at the top of `let` — + prefer `inherit (lib) mkOption ...;` (and bare use) over qualifying inline as `lib.mkOption`. **Ask before running `fmt`** — some files aren't canonically formatted, so `fmt` can reindent a whole file and bury a logical change in whitespace churn. Match the surrounding style by hand and leave formatting to the user unless they ask. @@ -186,6 +190,69 @@ private keys) is required for editing secrets, deploying, and running dev VMs. as `overlays.default`. - In prose and commit messages, quote code-like identifiers (commands, options, paths, package and attribute names) in backticks. -- Call the machines **"boxes"**, never "fleet". +- Keep docs and prose plain — avoid vague AI-tell filler. In particular use "layout"/"structure" + rather than "shape", and "General" rather than "cross-cutting topics"; name things directly + instead of reaching for umbrella words. +- For network connectivity, use the concrete term — "interface", "connection", "network + attachment" or the network's name — rather than calling it a "leg". +- Call the systems **"boxes"**, never "machines" or "fleet", except where "machine" is part of a + command, option or upstream technical term such as QEMU's machine type. - Commit subjects follow `area/scope: Capitalized summary` (e.g. `nixos/home: ...`); keep logically - distinct changes in separate commits. + distinct changes in separate commits. Aim for 50-character subjects and do not exceed 72 + characters. Wrap commit bodies at 72 columns. A concise body describing the change and its + rationale is welcome when the subject alone does not provide enough context. + +## Documentation + +Human-readable docs live under `docs/` (index: `docs/README.md`). Box pages are under +`docs/sites//`, `docs/remote/`, `docs/mobile/`. A box that itself hosts sub-systems +(containers/nested VMs) gets a **directory named for it** with a `README.md` and the child pages +beneath it (e.g. `shill/README.md` + `shill/containers/*.md`, `sfh/README.md`). When a site and its +physical box share a name, the site keeps the `README.md` and the box page stays alongside it (for +example `sites/colony/README.md` and `sites/colony/colony.md`). + +**Box page layout** (match the existing pages): H1 + a one-line intro; a short bullet list of +`Source` / `Host` / `nixpkgs`; `## Role`; `## Network assignments` that **links** to +[`networking.md#box-assignments`](docs/networking.md#box-assignments) (never inline the table); one +`##` section per topic; `## Notable config files` last. A box without static assignments still gets +the section with a short explanation instead of a generated-table link. + +**Structure and layout:** +- Use **tables** for lists of structured items (BGP peers, forwarded ports, vhosts, containers, + VMs, VLANs). Make the item **name the link**; don't add a separate `Page`/`Docs` column. +- Break up **long prose**: any bullet running past ~4 lines over several distinct facts becomes a + `###` subheading (lead sentence + nested bullets). Don't leave walls of long bullets. +- Prefer durable concepts and named configuration identifiers over copying exact numbers that are + tunable, generated or likely to drift. Exact values are appropriate when they are useful parts of + the inventory or interface: VM resource allocations, hardware-fixed properties, network and + protocol identifiers, exposed service ports, and safety or recovery values. Describe mutable + implementation tunables by purpose and link to their source instead of duplicating the current + value. +- **Per-site index pages** (`docs/sites/*/README.md`, `docs/remote/README.md`, and + `docs/mobile/README.md`) use one table listing **boxes only**; don't repeat that inventory in a + diagram. A box's containers / nested VMs are tabulated on that box's own page. The global + `docs/README.md` is the exception: it may show them in high-level site diagrams, but should not + add a second detailed inventory outside those diagrams. + +**De-dup by ownership** — each fact has one home: +- Shared cross-box **fabric** (the AS211024 mesh, Tailscale/headscale topology, the BGP overview, + the WireGuard-tunnel summary) is documented once in `docs/networking.md`; box pages give a + one-line summary and link to it. +- Site-wide network definitions (prefixes, VLANs, router VIPs and router HA) also live in + `docs/networking.md`; site pages summarize and link to the canonical section. +- **Box-specific** detail (a box's own BGP peers, the WireGuard tunnels it terminates, its + services) lives on the box page; `networking.md` carries only a per-box summary that links out. +- Addresses represented by generated `assignments` or `extraAssignments` live only in the + assignment tables; handwritten box and workload inventories link there instead of copying them. + Addresses outside that data model (such as external peers or service endpoints) stay with the + topic that owns them. + +**Generated content:** the network-assignment tables in `networking.md` and the option reference +(`docs/reference/nixos-options.md`) are CI-generated (`nix run .#update-docs-{assignments,options}`) +— don't hand-edit between the `` markers; write the prose and let the updater refresh +the tables. + +**Keep docs current:** when you add, remove or repurpose a box or service, update its box page, the +relevant site-index `README.md`, and any affected prose in `networking.md` (the assignment/option +tables refresh via CI). This is the same "prefer docs over agent memory" rule from the top of this +file.