Compare commits

...

36 Commits

Author SHA1 Message Date
5a8147401b refactor(host): simplify local names 2026-08-28 18:38:56 -04:00
276e7a02a6 refactor(host): derive host paths from root 2026-08-28 18:25:03 -04:00
ab969fe5de refactor(flake): use singular assembly file names 2026-08-28 16:33:30 -04:00
81732aef84 refactor(flake): assemble repository modules 2026-08-28 16:16:40 -04:00
0f9b639c0d refactor(lib): move nix file discovery helper 2026-08-28 11:23:12 -04:00
d43452a8d3 docs(nix): use real flake namespace example 2026-08-28 11:12:46 -04:00
64d22473c3 refactor(flake): pass assembly namespace to modules 2026-08-28 11:09:13 -04:00
3f24669f78 docs(nix): define assembly file ownership 2026-08-28 11:01:39 -04:00
d0b00ada96 refactor(flake): split assembly by concept 2026-08-28 11:01:15 -04:00
fc834168aa docs(nix): define default file placement 2026-08-28 10:54:25 -04:00
119eadbfb8 refactor(shell): use default modules for grouped tools 2026-08-28 10:54:00 -04:00
ebd61ee60d docs(nix): tighten writing style 2026-08-28 10:41:10 -04:00
12f3abc11e docs(nix): remove premature flake outputs rule 2026-08-28 10:38:46 -04:00
eef8e37fbb docs(nix): use NixOS module terminology 2026-08-28 10:37:59 -04:00
1654aef9ae docs(nix): clarify file placement terms 2026-08-28 10:33:37 -04:00
f16d041e63 docs(nix): match flake import examples 2026-08-28 10:22:55 -04:00
b61b8e3abf docs(nix): use actual flake binding name 2026-08-28 10:22:03 -04:00
a55592f59e docs(nix): remove premature let scope rule 2026-08-28 10:20:25 -04:00
09aee329cf docs(nix): remove unobserved let rule 2026-08-28 10:18:34 -04:00
71570edeaa docs(nix): narrow let binding rules 2026-08-28 10:15:56 -04:00
276e4a3172 docs(nix): split let binding rules 2026-08-28 10:14:50 -04:00
8493f12829 docs(nix): clarify let binding rules 2026-08-28 10:12:56 -04:00
720ebdba63 docs(nix): remove premature forwarding rule 2026-08-28 10:11:06 -04:00
9f01253d92 docs(nix): use host forwarding example 2026-08-28 10:09:24 -04:00
0c4e4df772 docs(nix): clarify module file interfaces 2026-08-28 10:08:10 -04:00
597dbd4369 docs(nix): improve assembly function example 2026-08-28 10:05:49 -04:00
1e9f1bf907 docs(nix): refine function terminology 2026-08-28 10:03:35 -04:00
f3494be6cc docs(nix): apply writing conventions 2026-08-28 09:52:05 -04:00
8cc171974a docs(agents): require writing guides 2026-08-28 09:45:12 -04:00
3c3d9d80f2 docs(nix): clarify convention examples 2026-08-28 09:43:52 -04:00
4c40aa12a7 docs(nix): record local conventions 2026-08-28 09:33:50 -04:00
a8aa5b6fb5 refactor(flake): qualify nixpkgs lib calls 2026-08-28 09:32:15 -04:00
41b9d41776 Revert "refactor(flake): use nixpkgs lib directly"
This reverts commit 4a422599c7.
2026-08-28 09:18:11 -04:00
4a422599c7 refactor(flake): use nixpkgs lib directly 2026-08-28 09:10:11 -04:00
5185abd8a2 docs(agents): preserve pull request review history 2026-08-28 09:10:11 -04:00
828cc15129 refactor(flake): move assembly code out of lib 2026-08-28 09:00:28 -04:00
22 changed files with 470 additions and 142 deletions

View File

@@ -4,13 +4,16 @@ One flake builds every machine I own.
## Repository Conventions
- Follow `docs/technical-writing.md` for comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
- Use `docs/technical-writing.md` and `docs/dictionary.md` while authoring any file.
Apply them to comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
- Comments posted to the forge under the operator's account must end with `— Pi`.
The sign-off identifies the agent as the author instead of the operator.
- Commit messages follow Conventional Commits, defined in `docs/conventional-commits.md`.
Use a scope for the changed module or host, such as `fish`, `nvim`, or `neogaia`.
- Keep commit messages about the Git history, not the forge workflow.
Do not reference issue or pull-request numbers unless the commit itself requires a stable external reference.
- During pull-request development, add follow-up commits instead of amending existing commits.
Preserve review history until the pull request is ready to merge.
- Use `gitea-axi` for Gitea issues, pull requests, labels, reviews, comments, and milestones.
It resolves this repository from the `origin` remote inside the checkout.
@@ -66,7 +69,7 @@ One flake builds every machine I own.
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
- `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads only top-level host `config.registry` declarations.
- The registry collector reads only top-level host `config.registry` declarations.
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry`.
The collected registry does not include those guest-interior declarations.
Publish guest values from host-side code, or extend the collector to recurse into guests.

