From acdc497e4ced8969c29ce2ccfe86ca0a9f8a784a Mon Sep 17 00:00:00 2001 From: alexion Date: Sun, 16 Aug 2026 14:42:16 -0400 Subject: [PATCH] feat(lib): add stable service uid registry --- AGENTS.md | 4 +-- flake.nix | 28 ++++++++++----- lib/default.nix | 4 +++ lib/service-identities.nix | 59 +++++++++++++++++++++++++++++++ modules/services/actualbudget.nix | 5 +-- 5 files changed, 88 insertions(+), 12 deletions(-) create mode 100644 lib/service-identities.nix diff --git a/AGENTS.md b/AGENTS.md index f6e97bd..adcd3a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,8 +40,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla It is deployed on `neogaia` and verified: a commit in a repository outside this checkout is authored `alexion ` with no override. Verify it that way rather than from this checkout, whose `.git/config` carries the same identity and would mask a broken module. `~/.gitconfig` (a second global file that outranks the flake-managed `~/.config/git/config`) currently holds only a `tea` credential helper and no `user.*`, so it does not shadow the identity, but it is undeclared and will not survive a reimage. -- The primary build/verify seam for any Host is `nix flake check`, which builds `checks.x86_64-linux.` (the system toplevel). - Cheap targeted checks use `nix eval .#nixosConfigurations..config...`. +- The primary build/verify seam is `nix flake check`, which builds project-level checks plus `checks.x86_64-linux.` system toplevels. + Cheap targeted host checks use `nix eval .#nixosConfigurations..config...`. - chaotic-nyx must **not** follow our `nixpkgs`, and its packages are built against chaotic's own pinned nixpkgs (its overlay defaults to `onTopOf = "flake-nixpkgs"`, the cache-friendly path). That is what lets the `nyx-cache.chaotic.cx` binary cache hit instead of compiling the CachyOS kernel from source. The tradeoff is that chaotic packages do not see our `unstable`/`stable` overlays. diff --git a/flake.nix b/flake.nix index d2f8ea6..e1c2a5d 100644 --- a/flake.nix +++ b/flake.nix @@ -98,13 +98,25 @@ ]; }; - # `nix flake check` builds each host's toplevel. - checks.x86_64-linux = lib.mapAttrs ( - name: host: - if host.config.warnings == [] then - host.config.system.build.toplevel - else - throw "Host ${name} has evaluation warnings:\n${lib.concatStringsSep "\n" host.config.warnings}" - ) self.nixosConfigurations; + # `nix flake check` builds host toplevels and project-level checks. + checks.x86_64-linux = + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + serviceIdentities = import ./lib/service-identities.nix { inherit lib; }; + serviceIdentityTestsPass = lib.all (result: result) (lib.attrValues serviceIdentities.tests); + in + (lib.mapAttrs ( + name: host: + if host.config.warnings == [] then + host.config.system.build.toplevel + else + throw "Host ${name} has evaluation warnings:\n${lib.concatStringsSep "\n" host.config.warnings}" + ) self.nixosConfigurations) + // { + service-identities = pkgs.runCommand "service-identities-check" { } '' + ${lib.optionalString (!serviceIdentityTestsPass) "exit 1"} + touch $out + ''; + }; }; } diff --git a/lib/default.nix b/lib/default.nix index 62b0a30..ba72278 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -13,6 +13,8 @@ let mapAttrsToList ; + serviceIdentityRegistry = import ./service-identities.nix { inherit lib; }; + # Recursively collect every `.nix` file under `dir` as a flat list, for a # module's `imports`. collectNixFiles = @@ -418,4 +420,6 @@ in guest bridgeName ; + inherit (serviceIdentityRegistry) serviceUid; + serviceIdentities = serviceIdentityRegistry.identities; } diff --git a/lib/service-identities.nix b/lib/service-identities.nix new file mode 100644 index 0000000..d389267 --- /dev/null +++ b/lib/service-identities.nix @@ -0,0 +1,59 @@ +{ lib }: +let + defaultIdentities = { + actualbudget = 20001; + copyparty = 20002; + }; + + duplicateUidMessage = duplicates: '' + Duplicate stable service UID allocation(s): ${lib.concatStringsSep ", " ( + map (entry: "${toString entry.uid} is assigned to ${lib.concatStringsSep ", " entry.names}") duplicates + )} + ''; + + mk = identities: + let + names = lib.attrNames identities; + namesByUid = lib.foldl' ( + acc: name: + let + uid = toString identities.${name}; + in + acc // { + ${uid} = (acc.${uid} or [ ]) ++ [ name ]; + } + ) { } names; + duplicates = lib.mapAttrsToList (uid: namesForUid: { + uid = lib.toInt uid; + names = namesForUid; + }) (lib.filterAttrs (_uid: namesForUid: builtins.length namesForUid > 1) namesByUid); + checkedIdentities = + assert lib.assertMsg (duplicates == [ ]) (duplicateUidMessage duplicates); + identities; + in + rec { + inherit checkedIdentities; + + serviceUid = name: + if builtins.hasAttr name checkedIdentities then + checkedIdentities.${name} + else + throw "Unknown stable service identity `${name}`. Known identities: ${lib.concatStringsSep ", " (lib.attrNames checkedIdentities)}"; + }; + + default = mk defaultIdentities; + + tests = { + actualbudgetUid = default.serviceUid "actualbudget" == 20001; + copypartyUid = default.serviceUid "copyparty" == 20002; + missingLookupFails = !(builtins.tryEval (default.serviceUid "missing-service")).success; + duplicateUidFails = !(builtins.tryEval (mk { + first = 20003; + second = 20003; + }).checkedIdentities).success; + }; +in +default // { + identities = default.checkedIdentities; + inherit tests; +} diff --git a/modules/services/actualbudget.nix b/modules/services/actualbudget.nix index 4ed8b44..68683bd 100644 --- a/modules/services/actualbudget.nix +++ b/modules/services/actualbudget.nix @@ -1,6 +1,7 @@ { config, lib, + my, ... }: let @@ -30,8 +31,8 @@ in uid = lib.mkOption { type = lib.types.ints.positive; - default = 20001; - description = "Fixed uid for the Actual Budget service user."; + default = my.serviceUid "actualbudget"; + description = "Stable uid for the Actual Budget service user."; }; group = lib.mkOption { -- 2.47.3