View File

@@ -45,11 +45,15 @@ Use the simplest accurate word when a term is not listed.
| operator | Agent tooling | The human driving the development process. Use when distinguishing the human from the agent acting on their behalf. |
| guest | dotfiles | A systemd-nspawn container managed by this flake. Use for configured containers under `guests/`. |
| host | dotfiles | A machine with a NixOS configuration built by this flake. |
| module | dotfiles | In this repository, a self-contained configuration under `modules/`, usually scoped to one program, service, or subsystem. Use `Nix module` for the language-level module-system concept. |
| module | dotfiles | In this repository, a self-contained configuration under `modules/`. Use for one program, service, or subsystem. |
| registry | dotfiles | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. |
| Conventional Commits | Git | A commit-message format defined in `docs/conventional-commits.md`. |
| forge | Git | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. |
| flake | Nix | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. |
| Nix module | Nix | A Nix language unit that declares or sets options through the Nix module system. |
| assembly file | Nix | A Nix file under `flake/` that constructs flake outputs or arguments passed to NixOS modules. |
| assembly function | Nix | A project function exposed under `flake` because it creates modules, systems, or assembly values. |
| library file | Nix | A Nix file that defines reusable project functions instead of a NixOS module or flake output. |
| library function | Nix | A pure project function exposed under `flake.util`. |
| NixOS module | Nix | A Nix file evaluated by `nixosSystem` for a NixOS configuration. |
| overlay | Nix | A function that modifies a Nix package set by adding or replacing packages. |
| workspace | Window management | A named or numbered group of windows in a graphical session. Some desktop environments call this a virtual desktop. |

270
docs/nix.md Normal file
View File

@@ -0,0 +1,270 @@
# Nix Conventions
This document records local Nix style and design conventions.
Use these conventions when changing this flake.
Read this document with `docs/dictionary.md`.
## Names
### Reserve `lib` for `nixpkgs.lib`
Use `lib` only for the base `nixpkgs.lib` value.
Do not use `lib` for project functions.
Use `flake.util` for library functions passed through module arguments.
Use `flake.mk<Name>` for assembly functions passed through module arguments.
Avoid:
```nix
specialArgs = {
lib = util;
};
```
Use:
```nix
specialArgs = {
inherit flake;
};
```
Then call project functions through `flake` inside modules:
```nix
{
flake,
lib,
...
}:
{
networking.networkmanager.unmanaged = [ "interface-name:${flake.util.bridgeName 10}" ];
}
```
### Name project function namespaces by role
Put pure project functions under `flake.util`.
Put functions that create modules, systems, or assembly values under `flake`.
This keeps data-only functions separate from functions that carry flake context.
Avoid:
```nix
flake.util.mkGuest { name = "sample"; }
```
Use:
```nix
flake.mkGuest { name = "sample"; }
```
## File Placement
### Put files in the directory that owns their role
Choose the file location before writing a Nix file.
Put assembly files under `flake/`.
Put library files under `lib/`.
Put modules under `modules/`.
Put host configurations under `hosts/<name>/`.
Put host-side guest wiring under `guests/`.
Put registry definitions under `registry/`.
Keep `flake.nix` focused on inputs and output wiring.
Do not add root-level Nix files other than `flake.nix`.
A module is self-contained configuration under `modules/`.
A module configures one program, service, or subsystem.
Files under `hosts/`, `guests/`, and `modules/` return NixOS modules.
### Use `default.nix` for module directories with support files
Use `default.nix` as the primary module when a module directory owns support files.
Keep sibling files for data or code read by that primary module.
Avoid:
```text
modules/shell/fish/fish.nix
modules/shell/fish/config.fish
```
Use:
```text
modules/shell/fish/default.nix
modules/shell/fish/config.fish
```
### Use non-module `default.nix` files only for directory assembly
Outside `modules/`, use `default.nix` to connect files in its directory.
Do not define primary assembly functions in `default.nix`.
Put primary assembly functions in named files.
Avoid:
```text
flake/default.nix # defines mkHost
```
Use:
```text
flake/default.nix # imports ./hosts.nix
flake/hosts.nix # defines mkHosts
```
### Keep assembly files focused on one concept
Give each assembly file under `flake/` one top-level concept.
Keep helper functions private to that file when no other file needs them.
Export the constructor for the concept when the concept has one.
Name that constructor `mk<Concept>`.
Avoid:
```nix
{
inherit collectNixFiles mkHost mkHosts;
}
```
Use:
```nix
{
inherit mkHosts;
}
```
## Function Arguments
### Use closed interfaces for assembly files and library files
Use exact function argument patterns for assembly files and library files.
The caller owns the argument set for these files.
The imported file names each accepted attribute.
The imported file rejects stale or accidental extra attributes.
Avoid:
```nix
{
inputs,
lib,
self,
...
}:
```
Use:
```nix
{
inputs,
lib,
self,
}:
```
### Pass function arguments explicitly
Pass only the values an assembly file or library file needs.
Bind `lib` to the base `nixpkgs.lib` value at the call site.
Use `inherit` only for values that keep the same name.
Avoid:
```nix
import ./flake inputs
```
Use:
```nix
let
inherit (nixpkgs) lib;
in
import ./flake { inherit lib inputs self; }
```
### Keep NixOS module interfaces open
Files under `hosts/`, `guests/`, and `modules/` return NixOS modules.
`nixosSystem` owns their argument shape.
Name each argument the module uses.
Keep `...` so the module accepts standard arguments and special arguments.
Avoid:
```nix
{
config,
lib,
pkgs,
registry,
}:
```
Use:
```nix
{
config,
lib,
pkgs,
registry,
...
}:
```
## `let` Bindings
### Use role-specific binding names
A `let` binding name must explain the value's role in the expression.
Avoid names that describe only the value's type.
Avoid:
```nix
let
names = lib.attrNames hosts;
in
lib.genAttrs names mkHost
```
Use:
```nix
let
hostNames = lib.attrNames hosts;
in
lib.genAttrs hostNames mkHost
```
## Standard Library Calls
### Qualify standard library calls in flake assembly
Use qualified standard library calls inside assembly files.
Use `lib.mapAttrs` instead of inheriting `mapAttrs` from `lib`.
This keeps the source of each function visible.
Avoid:
```nix
let
inherit (lib) mapAttrs;
in
mapAttrs f attrs
```
Use:
```nix
lib.mapAttrs f attrs
```

View File

@@ -76,14 +76,13 @@
{ self, nixpkgs, ... }@inputs:
let
inherit (nixpkgs) lib;
my = import ./lib { inherit lib inputs self; };
flake = import ./flake { inherit lib inputs self; };
in
{
# Helper functions for discovering and building hosts.
lib = my;
# Every host under hosts/ is discovered and built.
nixosConfigurations = my.mkHosts (self + "/hosts");
nixosConfigurations = flake.mkHosts {
root = self + "/hosts";
};
# A project shell for agent-local resources that should travel with this
# checkout rather than the operator's global profile.

61
flake/default.nix Normal file
View File

@@ -0,0 +1,61 @@
{
lib,
inputs,
self,
}:
let
# TODO: refactor the registry code
registryAssembly = import ./registry.nix { inherit lib self; };
util = import ../lib { inherit lib; };
mkModules = import ./module.nix { inherit util; };
modules = mkModules {
root = self + "/modules";
};
#----------------------------------------------------------
guestFiles = util.collectNixFiles (self + "/guests");
collectedRegistry = registryAssembly.collect { hosts = self.nixosConfigurations; };
specialArgs = {
inherit
flake
inputs
self
;
registry = collectedRegistry;
};
flake = {
inherit (hosts) mkHosts;
inherit (guests) mkGuest;
inherit util;
};
guests = import ./guest.nix {
inherit
inputs
lib
self
specialArgs
registryAssembly
util
;
repositoryModules = modules;
};
hosts = import ./host.nix {
inherit
inputs
lib
specialArgs
guestFiles
registryAssembly
;
repositoryModules = modules;
};
in
flake

View File

@@ -3,22 +3,12 @@
lib,
self,
specialArgs,
bridgeName,
moduleFiles,
registry,
repositoryModules,
registryAssembly,
util,
}:
let
inherit (lib) escapeShellArg mapAttrsToList;
deriveMac =
name:
let
hash = builtins.hashString "sha256" name;
octet = i: builtins.substring (i * 2) 2 hash;
in
lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]);
guest =
mkGuest =
{
name,
interior ? { },
@@ -59,13 +49,13 @@ let
createdMounts = lib.filterAttrs (_guestPath: m: m.create != null) cfg.mounts;
mountCreateScript = lib.concatStringsSep "\n" (
mapAttrsToList (
lib.mapAttrsToList (
_guestPath: m:
let
create = m.create;
in
''
${config.system.path}/bin/install -d -o ${escapeShellArg (toString create.owner)} -g ${escapeShellArg create.group} -m ${escapeShellArg create.mode} ${escapeShellArg m.hostPath}
${config.system.path}/bin/install -d -o ${lib.escapeShellArg (toString create.owner)} -g ${lib.escapeShellArg create.group} -m ${lib.escapeShellArg create.mode} ${lib.escapeShellArg m.hostPath}
''
) createdMounts
);
@@ -113,7 +103,7 @@ let
};
mac = lib.mkOption {
type = lib.types.str;
default = deriveMac name;
default = util.deriveMac name;
defaultText = lib.literalMD "a stable address derived from the guest's namespace path";
example = "bc:24:11:00:00:01";
description = ''
@@ -136,7 +126,7 @@ let
enable = lib.mkEnableOption "a reverse-proxy route for this guest";
route = lib.mkOption {
type = self.lib.registry.definitions.route.type;
type = registryAssembly.definitions.route.type;
description = "Route registry entry published for this guest.";
};
};
@@ -316,7 +306,7 @@ let
}
(lib.mkIf networked (
let
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (bridgeName cfg.vlan)}.device";
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device";
in
{
after = [ bridgeDevice ];
@@ -329,7 +319,7 @@ let
containers.${machineName} = lib.mkIf (cfg.backend == "container") {
autoStart = cfg.autoStart;
privateNetwork = lib.mkDefault true;
hostBridge = lib.mkIf networked (bridgeName cfg.vlan);
hostBridge = lib.mkIf networked (util.bridgeName cfg.vlan);
privateUsers = lib.mkDefault "no";
additionalCapabilities = lib.optionals cfg.nesting [ "CAP_NET_ADMIN" ];
allowedDevices = lib.optionals cfg.nesting [
@@ -349,9 +339,11 @@ let
config = {
imports =
moduleFiles
[
repositoryModules
]
++ [
registry.module
registryAssembly.module
inputs.sops-nix.nixosModules.sops
inputs.stylix.nixosModules.stylix
{
@@ -366,5 +358,5 @@ let
};
in
{
inherit deriveMac guest;
inherit mkGuest;
}

51
flake/host.nix Normal file
View File

@@ -0,0 +1,51 @@
{
inputs,
lib,
specialArgs,
repositoryModules,
# TODO refactor guests
guestFiles,
# TODO refactor registry
registryAssembly,
}:
let
mkHost =
{
root,
name,
}:
lib.nixosSystem {
inherit specialArgs;
modules =
guestFiles
++ [
inputs.chaotic.nixosModules.default
inputs.disko.nixosModules.disko
inputs.sops-nix.nixosModules.sops
inputs.stylix.nixosModules.stylix
repositoryModules
# TODO: refactor
registryAssembly.module (root + "/${name}")
{
networking.hostName = name;
modules.host.enable = true;
}
];
};
in
{
mkHosts =
{ root }:
let
directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root);
names = lib.attrNames directories;
in
lib.genAttrs names (
name:
mkHost { inherit root name; }
);
}

5
flake/module.nix Normal file
View File

@@ -0,0 +1,5 @@
{ util }:
{ root }:
{
imports = util.collectNixFiles root;
}

View File

@@ -1,7 +1,7 @@
{
config,
lib,
my,
flake,
...
}:
let
@@ -10,7 +10,7 @@ let
in
{
imports = [
(my.guest {
(flake.mkGuest {
name = "actualbudget";
interior = {
modules.services.actualbudget = {

View File

@@ -1,7 +1,7 @@
{
config,
lib,
my,
flake,
...
}:
let
@@ -35,7 +35,7 @@ let
in
{
imports = [
(my.guest {
(flake.mkGuest {
name = "copyparty";
interior = {
modules.services.copyparty = {

View File

@@ -1,7 +1,7 @@
args@{ my, ... }:
args@{ flake, ... }:
# A sample guest whose interior runs an OCI container on Podman.
# The image is pulled at runtime, so the guest builds with no build-time fetch.
my.guest {
flake.mkGuest {
name = "nesting-sample";
interior = {
virtualisation.oci-containers.containers.hello = {

View File

@@ -1,7 +1,7 @@
{
config,
lib,
my,
flake,
...
}:
let
@@ -9,7 +9,7 @@ let
in
{
imports = [
(my.guest {
(flake.mkGuest {
name = "reverse-proxy";
interior = {
modules.services.reverse-proxy.enable = true;

View File

@@ -1,5 +1,5 @@
args@{ my, ... }:
args@{ flake, ... }:
# The tracer-bullet guest: the thinnest complete path from discovery to a
# running nested container. Its interior is just the guest-base — the baseline
# toolset and SSH access — so it proves the concept without carrying a service.
my.guest { name = "sample"; } args
flake.mkGuest { name = "sample"; } args

View File

@@ -1,100 +1,9 @@
{
lib,
inputs,
self,
}:
{ lib }:
let
inherit (lib)
attrNames
filterAttrs
genAttrs
flatten
hasSuffix
mapAttrsToList
;
registry = import ./registry.nix { inherit lib self; };
collectNixFiles =
dir:
flatten (
mapAttrsToList (
name: type:
let
path = dir + "/${name}";
in
if type == "directory" then
collectNixFiles path
else if type == "regular" && hasSuffix ".nix" name then
[ path ]
else
[ ]
) (builtins.readDir dir)
);
moduleFiles = collectNixFiles (self + "/modules");
guestFiles = collectNixFiles (self + "/guests");
specialArgs = {
inherit inputs self;
my = self.lib;
registry = self.lib.registry.collect { hosts = self.nixosConfigurations; };
};
bridgeName = id: "br-vlan${toString id}";
guests = import ./guests.nix {
inherit
inputs
lib
self
specialArgs
bridgeName
moduleFiles
registry
;
};
mkHost =
{
hostName,
system ? "x86_64-linux",
}:
inputs.nixpkgs.lib.nixosSystem {
inherit system specialArgs;
modules =
moduleFiles
++ guestFiles
++ [
inputs.chaotic.nixosModules.default
inputs.disko.nixosModules.disko
inputs.sops-nix.nixosModules.sops
inputs.stylix.nixosModules.stylix
registry.module
(self + "/hosts/${hostName}")
{
networking.hostName = hostName;
modules.host.enable = true;
}
];
};
mkHosts =
hostsDir:
let
hostNames = attrNames (filterAttrs (_name: type: type == "directory") (builtins.readDir hostsDir));
in
genAttrs hostNames (hostName: mkHost { inherit hostName; });
files = import ./files.nix { inherit lib; };
network = import ./network.nix { inherit lib; };
in
{
inherit
collectNixFiles
mkHost
mkHosts
bridgeName
registry
;
inherit (guests) deriveMac guest;
inherit (files) collectNixFiles;
inherit (network) bridgeName deriveMac;
}

22
lib/files.nix Normal file
View File

@@ -0,0 +1,22 @@
{ lib }:
let
collectNixFiles =
dir:
lib.flatten (
lib.mapAttrsToList (
name: type:
let
path = dir + "/${name}";
in
if type == "directory" then
collectNixFiles path
else if type == "regular" && lib.hasSuffix ".nix" name then
[ path ]
else
[ ]
) (builtins.readDir dir)
);
in
{
inherit collectNixFiles;
}

12
lib/network.nix Normal file
View File

@@ -0,0 +1,12 @@
{ lib }:
{
bridgeName = id: "br-vlan${toString id}";
deriveMac =
name:
let
hash = builtins.hashString "sha256" name;
octet = i: builtins.substring (i * 2) 2 hash;
in
lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]);
}

View File

@@ -1,6 +1,7 @@
{
config,
lib,
flake,
pkgs,
inputs,
...
@@ -63,8 +64,7 @@ in
useGlobalPkgs = true;
useUserPackages = true;
extraSpecialArgs = {
inherit inputs;
my = inputs.self.lib;
inherit flake inputs;
};
users.${user.name} = {
home.username = user.name;

View File

@@ -1,14 +1,14 @@
{
config,
lib,
my,
flake,
...
}:
# The host networking foundation: per-VLAN bridges over a tagged trunk.
let
cfg = config.modules.network.vlans;
inherit (my) bridgeName;
inherit (flake.util) bridgeName;
vlanName = id: "${cfg.trunk}.${toString id}";
in