From d3b3b646d383b164647fe3c7d25ddd3a489544e7 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:33:17 -0400 Subject: [PATCH 01/13] fix(magnetite): confine the builder account's key to the nix protocol The nix-remote-build key that stibnite holds authorized an unrestricted shell on magnetite's builder account, which is a nix trusted user on a host whose sshd accepts connections from the public internet. Wrap that key with restrict and a forced nix-daemon --stdio command, so it can start only the protocol its single consumer speaks: the ssh-ng buildMachines entry in modules/system/magnetite-builder.nix, which authenticates with the matching private half at config.clan.core.vars.generators.nix-remote-build.files.key.path. The forced command interpolates config.nix.package rather than a bare program name so it resolves to the nix deployed on magnetite, and the account keeps its default bash shell because sshd runs the forced command through it. Drop crs58's personal keys from the same account. They authorize the wheel account fleet-wide through modules/system/admins.nix, where wheel also has passwordless sudo, so sudo -u builder still reaches the account for debugging and no access is lost. The builder entry in nix.settings.trusted-users is deliberately unchanged; removing it depends on per-machine store signing, which is tracked separately. Verified by evaluation, which renders the key list as a single entry carrying restrict, the store path of the deployed nix, and no personal keys. Ran the treefmt, structure-nixos-configurations and deployment-safety checks. No deployment was performed, so the live remote build path is untested. --- modules/machines/nixos/magnetite/default.nix | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/machines/nixos/magnetite/default.nix b/modules/machines/nixos/magnetite/default.nix index 76cbc0b23..f0740f288 100644 --- a/modules/machines/nixos/magnetite/default.nix +++ b/modules/machines/nixos/magnetite/default.nix @@ -396,8 +396,17 @@ in users.users.builder = { isNormalUser = true; description = "Remote nix build user"; - openssh.authorizedKeys.keys = inputs.self.users.crs58.meta.sshKeys ++ [ - inputs.self.darwinConfigurations.stibnite.config.clan.core.vars.generators.nix-remote-build.files."key.pub".value + # nix-daemon --stdio is exactly what an ssh-ng caller would otherwise + # invoke (`remote-program` defaults to nix-daemon), so forcing it serves + # the build protocol and discards anything else the client asks for. + # sshd runs the forced command through the account's login shell, so the + # shell must stay executable; a nologin shell would break the protocol + # rather than harden it. Mirrors the stibnite direction of this pair. + openssh.authorizedKeys.keys = [ + ''restrict,command="${config.nix.package}/bin/nix-daemon --stdio" ${ + lib.removeSuffix "\n" + inputs.self.darwinConfigurations.stibnite.config.clan.core.vars.generators.nix-remote-build.files."key.pub".value + }'' ]; }; From 4d39c00b1878bfa0c71ce2d6516da52c80eefb35 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:47:49 -0400 Subject: [PATCH 02/13] feat(system): add the stibnite build-access module and wire both machines stibnite is the fleet's only aarch64-darwin machine. magnetite is x86_64-linux and cannot build darwin derivations, and no nix.buildMachines entry named stibnite existed, so darwin derivations had no build target. modules/system/stibnite-access.nix exports three aspects. The nixos aspect services.stibnite-builder computes a nix.buildMachines entry, which magnetite splices into its own nix.buildMachines; the module never sets that option itself. The same aspect computes a read-only storeUri and materializes it at /etc/nix/stibnite-store-uri, for a caller that wants nix build --store to run the build inside stibnite's store rather than copying the output closure back. The darwin aspect services.stibnite-build-host creates the nixbuild account on stibnite and authorizes the two keys. The build key is authorized on nixbuild with restrict and a forced nix-daemon --stdio command, which is the program an ssh-ng caller would have invoked, so the key can speak the protocol and nothing else. nixbuild is also placed in nix.settings.trusted-users, which is store-root equivalent on stibnite. That grant is required rather than incidental: an untrusted account cannot receive the unsigned store paths a caller evaluated itself, because the daemon rejects them as lacking a signature by a trusted key. magnetite's own builder account carries the same grant. The session key is authorized on crs58 without a forced command and is the broader of the two credentials, since crs58 is in the admin group and stibnite's trusted-users includes @admin. Activation adds nixbuild to the com.apple.access_ssh service ACL. sshd gates on that ACL, which on stibnite nests only the admin group, so a non-admin build account is refused before its key is consulted and the failure reads as Permission denied (publickey) with a correct key installed. The builder entry advertises aarch64-darwin alone, because nix config show extra-platforms on stibnite reports that system and no other. maxJobs is 4 against 18 logical cores and 64 GiB on a machine that is also in interactive use and commits 12 cores and 48 GiB to the rosetta VM. --- modules/machines/darwin/stibnite/default.nix | 22 ++ modules/machines/nixos/magnetite/default.nix | 26 ++ modules/system/stibnite-access.nix | 361 +++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 modules/system/stibnite-access.nix diff --git a/modules/machines/darwin/stibnite/default.nix b/modules/machines/darwin/stibnite/default.nix index 3e520d7b5..69283d935 100644 --- a/modules/machines/darwin/stibnite/default.nix +++ b/modules/machines/darwin/stibnite/default.nix @@ -40,6 +40,7 @@ in zt-dns zt-services-trust magnetite-builder + stibnite-build-host # Not importing users module (defines testuser at UID 550) # stibnite defines its own user (crs58) ]); @@ -224,6 +225,27 @@ in # Offload native x86_64-linux builds to magnetite over ZeroTier. services.magnetite-builder.enable = true; + # Inbound side of the same asymmetry: stibnite is the fleet's only + # aarch64-darwin machine, so it serves darwin builds to hosts that cannot + # perform them. The two keypairs come from magnetite and are authorized + # separately for independent revocation and rotation. The build key is + # confined at the SSH boundary to the nix protocol. The session key is + # broader: it logs in as admin-group crs58, a Nix trusted user, so it has + # build authority plus shell access. + # Both encrypted private halves and public values are committed under + # vars/per-machine/magnetite/. `clan vars generate magnetite` populates + # them; only magnetite and authorized users can decrypt the private + # halves. + services.stibnite-build-host = { + enable = true; + buildKeys = [ + inputs.self.nixosConfigurations.magnetite.config.clan.core.vars.generators.stibnite-nix-build.files."key.pub".value + ]; + sessionKeys = [ + inputs.self.nixosConfigurations.magnetite.config.clan.core.vars.generators.stibnite-agent-session.files."key.pub".value + ]; + }; + # Colima for OCI container management (complementary to nix-rosetta-builder) services.colima = { enable = true; diff --git a/modules/machines/nixos/magnetite/default.nix b/modules/machines/nixos/magnetite/default.nix index f0740f288..f22ebef5c 100644 --- a/modules/machines/nixos/magnetite/default.nix +++ b/modules/machines/nixos/magnetite/default.nix @@ -41,6 +41,8 @@ in hm-sops-bridge niks3 ssh-known-hosts + stibnite-builder + stibnite-session buildbot nixbot gitea @@ -82,6 +84,30 @@ in ]; }; + # magnetite is x86_64-linux and cannot build aarch64-darwin derivations, + # so darwin work is dispatched to stibnite, the fleet's only machine of + # that system. Two mechanisms, two callers: + # nix.buildMachines below — a local nix build by an operator who wants + # the darwin result in magnetite's store. + # /etc/nix/stibnite-store-uri — a caller that wants the build to happen + # entirely in stibnite's store with nothing copied back. + # nixbot.toml sets attribute = "checks.x86_64-linux", which prevents CI + # from evaluating or requesting aarch64-darwin work and makes this builder + # unreachable from CI. modules/nixos/nixbot.nix and + # modules/nixos/buildbot.nix each set buildSystems = [ "x86_64-linux" ] + # as an independent second layer. These controls remain because stibnite + # is a laptop without guaranteed availability and a sleeping machine + # could gate CI. + services.stibnite-builder.enable = true; + nix.buildMachines = config.services.stibnite-builder.buildMachines; + + # The build and session keys are separately authorized for independent + # revocation and rotation. Only the build key is confined to the Nix + # protocol by `restrict` and a forced command. The session key is broader: + # it grants an unrestricted login as admin-group crs58, already a Nix + # trusted user, with build authority plus shell. + services.stibnite-session.enable = true; + # User configuration managed via clan inventory users service (modules/clan/inventory/services/users/cameron.nix). security.sudo.wheelNeedsPassword = false; diff --git a/modules/system/stibnite-access.nix b/modules/system/stibnite-access.nix new file mode 100644 index 000000000..678fd5744 --- /dev/null +++ b/modules/system/stibnite-access.nix @@ -0,0 +1,361 @@ +# Access to stibnite (aarch64-darwin) from the fleet, in three separable pieces. +# +# stibnite is the fleet's only aarch64-darwin machine, and until this module +# nothing declared it as a build target: magnetite is x86_64-linux and cannot +# build darwin derivations, and stibnite's own nix.buildMachines list carries +# only the rosetta VM and magnetite. The two mechanisms below are the two ways +# a caller can reach stibnite's store, and they are not interchangeable. +# +# services.stibnite-builder — remote BUILDER (nix.buildMachines). +# The caller's nix daemon copies the input closure to stibnite, builds there, +# and copies the output closure back, so the result exists in the CALLER's +# store. Callers: developers and operators who need an aarch64-darwin result +# locally, for example to build or test Darwin configurations from magnetite. +# +# services.stibnite-builder.storeUri — remote STORE (--store ssh-ng://). +# The whole build happens inside stibnite's store; evaluation stays with the +# caller and no closure is copied back. Callers: a machine whose own store is +# empty and stays empty — an ephemeral CI runner, a fresh container, an +# installer image — where a remote builder would spend the whole job +# populating a store that is about to be discarded. Nothing lands locally, so +# a caller that needs the output path locally wants the builder above. +# +# services.stibnite-session — an unrestricted login as admin-group crs58, who +# is already a Nix trusted user, so it grants build authority plus shell. Its +# key is separately authorized for independent revocation and rotation; only +# the build key is confined to the Nix protocol by `restrict` and a forced +# command. +# +# services.stibnite-build-host — the stibnite side: the account restricted to +# nix-daemon at the SSH boundary and trusted with store-root-equivalent +# authority, plus the authorization for both keypairs. +{ lib, ... }: +let + # Deterministic ZeroTier IPv6, matching modules/system/ssh-known-hosts.nix + # and modules/machines/nixos/cinnabar/zt-dns.nix. + stibniteZt = "fddb:4344:343b:14b9:399:9324:19d9:3451"; + + # clan's public var values carry the trailing newline of the file they were + # read from, and an authorized_keys entry is one line. + trimKey = key: lib.removeSuffix "\n" key; + + mkSshBlock = args: '' + Host ${args.hostAlias} + HostName ${stibniteZt} + User ${args.sshUser} + IdentityFile ${args.sshKeyPath} + IdentitiesOnly yes + HostKeyAlias stibnite.zt + ''; + + builderOptions = + config: + let + cfg = config.services.stibnite-builder; + sshKeyPath = config.clan.core.vars.generators.stibnite-nix-build.files.key.path; + in + { + options.services.stibnite-builder = { + enable = lib.mkEnableOption "build access to stibnite, the fleet's aarch64-darwin machine"; + + systems = lib.mkOption { + type = lib.types.listOf lib.types.str; + # aarch64-darwin only. `nix config show extra-platforms` on stibnite + # reports aarch64-darwin and nothing else, so advertising + # x86_64-darwin would route derivations the machine refuses to build. + default = [ "aarch64-darwin" ]; + description = "Systems stibnite can build for."; + }; + + maxJobs = lib.mkOption { + type = lib.types.int; + # stibnite has 18 logical cores (12 performance + 6 efficiency) and + # 64 GiB of memory. It is also a laptop in interactive use that + # commits 12 cores and 48 GiB to the rosetta builder VM and the same + # again to colima when either is running, so the remote share is + # deliberately a minority of the machine: 4 concurrent jobs, each + # free to use every core through the daemon's own `cores` setting. + default = 4; + description = "Maximum simultaneous build jobs dispatched to stibnite."; + }; + + speedFactor = lib.mkOption { + type = lib.types.int; + # Inert while stibnite is the only aarch64-darwin builder: nix + # compares speedFactor among machines that can build the same system, + # and there is no second darwin machine to compare against. 1 is the + # neutral value to raise if one appears. + default = 1; + description = "Scheduler weight, compared only against other aarch64-darwin builders."; + }; + + supportedFeatures = lib.mkOption { + type = lib.types.listOf lib.types.str; + # Measured with `nix config show system-features` on stibnite, minus + # what the value means rather than what nix prints. apple-virt is + # real and stibnite is the only machine in the fleet that has it. + # big-parallel is real on 18 cores. "benchmark" is dropped because + # timings taken on a laptop under interactive load are not + # measurements, and "nixos-test" is dropped because it is a Linux + # sandbox capability that nix lists unconditionally. + default = [ + "apple-virt" + "big-parallel" + ]; + description = "Build features stibnite advertises to the scheduler."; + }; + + sshUser = lib.mkOption { + type = lib.types.str; + default = "nixbuild"; + description = "Build account restricted to nix-daemon at the SSH boundary and trusted with store-root-equivalent authority; see services.stibnite-build-host.user."; + }; + + hostAlias = lib.mkOption { + type = lib.types.str; + # Distinct from the "stibnite" / "stibnite.zt" aliases an interactive + # session uses, so the build identity and the session identity cannot + # be reached through each other's alias by accident. + default = "stibnite-builder"; + description = "ssh Host alias the nix daemon resolves to stibnite's ZeroTier address."; + }; + + buildMachines = lib.mkOption { + type = lib.types.listOf (lib.types.attrsOf lib.types.anything); + default = + if cfg.enable then + [ + { + hostName = cfg.hostAlias; + sshUser = cfg.sshUser; + protocol = "ssh-ng"; + sshKey = sshKeyPath; + systems = cfg.systems; + maxJobs = cfg.maxJobs; + speedFactor = cfg.speedFactor; + supportedFeatures = cfg.supportedFeatures; + mandatoryFeatures = [ ]; + } + ] + else + [ ]; + defaultText = lib.literalExpression "single-element stibnite buildMachines entry when enabled, else []"; + description = "Computed nix.buildMachines entry. Consumers splice this into their own nix.buildMachines; this module never sets nix.buildMachines itself."; + }; + + storeUri = lib.mkOption { + type = lib.types.str; + default = "ssh-ng://${cfg.sshUser}@${cfg.hostAlias}?ssh-key=${sshKeyPath}"; + defaultText = lib.literalExpression "ssh-ng URI naming the build account, the ssh alias and the build key"; + readOnly = true; + description = '' + Remote-store URI for `nix build --store`, the other mechanism. + Materialized at /etc/nix/stibnite-store-uri so a caller reads the + key path from configuration rather than retyping it. The key is + root-owned, so a non-root caller needs `sudo -E`. + ''; + }; + }; + }; + + # Generated rather than operator-populated: no plaintext private half leaves + # magnetite. Clan commits the encrypted private half and public value, and + # only magnetite and authorized users can decrypt the private half. This + # avoids manual transcription. The operator step is `clan vars generate + # ` plus committing both outputs; stibnite reads the public value at + # evaluation time. + mkBuildKeyGenerator = pkgs: { + clan.core.vars.generators.stibnite-nix-build = { + files.key = { }; + files."key.pub".secret = false; + runtimeInputs = [ pkgs.openssh ]; + script = '' + ssh-keygen -t ed25519 -N "" -C "stibnite-nix-build" -f "$out"/key + ''; + }; + }; + + sessionOptions = { + options.services.stibnite-session = { + enable = lib.mkEnableOption "interactive ssh access to stibnite under a dedicated keypair"; + + sshUser = lib.mkOption { + type = lib.types.str; + default = "crs58"; + description = "Account on stibnite this key logs in as."; + }; + + hostAlias = lib.mkOption { + type = lib.types.str; + default = "stibnite-session"; + description = "ssh Host alias for the session identity, distinct from the builder alias."; + }; + }; + }; + + mkSessionKeyGenerator = pkgs: { + clan.core.vars.generators.stibnite-agent-session = { + files.key = { }; + files."key.pub".secret = false; + runtimeInputs = [ pkgs.openssh ]; + script = '' + ssh-keygen -t ed25519 -N "" -C "stibnite-agent-session" -f "$out"/key + ''; + }; + }; +in +{ + flake.modules.nixos.stibnite-builder = + { config, pkgs, ... }: + let + cfg = config.services.stibnite-builder; + sshKeyPath = config.clan.core.vars.generators.stibnite-nix-build.files.key.path; + in + (builderOptions config) + // { + config = lib.mkIf cfg.enable ( + (mkBuildKeyGenerator pkgs) + // { + nix.distributedBuilds = true; + + # stibnite subscribes to the same binary cache, so a dependency it can + # substitute itself is not worth shipping to it over ZeroTier. + nix.settings.builders-use-substitutes = true; + + programs.ssh.extraConfig = mkSshBlock { + inherit (cfg) hostAlias sshUser; + inherit sshKeyPath; + }; + + environment.etc."nix/stibnite-store-uri".text = "${cfg.storeUri}\n"; + } + ); + }; + + flake.modules.nixos.stibnite-session = + { config, pkgs, ... }: + let + cfg = config.services.stibnite-session; + sshKeyPath = config.clan.core.vars.generators.stibnite-agent-session.files.key.path; + in + sessionOptions + // { + config = lib.mkIf cfg.enable ( + (mkSessionKeyGenerator pkgs) + // { + programs.ssh.extraConfig = mkSshBlock { + inherit (cfg) hostAlias sshUser; + inherit sshKeyPath; + }; + } + ); + }; + + # stibnite's own side: one account both build mechanisms land in, and the + # authorization for the two keypairs above. + flake.modules.darwin.stibnite-build-host = + { config, lib, ... }: + let + cfg = config.services.stibnite-build-host; + in + { + options.services.stibnite-build-host = { + enable = lib.mkEnableOption "the remote build account restricted to nix-daemon at the SSH boundary and trusted with store-root-equivalent authority"; + + user = lib.mkOption { + type = lib.types.str; + default = "nixbuild"; + description = "Account the nix build protocol is served under."; + }; + + uid = lib.mkOption { + type = lib.types.int; + # Free on stibnite as of 2026-08-31: 501 crs58, 502 runner, + # 535 _dnscrypt-proxy are the only accounts at or above 500. + default = 530; + description = "UID for the build account."; + }; + + buildKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Public keys authorized to speak the nix build protocol. Each is + installed with a forced command and `restrict`, so it can run the + protocol and nothing else at the SSH boundary. This restricts which + program the key starts, not the nix daemon's authority. The account + is a Nix trusted user with store-root-equivalent authority: it can + cause arbitrary paths to enter the store and influence what the + daemon trusts. This grant is required to receive caller-evaluated + unsigned paths. + ''; + }; + + sessionUser = lib.mkOption { + type = lib.types.str; + default = "crs58"; + description = "Account the session keys below log in as."; + }; + + sessionKeys = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Public keys authorized for an ordinary interactive login, with no forced command."; + }; + + authorizeSshAccessGroup = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Add the build account to macOS's `com.apple.access_ssh` service + ACL at activation. On stibnite that ACL nests only the admin + group, and the build account is deliberately not an admin, so + without this every dispatch fails as `Permission denied + (publickey)` with a correct key installed. Set false to manage the + ACL by hand with + `dseditgroup -o edit -a -t user com.apple.access_ssh`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.${cfg.user} = { + inherit (cfg) uid; + description = "Remote nix build account"; + # sshd runs a forced command through the account's shell, so + # /usr/bin/false (nix-darwin's default for shell = null) would break + # the build protocol rather than harden it. Interactive use is + # foreclosed by the key options below, which sshd applies before the + # shell ever runs. + shell = "/bin/sh"; + # nix-daemon --stdio is exactly what an ssh-ng caller would have + # invoked (`remote-program` defaults to nix-daemon), so forcing it + # serves both the remote-builder and the remote-store mechanism and + # discards anything else the client asks for. Legacy `ssh://`, which + # would run `nix-store --serve`, is deliberately not served. + openssh.authorizedKeys.keys = map ( + key: ''restrict,command="${config.nix.package}/bin/nix-daemon --stdio" ${trimKey key}'' + ) cfg.buildKeys; + }; + + users.knownUsers = [ cfg.user ]; + + # This deliberate trusted-user grant is store-root-equivalent on + # stibnite: the account can cause arbitrary paths to enter the store and + # influence what the daemon trusts. The forced SSH command restricts + # which program the key starts, not that program's authority. The grant + # is required because an untrusted build account cannot receive unsigned + # caller-evaluated paths, matching magnetite's builder account. + nix.settings.trusted-users = [ cfg.user ]; + + users.users.${cfg.sessionUser}.openssh.authorizedKeys.keys = map trimKey cfg.sessionKeys; + + system.activationScripts.postActivation.text = lib.mkIf cfg.authorizeSshAccessGroup '' + if ! /usr/sbin/dseditgroup -o checkmember -m ${cfg.user} com.apple.access_ssh >/dev/null 2>&1; then + echo "Adding ${cfg.user} to the com.apple.access_ssh service ACL..." + /usr/sbin/dseditgroup -o edit -a ${cfg.user} -t user com.apple.access_ssh + fi + ''; + }; + }; +} From 326575a36cf13856fec62a75288f45aaf3ce9f27 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:47:49 -0400 Subject: [PATCH 03/13] chore(vars): add the generated stibnite-nix-build key record for magnetite The stibnite-nix-build generator produces the keypair magnetite uses to reach stibnite's nix daemon. The private half is committed encrypted and is decryptable by magnetite and by the users recorded alongside it; the public half is committed in clear, and stibnite's configuration reads it at evaluation time to authorize the nixbuild account. No plaintext private key material is committed. --- .../magnetite/stibnite-nix-build/key.pub/value | 1 + .../stibnite-nix-build/key/machines/magnetite | 1 + .../magnetite/stibnite-nix-build/key/secret | 18 ++++++++++++++++++ .../stibnite-nix-build/key/users/cameron | 1 + 4 files changed, 21 insertions(+) create mode 100644 vars/per-machine/magnetite/stibnite-nix-build/key.pub/value create mode 120000 vars/per-machine/magnetite/stibnite-nix-build/key/machines/magnetite create mode 100644 vars/per-machine/magnetite/stibnite-nix-build/key/secret create mode 120000 vars/per-machine/magnetite/stibnite-nix-build/key/users/cameron diff --git a/vars/per-machine/magnetite/stibnite-nix-build/key.pub/value b/vars/per-machine/magnetite/stibnite-nix-build/key.pub/value new file mode 100644 index 000000000..2385599f1 --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-nix-build/key.pub/value @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyo39zRk8tI0IZ238Vk2iOk7K80Rf9IEeOfbD/NkfQf stibnite-nix-build diff --git a/vars/per-machine/magnetite/stibnite-nix-build/key/machines/magnetite b/vars/per-machine/magnetite/stibnite-nix-build/key/machines/magnetite new file mode 120000 index 000000000..41bd9646c --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-nix-build/key/machines/magnetite @@ -0,0 +1 @@ +../../../../../../sops/machines/magnetite \ No newline at end of file diff --git a/vars/per-machine/magnetite/stibnite-nix-build/key/secret b/vars/per-machine/magnetite/stibnite-nix-build/key/secret new file mode 100644 index 000000000..2954cd0e1 --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-nix-build/key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:uizSA5+21CSLLu425FO1Z/lzwD8U35pRq+KwfSOhkM0FoQbUxHkjUre7fMVQwAQjwVYltcyVNkw//VecTXNJ6CaS6d2HHN3mVrXfy3puSiptU3eSPdiCCY4g3HZtZtBQbD8uzpWQqjif3msyDN5oA8ZcUpa7lrnSQPhtx4WyfsMx2JYpOEEc23wWsEg055irfCIR/QXu5z6w2mUTFCQF5rsP82ySR0o9a1X5czbBS9XHlfUDSQBY3Zf3UWLIoWTel6QoeZVpfJGQ2bl+59uHDtxOoD8Vqwml1lIcZyecuaquqr5P4EdShWQixTB2N1hdqx2xkZYYEXPtud4EFR/RVnR4W271wJ5jhaGNH61NDf+FUK9mea7elnznqbGVOLd7UCC1Xx+H36vpxzMfPkLVcT/grmuMJUq9zc38GF3TLezdoLsRiLrHhWQ4E3z1Vt+0ME5WaRhJb4QXSxZ0vl/rif32wnX1/ODS3mEsRDRTOEdR+ytL4ztvgcR8j652iEQZOuT1AbsicWVsyy1GdrpNjUaPolQPOS88fwqB,iv:xVyTJJ+BYxiTXldjAfinkUz8z/6MOQCuuluZgLoV+3Y=,tag:1+dKhn0da8NuLFYAMh6ucA==,type:str]", + "sops": { + "age": [ + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByQlFkaFFZQ3gzTFBqb3My\nQk1Ob2dIQU1VQ20xSjRsN0syUHZMSTJrRmxJCkdjK0wvZklTMDlFOHhuV1VwVE91\nMXQ5N1ZNcGozQzczUVA5OFZhcEptWjAKLS0tIFloMFFqbDJOaXJjSDg5SlNmTTVR\nTVNsSlZwRzRTbE9GV2FBakRGbVFURmcKjpGxx+RVZSo4IQZL1yZjBC4u2n9iKtqh\nuUv0UiHPt4u+rDxrdY7oOtk921EMTfrhc2h9JbSFMnl2eAeKgRsHVw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1a7a70qcpjemlvk6q4uaf4k77p9eq7lj7wcal5jdj3xuetznyqdrs3mfnsf" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYLysvZUZwQ1lRMGgvS2Zn\ncUIwRUU4SWNSWGI5c2FNMURocG9ZbVJvUGxVCjZkSFhuaElndW90dmlGT1BiKzJi\nZEljV1NMUTVDRmVVQ2FXMjlFWDNOQXMKLS0tIDA2RzVJckVxM3VJV0UrQS94d1FU\nOE9DRDBYelpQY1ZCVks4WjhobXlSa0UKInEmnwU/bokc3njKkQ0SNxfOnvDXMeCz\ne1tcmBXK2h7LIwoCoFMjgd1qG8JrgSaQdk/ReRZS9z0fC0cK2TB4AQ==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1vn8fpkmkzkjttcuc3prq3jrp7t5fsrdqey74ydu5p88keqmcupvs8jtmv8" + } + ], + "lastmodified": "2026-08-31T23:17:11Z", + "mac": "ENC[AES256_GCM,data:a/T5NopbVUpctCMzw50W2a6xleWgkyhIZ6pMXA5nk/+B82P7ayQT5kzZJpSGHgjc2OOmreHavSt6ALnSSJ2hWao9oEji9aeDKJBiU28iozI2bCEFJ825h32bZXv8wEQ8nMqE2affX+9MFhLZUfwvPRE/xeIPyHpdgKPrmrnIZwY=,iv:liPe+XEDXffVqDxGfZlPvRvEtDa16Po2psWFGya5mEo=,tag:yiYxIVfKBa5DVkn+/X/Jew==,type:str]", + "version": "3.13.3" + } +} diff --git a/vars/per-machine/magnetite/stibnite-nix-build/key/users/cameron b/vars/per-machine/magnetite/stibnite-nix-build/key/users/cameron new file mode 120000 index 000000000..015130152 --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-nix-build/key/users/cameron @@ -0,0 +1 @@ +../../../../../../sops/users/cameron \ No newline at end of file From ddb899e573bc0a44311455bb7794f704bdd8aa0c Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:47:49 -0400 Subject: [PATCH 04/13] chore(vars): add the generated stibnite-agent-session key record for magnetite The stibnite-agent-session generator produces the keypair magnetite uses for interactive ssh to stibnite as crs58. It is a separate generator from stibnite-nix-build so that either key can be revoked or rotated without disturbing the other. The private half is committed encrypted and the public half in clear, as with the build key. --- .../stibnite-agent-session/key.pub/value | 1 + .../key/machines/magnetite | 1 + .../stibnite-agent-session/key/secret | 18 ++++++++++++++++++ .../stibnite-agent-session/key/users/cameron | 1 + 4 files changed, 21 insertions(+) create mode 100644 vars/per-machine/magnetite/stibnite-agent-session/key.pub/value create mode 120000 vars/per-machine/magnetite/stibnite-agent-session/key/machines/magnetite create mode 100644 vars/per-machine/magnetite/stibnite-agent-session/key/secret create mode 120000 vars/per-machine/magnetite/stibnite-agent-session/key/users/cameron diff --git a/vars/per-machine/magnetite/stibnite-agent-session/key.pub/value b/vars/per-machine/magnetite/stibnite-agent-session/key.pub/value new file mode 100644 index 000000000..636532e33 --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-agent-session/key.pub/value @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZubXhStfGXDJuE1PXKai8YHGPN6XDFkErjyle9WjOy stibnite-agent-session diff --git a/vars/per-machine/magnetite/stibnite-agent-session/key/machines/magnetite b/vars/per-machine/magnetite/stibnite-agent-session/key/machines/magnetite new file mode 120000 index 000000000..41bd9646c --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-agent-session/key/machines/magnetite @@ -0,0 +1 @@ +../../../../../../sops/machines/magnetite \ No newline at end of file diff --git a/vars/per-machine/magnetite/stibnite-agent-session/key/secret b/vars/per-machine/magnetite/stibnite-agent-session/key/secret new file mode 100644 index 000000000..746ddde05 --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-agent-session/key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:mD2pXSyWuDLVvDz6LEJmYDYbdIgeWVNv47DA3/LeXzgT5bps8BhPNbzUERh6vzVWb1SpIjVdlzklnp303xQpIOrEf8MEaZ9dtPaBMH6Idu+Y38QGK97j0J1QTjReikqJ6p7952U1bUwsWuBkcgRduAEuOQ3Duqd5gNL8Bg8YcLFRg7msWp5gO+a9xIMAInXsCFqRPETDPfMcQVIBkBAhD6cY0k6T9ggRFiicPNjTYA6kvkdugEC8Rrtdwya9MBKqOxvrZxuyB/LyxNgGusKFf/UW/rB3mOdj0rdIxT4uT4Fv5wVyhN3wr36Geiwbd6/2sVLQPJiP46TdgbknZd8gs0BOmtviPBxnhRYgV4LeR7LAFKtKQrZLPqQMjIvpt/XUTygxJDskADhAGnDHpsEWyOqqQMLOE68KP1Hx8YAfGoebsSn9PM/tJ0MgYp8bOcdB5ddC2nzqBpS3yiHJp4+ScNSch3LynXO8uVfB3hWS69fMplITayd+a2QI1ERB+gVUk2lPesj1oey/6RcCnZHaro0ocFUzK5Yaab+BB11ffPWy4Yg=,iv:HcQ1nE9oyHiZEIFOwiQf8Ex4B6yUMX6zdMZ2TF/HEzA=,tag:VFhONshwQ00ibvEI218DFw==,type:str]", + "sops": { + "age": [ + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2TjNuVk1VMklmbU9TUkYv\nRFl6UkxGeFg3OEVKQXgwOC9SOVpDZllUdFhzClRsRGl6dHNybHF0dHJwbnBlYXVZ\nNjVRNUJyVWtmbTRZcWxWaFVuL21GbjgKLS0tIHQ0ZHJPcnd3bnVKbnBuUWZjL096\naklEYkdqdGZaeVZwSGhJYjQyWFdWeXMKF+mhdQxb9RIysw/YN5lSUr8Lc/gFHvLc\nhEBYWRlFnDL5AUDrFHwVKQqZ3F86jIA6niCOQqugWTPyuZOY6j7LVQ==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1a7a70qcpjemlvk6q4uaf4k77p9eq7lj7wcal5jdj3xuetznyqdrs3mfnsf" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFZXJLSjI1ekJXQTZaMXRH\nNHVjU2ZQSjJhcmorY3NjMUtUN1doZWNUZGowCjA0NnNjdHVOV1F5RUdJR09DS0dz\nSTVaUGtaZjlrRzloakZCT0Q1bWllV0kKLS0tIEIyTkNEaWZBOEtGN0Zpc0NKUXVE\nMEhsYmpwY04vTVRHenlNZ2J1S1I3UVEK31BLQJXTUE0WtQojy07KuLUa/dTFR3Zb\n0Jjm+CZnwRIytIsoROTpaOnL/FGbn6RazzhN508ZFEgU229YN0SdAg==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1vn8fpkmkzkjttcuc3prq3jrp7t5fsrdqey74ydu5p88keqmcupvs8jtmv8" + } + ], + "lastmodified": "2026-08-31T23:17:22Z", + "mac": "ENC[AES256_GCM,data:juMELRuv9Vl/kcfS4xYLFjGgLAPhTcKdUezcdwvs4X7rUqoNzra1q4N3nIh3cCStozh87adJPMrZ2QZXyJ9P/x7/dFiTDpSIruPg9NtPg39WYTh6PS/0+AZHqJQGew+5/v7lClgtHo7O/rUya07LWvqjbkVwFW8rrcEB6oRICII=,iv:2feuuTK6uzEIigpTN3usN2LvTsxnRKZAl/ZX426lq1E=,tag:xStuYm5s6W2cRwAxtyMrpA==,type:str]", + "version": "3.13.3" + } +} diff --git a/vars/per-machine/magnetite/stibnite-agent-session/key/users/cameron b/vars/per-machine/magnetite/stibnite-agent-session/key/users/cameron new file mode 120000 index 000000000..015130152 --- /dev/null +++ b/vars/per-machine/magnetite/stibnite-agent-session/key/users/cameron @@ -0,0 +1 @@ +../../../../../../sops/users/cameron \ No newline at end of file From 5f26a199ba333c2fdb2b0219ba7886288bc667fd Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:47:49 -0400 Subject: [PATCH 05/13] test(checks): assert the stibnite build-access wiring across both machines The wiring spans two machines, and neither machine's own evaluation can catch a mismatch between them. Swapping the two keys, dropping the forced command from the build key's authorized-keys entry, or letting magnetite's buildMachines entry and its /etc/nix/stibnite-store-uri name different accounts all evaluate and activate cleanly while being wrong. stibnite-access-wiring evaluates both configurations together and asserts the pairing: that magnetite's builder entry and store URI name the same account and ssh alias, that stibnite authorizes the build key with restrict and the forced nix-daemon --stdio command, and that the build account is a trusted user. The check is defined for both x86_64-linux and aarch64-darwin so either machine can run it. --- modules/checks/stibnite-access-wiring.nix | 115 ++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 modules/checks/stibnite-access-wiring.nix diff --git a/modules/checks/stibnite-access-wiring.nix b/modules/checks/stibnite-access-wiring.nix new file mode 100644 index 000000000..620e5d924 --- /dev/null +++ b/modules/checks/stibnite-access-wiring.nix @@ -0,0 +1,115 @@ +# Structural check for stibnite's aarch64-darwin build access. +# +# It guards the pairing between two machines' configurations, which no single +# machine's evaluation can catch. magnetite generates two keypairs and stibnite +# authorizes them under two different accounts; swapping them, or authorizing +# the build key without its forced command, produces a configuration that +# builds and activates and is wrong in the one way that matters — the build +# key would carry an interactive login, or the independently revocable +# identities would be assigned to the wrong accounts. +# +# It also pins the two mechanisms to one account: the nix.buildMachines entry +# (result copied back to the caller) and the remote-store URI (build stays in +# stibnite's store) must name the same user and the same ssh alias, or one of +# them silently reaches an account that authorizes nothing. +{ + self, + lib, + ... +}: +{ + perSystem = + { pkgs, system, ... }: + let + mkCheck = self.lib.mkStructuralCheck pkgs; + + magnetite = self.nixosConfigurations.magnetite.config; + stibnite = self.darwinConfigurations.stibnite.config; + + buildKey = + lib.removeSuffix "\n" + magnetite.clan.core.vars.generators.stibnite-nix-build.files."key.pub".value; + sessionKey = + lib.removeSuffix "\n" + magnetite.clan.core.vars.generators.stibnite-agent-session.files."key.pub".value; + + authorized = user: stibnite.environment.etc."ssh/nix_authorized_keys.d/${user}".text; + authorizedLines = user: builtins.filter (line: line != "") (lib.splitString "\n" (authorized user)); + + darwinEntries = builtins.filter ( + m: builtins.elem "aarch64-darwin" m.systems + ) magnetite.nix.buildMachines; + darwinEntry = builtins.head darwinEntries; + + # The forced command names an activation-time store path, so the oracle + # asserts its shape: the key options, and that the program is the + # nix-daemon of the nix package stibnite activates. + buildLine = builtins.head (authorizedLines "nixbuild"); + forcedCommand = ''restrict,command="${stibnite.nix.package}/bin/nix-daemon --stdio"''; + in + { + checks = + lib.optionalAttrs + (builtins.elem system [ + "x86_64-linux" + "aarch64-darwin" + ]) + { + stibnite-access-wiring = mkCheck { + name = "stibnite-access-wiring"; + actual = { + darwinBuilderCount = builtins.length darwinEntries; + darwinBuilder = { + inherit (darwinEntry) + hostName + sshUser + protocol + systems + maxJobs + supportedFeatures + ; + }; + + # Both mechanisms, one account and one alias. + storeUriNamesBuildAccount = + magnetite.environment.etc."nix/stibnite-store-uri".text + == "ssh-ng://${darwinEntry.sshUser}@${darwinEntry.hostName}?ssh-key=${darwinEntry.sshKey}\n"; + + # Key separation, asserted in both directions. + buildAccountAuthorizes = authorizedLines "nixbuild"; + sessionKeyIsNotABuildKey = !(lib.hasInfix sessionKey (authorized "nixbuild")); + buildKeyIsNotASessionKey = !(lib.hasInfix buildKey (authorized "crs58")); + sessionKeyAuthorizedForSessions = lib.hasInfix sessionKey (authorized "crs58"); + + # The build key carries a forced command and nothing else. + buildLineIsRestrictedToTheProtocol = buildLine == "${forcedCommand} ${buildKey}"; + + buildAccountIsTrusted = builtins.elem "nixbuild" stibnite.nix.settings.trusted-users; + buildAccountIsManaged = builtins.elem "nixbuild" stibnite.users.knownUsers; + }; + expected = { + darwinBuilderCount = 1; + darwinBuilder = { + hostName = "stibnite-builder"; + sshUser = "nixbuild"; + protocol = "ssh-ng"; + systems = [ "aarch64-darwin" ]; + maxJobs = 4; + supportedFeatures = [ + "apple-virt" + "big-parallel" + ]; + }; + storeUriNamesBuildAccount = true; + buildAccountAuthorizes = [ "${forcedCommand} ${buildKey}" ]; + sessionKeyIsNotABuildKey = true; + buildKeyIsNotASessionKey = true; + sessionKeyAuthorizedForSessions = true; + buildLineIsRestrictedToTheProtocol = true; + buildAccountIsTrusted = true; + buildAccountIsManaged = true; + }; + }; + }; + }; +} From c41729d1147f04ed13db682dbd3eaae7228af6c7 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:47:49 -0400 Subject: [PATCH 06/13] docs(concepts): document reaching stibnite's store as two mechanisms A remote builder and a remote store are routinely treated as one thing. They differ in where the output closure ends up, which is what decides between them, so the distinction is written down alongside the wiring rather than left to be rediscovered. A remote builder copies input closures out to stibnite, builds there, and copies the output closure back, so the result exists in the caller's store. Under nix build --store ssh-ng:// the remote store is the store: evaluation stays with the caller, the derivation and its source inputs are written to stibnite, and the output stays there. The page also records the authority the two keys actually carry, that CI cannot reach this builder because nixbot.toml sets attribute to checks.x86_64-linux, and the operator steps that activation still requires on each machine. --- .../docs/concepts/darwin-build-access.md | 94 +++++++++++++++++++ .../docs/src/content/docs/concepts/index.md | 5 + 2 files changed, 99 insertions(+) create mode 100644 packages/docs/src/content/docs/concepts/darwin-build-access.md diff --git a/packages/docs/src/content/docs/concepts/darwin-build-access.md b/packages/docs/src/content/docs/concepts/darwin-build-access.md new file mode 100644 index 000000000..32f32f0c9 --- /dev/null +++ b/packages/docs/src/content/docs/concepts/darwin-build-access.md @@ -0,0 +1,94 @@ +--- +title: Darwin build access +description: How the fleet reaches stibnite's aarch64-darwin store, and why the remote builder and the remote store are two mechanisms rather than one +sidebar: + order: 9 +--- + +Nix evaluation is portable and nix building is not. +Magnetite, which carries both CI services, is x86_64-linux, so it can evaluate an aarch64-darwin derivation and cannot build one. +Stibnite is the fleet's only aarch64-darwin machine, and until `modules/system/stibnite-access.nix` nothing declared it as a build target: the only `nix.buildMachines` entries anywhere named the rosetta VM and magnetite, both of them Linux. +Darwin derivations therefore had no build target at all, which is a different condition from having a slow one. + +## Two mechanisms, two callers + +The two ways to reach another machine's store are not interchangeable, and treating one as a synonym for the other is the mistake this page exists to prevent. + +A remote builder is `nix.buildMachines` on the caller, or `--builders` on one invocation. +The caller's nix daemon copies the input closure out to stibnite, builds there, and copies the output closure back, so the result exists in the *caller's* store. +That is what a caller wants when the verdict is wanted locally: a CI service that will sign the output and push it to the binary cache, or an operator on magnetite who feeds the result to a later local step. + +A remote store build is `--store ssh-ng://…`, or `--eval-store` for the mirror-image split. +Evaluation stays with the caller and the entire build happens inside stibnite's store: the derivation and its source inputs are copied to stibnite, while the output closure is not copied back. +That is what a caller wants when its own store is empty and stays empty — an ephemeral CI runner, a fresh container, an installer image — where a remote builder would spend the job populating a store that is about to be discarded. +Nothing lands locally, so a caller that needs the output path locally wants the builder instead. + +| | Remote builder | Remote store | +|---|---|---| +| Configured as | `services.stibnite-builder.buildMachines`, spliced into `nix.buildMachines` | `services.stibnite-builder.storeUri`, written to `/etc/nix/stibnite-store-uri` | +| Evaluation | caller | caller | +| Build | stibnite | stibnite | +| Output closure | copied back to the caller | stays in stibnite's store | +| Intended caller | a developer or operator who needs an aarch64-darwin result locally, such as building or testing Darwin configurations from magnetite | a machine whose store is empty and stays empty | + +Both mechanisms speak `ssh-ng` to the same account through the same ssh alias, which is what the `stibnite-access-wiring` check pins. +Legacy `ssh://`, which would run `nix-store --serve` on the far side, is deliberately not served. + +## What the builder entry claims, and why + +`systems` is `aarch64-darwin` alone. +`nix config show extra-platforms` on stibnite reports `aarch64-darwin` and nothing else, so advertising `x86_64-darwin` would route derivations the machine refuses to build. + +`maxJobs` is 4. +Stibnite has 18 logical cores, 12 performance and 6 efficiency, and 64 GiB of memory, and it is also a laptop in interactive use that commits 12 cores and 48 GiB to the rosetta builder VM and the same again to colima when either runs. +The remote share is deliberately a minority of the machine. + +`speedFactor` is 1 and is inert. +Nix compares speed factors only among machines that can build the same system, and there is no second aarch64-darwin machine to compare against. + +`supportedFeatures` is `apple-virt` and `big-parallel`. +Both are real on this hardware. +`benchmark` is dropped because timings taken on a laptop under interactive load are not measurements, and `nixos-test` is dropped because it is a Linux sandbox capability that nix lists unconditionally. + +## Two keys, two authorizations + +The build and session keys are separately authorized so they can be revoked or rotated independently. + +Magnetite generates both keypairs as `clan.core.vars` generators, `stibnite-nix-build` and `stibnite-agent-session`. +No plaintext private key material is committed. +Each private half is committed age/SOPS-encrypted under `vars/per-machine/magnetite/` and is decryptable only by magnetite and the authorized users recorded beside it. +The public halves are committed under the same path, and stibnite's configuration reads them at evaluation time. + +The build key is authorized on `nixbuild`, a non-admin account on stibnite that exists only to serve the build protocol. +Its authorized-keys entry carries `restrict` and a forced command of `nix-daemon --stdio`, which restricts the key to starting that program with no pty, forwarding, shell, or other command. +The forced command does not bound the nix daemon's authority. +`nixbuild` deliberately belongs to Nix's `trusted-users` because an untrusted account cannot receive unsigned store paths that a caller evaluated itself. +That membership is store-root-equivalent on stibnite: the account can cause arbitrary paths to enter the store and influence what the daemon trusts. + +The session key is the broader credential: it authorizes an ordinary login as `crs58`, an admin-group member who is already a Nix trusted user through `@admin`. +It therefore includes build authority plus shell access, while giving automated sessions an identity that can be revoked or rotated independently of the human's keys and the build key. + +The `stibnite-access-wiring` check asserts the separation in both directions: the session key does not appear in the build account's authorized keys, the build key does not appear in the session account's, and the build key's line is exactly its forced command plus the key. + +## What an operator must do + +Two steps are the operator's. + +Run `clan vars generate magnetite` when either keypair is rotated or first created, and commit the resulting encrypted private half and public value under `vars/per-machine/magnetite/`. +Stibnite's authorization reads those values at evaluation time, so an ungenerated key is an evaluation failure rather than a silent grant. + +Activate both ends: `clan machines update magnetite` for the builder entry, the ssh alias and the store URI, and `just activate` on stibnite for the account and its authorization. +Stibnite's activation also adds `nixbuild` to macOS's `com.apple.access_ssh` service ACL, which on this machine nests only the admin group; without that the build account is refused by sshd before the key is ever consulted, and the failure reads as `Permission denied (publickey)` with a correct key installed. +Creating a macOS account requires Full Disk Access when `darwin-rebuild` runs over ssh, so the first activation carrying this change should run in a graphical session on the machine. + +## What is deliberately not enabled + +`nixbot.toml` sets `attribute = "checks.x86_64-linux"`, which prevents CI from evaluating or requesting aarch64-darwin work and makes the builder unreachable from CI. +The configurations in `modules/nixos/nixbot.nix` and `modules/nixos/buildbot.nix` each set `buildSystems = [ "x86_64-linux" ]` as an independent second layer. +These controls remain because stibnite is a laptop without guaranteed availability and a sleeping machine could gate CI. + +## Related + +- [Build service topology](/concepts/build-service-topology/) — the two CI services kept separate from this development builder +- [Clan Integration](/concepts/clan-integration/) — how `clan machines update` deploys these ends +- [Secrets management](/guides/secrets-management/) — how `clan.core.vars` credentials reach a service diff --git a/packages/docs/src/content/docs/concepts/index.md b/packages/docs/src/content/docs/concepts/index.md index 162179457..fc5285c2c 100644 --- a/packages/docs/src/content/docs/concepts/index.md +++ b/packages/docs/src/content/docs/concepts/index.md @@ -20,3 +20,8 @@ Conceptual documentation to help you understand the architecture and design patt - [Deferred module composition](/concepts/deferred-module-composition/) - Understanding deferred module composition where every Nix file is a module organized by aspect - [Clan Integration](/concepts/clan-integration/) - Multi-machine coordination with clan and clear boundaries with other tools - [System-user integration](/concepts/system-user-integration/) - Understanding admin users with integrated home-manager vs non-admin standalone users + +## Build and CI + +- [Build service topology](/concepts/build-service-topology/) - Two CI build services coexisting on magnetite, and the boundaries that keep them apart +- [Darwin build access](/concepts/darwin-build-access/) - How the fleet reaches stibnite's aarch64-darwin store, and why the remote builder and the remote store are two mechanisms From ab6c91e880de2e58895f4710063e71cbb9468666 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 19:28:19 -0400 Subject: [PATCH 07/13] fix(deps): pin conventional-changelog preset to the v9 line The docs release path has failed at generateNotes on every main push with a release to make since the preset was bumped to ^10.4.0. Preset major 10 requires conventional-changelog-writer major 9 or newer, but semantic-release@25.0.9 resolves @semantic-release/release-notes-generator@14.1.0, which pins writer ^8.0.0 and therefore 8.2.0. The failure is not a version check but a planted handlebars template: @conventional-changelog/template@1.4.0 supplies a mainTemplate whose body is the error sentence itself, a writer-8 renderer compiles it and throws it as a Missing helper error, and writer 9 ignores the option entirely. Upgrading forward is unavailable. Every semantic-release channel, including 26.0.0-beta.1, depends on release-notes-generator ^14.1.0, and only 15.0.0-beta.2 accepts writer 9, which no semantic-release release references. A top-level override forcing writer 9 also fails, because the package manager nests writer 8 beneath release-notes-generator to satisfy its own range and the nested copy is the one that renders. Pinning to a lower v10 does not help either, since presets 10.0.0 through 10.3.0 all resolve @conventional-changelog/template to 1.4.0 today. 9.3.1 is the version upstream itself tests against writer 8, and unlike 10.4.0 it does not raise the node engine requirement to 22. Both manifests change together because the root declares workspaces: ["packages/*"] and bun.lock keys packages by name, so one version resolves for the whole workspace. The renovate rule is required rather than hygiene: .github/workflows/regenerate-lock-files.yaml re-runs the generator on Renovate pull requests and amends the branch, so without the hold the next Renovate run restores ^10.4.0 and re-materializes it into bun.nix automatically. That workflow is gated to renovate[bot], so bun.nix is regenerated here by hand. Verification. The repository's own check set does not cover this defect: the release path runs only as a post-build effect on main, and the package-vanixiets-docs* checks never invoke generateNotes. A green check set is therefore not evidence that this is fixed, so the primary evidence is a paired reproduction. generateNotes from release-notes-generator@14.1.0 was invoked with the repository's real pluginConfig against the repository's own bun-resolved tree over the real 9 commits since @vanixiets/docs-v0.6.0, holding writer at 8.2.0 and changing only the preset: 10.4.0 fails with the CI error verbatim, and 9.3.1 renders the 0.6.1 notes correctly. Checks run, selected as the narrowest set that would fail if this change were wrong: nix build --no-link --print-out-paths \ .#checks.aarch64-darwin.{treefmt,package-vanixiets-docs-deps,\ package-vanixiets-docs,package-vanixiets-docs-test-unit} All four pass. package-vanixiets-docs-deps is the derivation that materializes node_modules from bun.nix, so a stale or inconsistent lockfile fails there, and its log shows bun-pkg-conventional-changelog-conventionalcommits-9.3.1.drv; package-vanixiets-docs builds atop it; treefmt covers the changed nix and json; -test-unit confirms the retuned tree still supports the test runner. Deliberately left out: -test-e2e and -test-linkcheck, which exercise docs site content this diff does not touch, and the full check set, whose blast radius far exceeds a dependency pin. --- .github/renovate.json | 7 +++++++ bun.lock | 8 +++----- bun.nix | 10 +++------- package.json | 2 +- packages/docs/package.json | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 084b391bb..46a3e013d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -59,6 +59,13 @@ "@cloudflare/vite-plugin" ], "groupName": "cloudflare-workers-tooling" + }, + { + "description": "conventional-changelog-conventionalcommits@10 requires conventional-changelog-writer@9, which semantic-release@25's bundled @semantic-release/release-notes-generator@14 does not resolve; hold on 9.x until semantic-release ships a compatible generator", + "matchPackageNames": [ + "conventional-changelog-conventionalcommits" + ], + "allowedVersions": "<10" } ] } diff --git a/bun.lock b/bun.lock index 015add300..ebda34219 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@semantic-release/changelog": "^7.0.0", "@semantic-release/git": "^11.0.1", "@semantic-release/github": "^12.0.9", - "conventional-changelog-conventionalcommits": "^10.4.0", + "conventional-changelog-conventionalcommits": "^9.3.1", "semantic-release": "25.0.9", "semantic-release-major-tag": "^0.3.2", "semantic-release-monorepo": "^8.0.2", @@ -32,7 +32,7 @@ "@semantic-release/github": "^12.0.9", "@types/node": "^26.4.0", "@vitest/coverage-v8": "^4.1.11", - "conventional-changelog-conventionalcommits": "^10.4.0", + "conventional-changelog-conventionalcommits": "^9.3.1", "playwright": "1.61.1", "semantic-release": "25.0.9", "semantic-release-major-tag": "^0.3.2", @@ -171,8 +171,6 @@ "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], - "@conventional-changelog/template": ["@conventional-changelog/template@1.4.0", "", {}, "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg=="], - "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="], @@ -621,7 +619,7 @@ "conventional-changelog-angular": ["conventional-changelog-angular@8.1.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w=="], - "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@10.4.0", "", { "dependencies": { "@conventional-changelog/template": "^1.4.0" } }, "sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA=="], + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@9.3.1", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw=="], "conventional-changelog-writer": ["conventional-changelog-writer@8.2.0", "", { "dependencies": { "conventional-commits-filter": "^5.0.0", "handlebars": "^4.7.7", "meow": "^13.0.0", "semver": "^7.5.2" }, "bin": { "conventional-changelog-writer": "dist/cli/index.js" } }, "sha512-Y2aW4596l9AEvFJRwFGJGiQjt2sBYTjPD18DdvxX9Vpz0Z7HQ+g1Z+6iYDAm1vR3QOJrDBkRHixHK/+FhkR6Pw=="], diff --git a/bun.nix b/bun.nix index e293d5048..8356d4adf 100644 --- a/bun.nix +++ b/bun.nix @@ -305,10 +305,6 @@ url = "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz"; hash = "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="; }; - "@conventional-changelog/template@1.4.0" = fetchurl { - url = "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.4.0.tgz"; - hash = "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg=="; - }; "@cspotcode/source-map-support@0.8.1" = fetchurl { url = "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz"; hash = "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="; @@ -1562,9 +1558,9 @@ url = "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.1.0.tgz"; hash = "sha512-GGf2Nipn1RUCAktxuVauVr1e3r8QrLP/B0lEUsFktmGqc3ddbQkhoJZHJctVU829U1c6mTSWftrVOCHaL85Q3w=="; }; - "conventional-changelog-conventionalcommits@10.4.0" = fetchurl { - url = "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.4.0.tgz"; - hash = "sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA=="; + "conventional-changelog-conventionalcommits@9.3.1" = fetchurl { + url = "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz"; + hash = "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw=="; }; "conventional-changelog-writer@8.2.0" = fetchurl { url = "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.2.0.tgz"; diff --git a/package.json b/package.json index e9f4f0f6d..208263d49 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@semantic-release/changelog": "^7.0.0", "@semantic-release/git": "^11.0.1", "@semantic-release/github": "^12.0.9", - "conventional-changelog-conventionalcommits": "^10.4.0", + "conventional-changelog-conventionalcommits": "^9.3.1", "semantic-release": "25.0.9", "semantic-release-major-tag": "^0.3.2", "semantic-release-monorepo": "^8.0.2" diff --git a/packages/docs/package.json b/packages/docs/package.json index 010d3c6b8..de7572e13 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -52,7 +52,7 @@ "@semantic-release/github": "^12.0.9", "@types/node": "^26.4.0", "@vitest/coverage-v8": "^4.1.11", - "conventional-changelog-conventionalcommits": "^10.4.0", + "conventional-changelog-conventionalcommits": "^9.3.1", "playwright": "1.61.1", "semantic-release": "25.0.9", "semantic-release-major-tag": "^0.3.2", From 27ad231101bfdba4d7b11be7d1f71df71eabc6c7 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:13:55 -0400 Subject: [PATCH 08/13] fix(atomic): update to 0.9.17 Regenerate the npm source, dependency hashes, and distribution repair patch with the package-owned updater. Verification: nix build .#atomic; ./result/bin/atomic --version; nix eval --raw .#atomic.version; nix eval --raw .#apps..update-atomic.program --- pkgs/by-name/atomic/manifest.json | 2 +- pkgs/by-name/atomic/npm-dist-repairs.patch | 62 +++++++++++----------- pkgs/by-name/atomic/package.nix | 4 +- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/pkgs/by-name/atomic/manifest.json b/pkgs/by-name/atomic/manifest.json index 048bcb583..79a75ef0a 100644 --- a/pkgs/by-name/atomic/manifest.json +++ b/pkgs/by-name/atomic/manifest.json @@ -1,3 +1,3 @@ { - "version": "0.9.16" + "version": "0.9.17" } diff --git a/pkgs/by-name/atomic/npm-dist-repairs.patch b/pkgs/by-name/atomic/npm-dist-repairs.patch index 5373fa449..dacc7afff 100644 --- a/pkgs/by-name/atomic/npm-dist-repairs.patch +++ b/pkgs/by-name/atomic/npm-dist-repairs.patch @@ -2,81 +2,81 @@ +++ b/npm-shrinkwrap.json @@ -519,6 +519,7 @@ "node_modules/@bastani/atomic-natives": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.16.tgz", -+ "integrity": "sha512-bSVR578xAe0cOvNGie66xt5zRx282K/3E7OR8bioPJjnztmkdlCyPXfp9/tE/8gWiHtB+di1p43le6mzVBdNWg==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.17.tgz", ++ "integrity": "sha512-EDRo08i9yeom/fw9NuhuFXbtGgMbvLZ8aXRUOm2intLkqGemRwQvz2w4YlC/6OtWbLaCtyRLGQSDDJGhECjXYA==", "license": "MIT", "optionalDependencies": { - "@bastani/atomic-natives-darwin-arm64": "0.9.16", + "@bastani/atomic-natives-darwin-arm64": "0.9.17", @@ -538,6 +539,7 @@ "node_modules/@bastani/atomic-natives-darwin-arm64": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.16.tgz", -+ "integrity": "sha512-r/ZYrD/+2UULEOoQWXCSPkYUOosC3rmoMsWgNaZoPN63324B8kmvTYDAe4OzdF8+dHKd3/Ta5aLdp/Q1z/gOSA==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.17.tgz", ++ "integrity": "sha512-qyiolVjBmeZO1irLA/DzcqWPl0w2uk0qMYS1fFY+CnOW2XppoRzu0gLcMRgBm1EQDVFAMpeBAbj8YR/WvJMgZg==", "license": "MIT", "os": [ "darwin" @@ -550,6 +552,7 @@ "node_modules/@bastani/atomic-natives-darwin-x64": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.16.tgz", -+ "integrity": "sha512-2/ISDvIBSjRdlYIU880A8OGF9BEemsf8+qp+IG2LtmELku1FPQd8JUvjlW6QQF5RMc3cd4o2vbUpV91FgGhWuQ==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.17.tgz", ++ "integrity": "sha512-ogZ2uXClJ7C3KfGJz/3sAdw7uxbCKUtV/atlGqpvfBwo3aGALqd6qRTvWRrPH1mdrthSITXdtkWnxjq98kmBvw==", "license": "MIT", "os": [ "darwin" @@ -562,6 +565,7 @@ "node_modules/@bastani/atomic-natives-linux-arm64-gnu": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.16.tgz", -+ "integrity": "sha512-pq8ci66yTnzhtrg6P54KJj6hJoKlaYPcbd0U7TmKHazWSxJqoh+IU/JJXb2k25r1G1jIfMbGZEhe16sSqNHAsQ==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.17.tgz", ++ "integrity": "sha512-6mOEF81kfpapQEOJKLpQYOHSop9fffOa51C27rglkmF0cS1ZpJcaFjOlf+0QVG965Dw5hWSCw0yVyTykVFs++Q==", "license": "MIT", "os": [ "linux" @@ -577,6 +581,7 @@ "node_modules/@bastani/atomic-natives-linux-arm64-musl": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.16.tgz", -+ "integrity": "sha512-jSq26oc+EiQhp6jUkVY9iASF1nHYf6cYLJGNfj00G3jt437slfa0IKmSKK2fU2+DuiySRL7fNxmlrvtiLDCKMg==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.17.tgz", ++ "integrity": "sha512-F+gCuJ9cq+ARXCd/FZBq6PnosxbPVIhWDo/XbKZ36NOK3v/yEfxellTl13zKzOqioXf+j7t6XmGdN2PWaByO1g==", "license": "MIT", "os": [ "linux" @@ -592,6 +597,7 @@ "node_modules/@bastani/atomic-natives-linux-x64-gnu": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.16.tgz", -+ "integrity": "sha512-ts/9jsfV1QdxfNyewcbFBPzo/3LeO9n1Ol+rz3+SumdWFvlYx3v37QbBm2hWJsAXQWYpHsbbCZAtNqvhIhfexw==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.17.tgz", ++ "integrity": "sha512-439kGppu3dDiY4wAe7wwSrNYryPzMEpxGKi3Uyir0xxQpVGnylVo/zdLTJWdHFufGLYtq+ONOSUnDUkG/XV+0Q==", "license": "MIT", "os": [ "linux" @@ -607,6 +613,7 @@ "node_modules/@bastani/atomic-natives-linux-x64-musl": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.16.tgz", -+ "integrity": "sha512-G2bsvsBlJK2Jf/5O8cOQwjb/JlPHXCsKGNFjmHOz8f1zigogUad3zi0do8vy5pHF27QBeamYeQ0w1WhCohJtZA==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.17.tgz", ++ "integrity": "sha512-nXJwdxfVXcBALNfwtzzWVh12i1bsqTBckzVrXwExL55Ki2kkqTZPTR6BqF1hGNhn+yWWG6JxuXCZ/uUivVjlNw==", "license": "MIT", "os": [ "linux" @@ -622,6 +629,7 @@ "node_modules/@bastani/atomic-natives-win32-arm64-msvc": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.16.tgz", -+ "integrity": "sha512-ELMYn1UhF9rLRIYq0Veyy1MOubPG/otJVAIPBGO75a+z3hcYg1De1tmQ29Z/pHudEaOYKgA098H6kCn1UQZZ+A==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.17.tgz", ++ "integrity": "sha512-jOChbAvteBkAwoSeGysjHDUeJwDOF3Io5W7+F3mFpCq6bFUdH7/y1tTQ3wSlvOmpsOi+sI7iMaIgTBQF119biw==", "license": "MIT", "os": [ "win32" @@ -634,6 +642,7 @@ "node_modules/@bastani/atomic-natives-win32-x64-msvc": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.16.tgz", -+ "integrity": "sha512-BSYQR7JP3sy/hh1kRARERUmVEof/Ke6hHIFJtRWYydSmLprQg5QWTVRhlamTZLRBUk8GZfd0SsXg0UqsxLhZ8Q==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.17.tgz", ++ "integrity": "sha512-LjINvOARzh3ceDr9gKcN+bJysz4tlhN9YlDNKJn4ZmDjnJcW6ml2Tjnn6O3qqAxVNFsELrE1rtkQgN0WA5Z54g==", "license": "MIT", "os": [ "win32" @@ -646,6 +655,7 @@ "node_modules/@bastani/pi-ai": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.16.tgz", -+ "integrity": "sha512-HhbK3GzZs+71exTD76oT/Xi+Y98k0LsAYVIJe63ORDP7Wx9lI5LfEWk1PN086inSyxpOpFc1tJN0IsSEUcERng==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.17.tgz", ++ "integrity": "sha512-q9mt/scYxl6AwbZ2fX5KHWZa20XCKW1+t827iqtXq2sWZjI7vyBwo6mPFB42OlD3iztzUY0SvV6Zos9p2tJMXw==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", diff --git a/pkgs/by-name/atomic/package.nix b/pkgs/by-name/atomic/package.nix index 630a35027..74d8a021c 100644 --- a/pkgs/by-name/atomic/package.nix +++ b/pkgs/by-name/atomic/package.nix @@ -60,12 +60,12 @@ buildNpmPackage (finalAttrs: { src = fetchurl { url = "https://registry.npmjs.org/@bastani/atomic/-/atomic-${finalAttrs.version}.tgz"; - hash = "sha256-eAgebHtzXCRvcMvEFzB/yDmf3HL5sksW/39iG/UhQBY="; + hash = "sha256-iuf3fE26q9gTPlz/Xc4PLx4zkI2Is9poWDNWNbQZtRk="; }; sourceRoot = "package"; nodejs = nodejs_22; - npmDepsHash = "sha256-1UIAt5YDuL86AnAc38fwKsPDeuCIOBbd8oOec+FMLuM="; + npmDepsHash = "sha256-WuzvuvsJDb0QWqPLbmTHUM3yqEPVT5EL/LaRRDMjRYQ="; postPatch = '' patch -p1 < ${./npm-dist-repairs.patch} From 106f539447ba48f4855c224198adccf719ac14fc Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Mon, 31 Aug 2026 23:24:48 -0400 Subject: [PATCH 09/13] feat(omp): set direnv preflight budget for agent bash commands omp folds a clone's direnv environment into the bash commands it runs, which is what lets a devshell-provided tool resolve without wrapping the call in `nix develop` or `direnv exec`. The mechanism is on by default; what this sets is the budget it runs under. `bash.direnvLoadTimeoutMs` goes from the upstream 30000 to 180000. The first export in a clone has to realise the flake devshell, measured at 7.4 seconds in this repository against an already-populated nix store and running to minutes when the closure has to be built, while later exports served from the nix-direnv cache return in under 10 milliseconds. The higher ceiling cannot stall a short command, because the effective budget is the smaller of this value and the command's own deadline. Overrunning it runs the command without the direnv environment rather than failing, so a tight value loses devshell tools silently. `bash.direnv` is declared at the value it already defaults to, so that a change to that default appears as a diff here rather than as devshell tools disappearing from every bash command. A clone whose .envrc has not been through `direnv allow` still runs without the devshell. That gate belongs to direnv's own configuration and is untouched here. --- modules/home/ai/omp/default.nix | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/home/ai/omp/default.nix b/modules/home/ai/omp/default.nix index 9dd8ede9e..7ece514ad 100644 --- a/modules/home/ai/omp/default.nix +++ b/modules/home/ai/omp/default.nix @@ -121,6 +121,18 @@ # aiAgentSettings.theme carries, which pi and atomic resolve against a # vendored theme file; the two are not interchangeable strings. theme.dark = lib.mkDefault "dark-catppuccin"; + + # 180000 rather than upstream's 30000: a clone's first export has to + # realise the flake devshell, 7.4s here against a populated nix store + # and minutes when the closure must be built, and overrunning the + # budget runs the command without the devshell rather than failing. + # direnv is pinned at its own default so a change to that default + # shows up as a diff here. + bash = { + direnv = lib.mkDefault "auto"; + + direnvLoadTimeoutMs = lib.mkDefault 180000; + }; modelRoles = { default = lib.mkDefault "anthropic/claude-opus-5:xhigh"; advisor = lib.mkDefault "openai-codex/gpt-5.6-sol:xhigh"; From 7f91bafb3579ef00484d0961e7baa23507b1c337 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Tue, 1 Sep 2026 00:38:49 -0400 Subject: [PATCH 10/13] fix(updaters): resolve package paths from repository root --- pkgs/by-name/chrome-devtools-axi/update.sh | 11 ++----- pkgs/by-name/dbt-fusion/update.sh | 8 ++--- pkgs/by-name/gh-axi/update.sh | 11 ++----- pkgs/by-name/hindsight/update.sh | 9 ++---- pkgs/by-name/lavish-axi/update.sh | 11 ++----- pkgs/by-name/linear-cli/update.sh | 37 ++++++++++++++++++++-- pkgs/by-name/quota-axi/update.sh | 11 ++----- pkgs/by-name/tasks-axi/update.sh | 11 ++----- pkgs/by-name/uncomment-bin/update.sh | 9 ++---- pkgs/by-name/worktrunk-bin/update.sh | 9 ++---- 10 files changed, 57 insertions(+), 70 deletions(-) diff --git a/pkgs/by-name/chrome-devtools-axi/update.sh b/pkgs/by-name/chrome-devtools-axi/update.sh index 02127ec3a..f06ffd8c8 100755 --- a/pkgs/by-name/chrome-devtools-axi/update.sh +++ b/pkgs/by-name/chrome-devtools-axi/update.sh @@ -2,18 +2,11 @@ #!nix-shell -i bash -p curl jq cacert git nodejs_22 nix # shellcheck shell=bash -# Bumps an axi-family package to the current npm registry latest. The firstmate -# bootstrap tracks these tools at latest and raises its version floors as they -# publish, so a stale pin eventually reports MISSING rather than out-of-date. -# -# This script is byte-identical across the axi-family package directories; the -# npm package name is taken from the directory it lives in. - set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" -PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PKG="$(basename "$PKG_DIR")" +PKG_DIR="${REPO_ROOT}/pkgs/by-name/chrome-devtools-axi" +PKG="${PKG_DIR##*/}" PKG_NIX="${PKG_DIR}/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/dbt-fusion/update.sh b/pkgs/by-name/dbt-fusion/update.sh index 215f96269..6742ef0a6 100755 --- a/pkgs/by-name/dbt-fusion/update.sh +++ b/pkgs/by-name/dbt-fusion/update.sh @@ -1,12 +1,12 @@ #!/usr/bin/env nix-shell -#!nix-shell -i bash -p curl gnused jq nix-prefetch +#!nix-shell -i bash -p curl git gnused jq nix-prefetch set -euo pipefail -ROOT="$(dirname "$(readlink -f "$0")")" -NIX_DRV="$ROOT/package.nix" +REPO_ROOT="$(git rev-parse --show-toplevel)" +NIX_DRV="${REPO_ROOT}/pkgs/by-name/dbt-fusion/package.nix" if [ ! -f "$NIX_DRV" ]; then - echo "ERROR: cannot find package.nix in $ROOT" + echo "ERROR: cannot find $NIX_DRV" exit 1 fi diff --git a/pkgs/by-name/gh-axi/update.sh b/pkgs/by-name/gh-axi/update.sh index 02127ec3a..da79ba0da 100755 --- a/pkgs/by-name/gh-axi/update.sh +++ b/pkgs/by-name/gh-axi/update.sh @@ -2,18 +2,11 @@ #!nix-shell -i bash -p curl jq cacert git nodejs_22 nix # shellcheck shell=bash -# Bumps an axi-family package to the current npm registry latest. The firstmate -# bootstrap tracks these tools at latest and raises its version floors as they -# publish, so a stale pin eventually reports MISSING rather than out-of-date. -# -# This script is byte-identical across the axi-family package directories; the -# npm package name is taken from the directory it lives in. - set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" -PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PKG="$(basename "$PKG_DIR")" +PKG_DIR="${REPO_ROOT}/pkgs/by-name/gh-axi" +PKG="${PKG_DIR##*/}" PKG_NIX="${PKG_DIR}/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/hindsight/update.sh b/pkgs/by-name/hindsight/update.sh index b7fc1be4b..55f94d7b9 100755 --- a/pkgs/by-name/hindsight/update.sh +++ b/pkgs/by-name/hindsight/update.sh @@ -1,5 +1,5 @@ #!/usr/bin/env nix-shell -#!nix-shell --pure -i bash -p curl jq cacert nix +#!nix-shell --pure -i bash -p curl jq cacert git nix # shellcheck shell=bash # # Bumps pkgs/by-name/hindsight to the latest upstream release: rewrites the @@ -8,11 +8,8 @@ set -euo pipefail -# Resolve package.nix relative to this script so the updater edits the correct -# file regardless of the caller's working directory (e.g. nixpkgs update -# machinery, which does not cd into the package directory). -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -PKG_NIX="${SCRIPT_DIR}/package.nix" +REPO_ROOT="$(git rev-parse --show-toplevel)" +PKG_NIX="${REPO_ROOT}/pkgs/by-name/hindsight/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/lavish-axi/update.sh b/pkgs/by-name/lavish-axi/update.sh index 02127ec3a..b4d1bad73 100755 --- a/pkgs/by-name/lavish-axi/update.sh +++ b/pkgs/by-name/lavish-axi/update.sh @@ -2,18 +2,11 @@ #!nix-shell -i bash -p curl jq cacert git nodejs_22 nix # shellcheck shell=bash -# Bumps an axi-family package to the current npm registry latest. The firstmate -# bootstrap tracks these tools at latest and raises its version floors as they -# publish, so a stale pin eventually reports MISSING rather than out-of-date. -# -# This script is byte-identical across the axi-family package directories; the -# npm package name is taken from the directory it lives in. - set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" -PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PKG="$(basename "$PKG_DIR")" +PKG_DIR="${REPO_ROOT}/pkgs/by-name/lavish-axi" +PKG="${PKG_DIR##*/}" PKG_NIX="${PKG_DIR}/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/linear-cli/update.sh b/pkgs/by-name/linear-cli/update.sh index 89c373435..65fe95ad5 100755 --- a/pkgs/by-name/linear-cli/update.sh +++ b/pkgs/by-name/linear-cli/update.sh @@ -1,5 +1,5 @@ #!/usr/bin/env nix-shell -#!nix-shell --pure -i bash -p curl jq cacert nix gnused coreutils gnugrep +#!nix-shell --pure -i bash -p curl jq cacert git nix gnused coreutils gnugrep # shellcheck shell=bash # # Update linear-cli (schpet/linear-cli) to the latest release. @@ -10,14 +10,15 @@ # paired to each preceding `url` line) # 3. the `src` fetchFromGitHub `hash = "...";` line (the hash following # the `rev = "v$VERSION";` line) +# 4. the Linux source build's `denoDeps.outputHash` # # Usage: ./update.sh [VERSION] (VERSION overrides the latest-release lookup) set -euo pipefail REPO="schpet/linear-cli" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PKG_NIX="${SCRIPT_DIR}/package.nix" +REPO_ROOT="$(git rev-parse --show-toplevel)" +PKG_NIX="${REPO_ROOT}/pkgs/by-name/linear-cli/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" @@ -86,4 +87,34 @@ fi sed -i'' -e "/rev = \"v\${version}\"/{ n; s|hash = \"sha256-[^\"]*\"|hash = \"${src_sri}\"|; }" "$PKG_NIX" echo " src: ${src_sri}" +echo "Prefetching x86_64-linux Deno dependencies..." +fake_hash="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +sed -i'' -e "s|outputHash = \"sha256-[^\"]*\";|outputHash = \"${fake_hash}\";|" "$PKG_NIX" + +if ! grep -qF "outputHash = \"${fake_hash}\";" "$PKG_NIX"; then + echo "error: failed to replace denoDeps.outputHash with the probe hash" >&2 + exit 1 +fi + +deno_deps_log="$(mktemp)" +trap 'rm -f "$deno_deps_log"' EXIT + +if nix build "${REPO_ROOT}#packages.x86_64-linux.linear-cli.denoDeps" \ + --system x86_64-linux \ + --no-link \ + -L 2>&1 | tee "$deno_deps_log"; then + echo "error: denoDeps unexpectedly matched the probe hash" >&2 + exit 1 +fi + +deno_deps_hash="$(sed -n 's/^[[:space:]]*got:[[:space:]]*\(sha256-[^[:space:]]*\)$/\1/p' "$deno_deps_log" | tail -1)" + +if [[ -z "$deno_deps_hash" ]]; then + echo "error: x86_64-linux denoDeps build failed without producing a fixed-output hash" >&2 + exit 1 +fi + +sed -i'' -e "s|outputHash = \"${fake_hash}\";|outputHash = \"${deno_deps_hash}\";|" "$PKG_NIX" +echo " denoDeps (x86_64-linux): ${deno_deps_hash}" + echo "Updated linear-cli to ${latest_version}" diff --git a/pkgs/by-name/quota-axi/update.sh b/pkgs/by-name/quota-axi/update.sh index 02127ec3a..10be61e4f 100755 --- a/pkgs/by-name/quota-axi/update.sh +++ b/pkgs/by-name/quota-axi/update.sh @@ -2,18 +2,11 @@ #!nix-shell -i bash -p curl jq cacert git nodejs_22 nix # shellcheck shell=bash -# Bumps an axi-family package to the current npm registry latest. The firstmate -# bootstrap tracks these tools at latest and raises its version floors as they -# publish, so a stale pin eventually reports MISSING rather than out-of-date. -# -# This script is byte-identical across the axi-family package directories; the -# npm package name is taken from the directory it lives in. - set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" -PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PKG="$(basename "$PKG_DIR")" +PKG_DIR="${REPO_ROOT}/pkgs/by-name/quota-axi" +PKG="${PKG_DIR##*/}" PKG_NIX="${PKG_DIR}/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/tasks-axi/update.sh b/pkgs/by-name/tasks-axi/update.sh index 02127ec3a..d47ebe6df 100755 --- a/pkgs/by-name/tasks-axi/update.sh +++ b/pkgs/by-name/tasks-axi/update.sh @@ -2,18 +2,11 @@ #!nix-shell -i bash -p curl jq cacert git nodejs_22 nix # shellcheck shell=bash -# Bumps an axi-family package to the current npm registry latest. The firstmate -# bootstrap tracks these tools at latest and raises its version floors as they -# publish, so a stale pin eventually reports MISSING rather than out-of-date. -# -# This script is byte-identical across the axi-family package directories; the -# npm package name is taken from the directory it lives in. - set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel)" -PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PKG="$(basename "$PKG_DIR")" +PKG_DIR="${REPO_ROOT}/pkgs/by-name/tasks-axi" +PKG="${PKG_DIR##*/}" PKG_NIX="${PKG_DIR}/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/uncomment-bin/update.sh b/pkgs/by-name/uncomment-bin/update.sh index aa11507f9..19f9d93ae 100755 --- a/pkgs/by-name/uncomment-bin/update.sh +++ b/pkgs/by-name/uncomment-bin/update.sh @@ -1,14 +1,11 @@ #!/usr/bin/env nix-shell -#!nix-shell --pure -i bash -p curl jq cacert nix +#!nix-shell --pure -i bash -p curl jq cacert git nix # shellcheck shell=bash set -euo pipefail -# Resolve package.nix relative to this script so the updater edits the correct -# file regardless of the caller's working directory (e.g. nixpkgs update -# machinery, which does not cd into the package directory). -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -PKG_NIX="${SCRIPT_DIR}/package.nix" +REPO_ROOT="$(git rev-parse --show-toplevel)" +PKG_NIX="${REPO_ROOT}/pkgs/by-name/uncomment-bin/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" diff --git a/pkgs/by-name/worktrunk-bin/update.sh b/pkgs/by-name/worktrunk-bin/update.sh index 66be3169a..e06cf8fb5 100755 --- a/pkgs/by-name/worktrunk-bin/update.sh +++ b/pkgs/by-name/worktrunk-bin/update.sh @@ -1,14 +1,11 @@ #!/usr/bin/env nix-shell -#!nix-shell --pure -i bash -p curl jq cacert nix +#!nix-shell --pure -i bash -p curl jq cacert git nix # shellcheck shell=bash set -euo pipefail -# Resolve package.nix relative to this script so the updater edits the correct -# file regardless of the caller's working directory (e.g. nixpkgs update -# machinery, which does not cd into the package directory). -SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" -PKG_NIX="${SCRIPT_DIR}/package.nix" +REPO_ROOT="$(git rev-parse --show-toplevel)" +PKG_NIX="${REPO_ROOT}/pkgs/by-name/worktrunk-bin/package.nix" current_version="$(sed -n 's/.*version = "\(.*\)";/\1/p' "$PKG_NIX" | head -1)" From 30078c37c71f95681629a6025154162b7bd0f59b Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Tue, 1 Sep 2026 00:38:49 -0400 Subject: [PATCH 11/13] test(updaters): enforce complete repository-relative updates --- modules/checks/hooks.nix | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/modules/checks/hooks.nix b/modules/checks/hooks.nix index 2cc950eae..3779cb60a 100644 --- a/modules/checks/hooks.nix +++ b/modules/checks/hooks.nix @@ -146,6 +146,52 @@ exit 1 fi + touch $out + ''; + + checks.updater-repository-paths = + pkgs.runCommand "updater-repository-paths" + { + nativeBuildInputs = [ pkgs.ripgrep ]; + updaters = ../../pkgs/by-name; + } + '' + set -uo pipefail + failures=0 + identity_pattern='BASH_SOURCE|[$][{]?0([^0-9]|$)' + + for updater in "$updaters"/*/update.sh; do + [ -f "$updater" ] || continue + package="''${updater%/update.sh}/package.nix" + + if matches=$(rg -n "$identity_pattern" "$updater"); then + relative_path="''${updater#"$updaters"/}" + echo "FAIL: $relative_path derives package paths from its copied updater identity:" >&2 + echo "$matches" >&2 + failures=$((failures + 1)) + fi + + if rg -q 'git rev-parse --show-toplevel' "$updater" \ + && ! sed -n '1,2p' "$updater" | rg -q '#!nix-shell.*[[:space:]]git([[:space:]]|$)'; then + relative_path="''${updater#"$updaters"/}" + echo "FAIL: $relative_path resolves the repository with git but does not declare git in its nix-shell packages." >&2 + failures=$((failures + 1)) + fi + + if [ -f "$package" ] \ + && rg -q 'outputHash[[:space:]]*=' "$package" \ + && ! rg -q 'outputHash' "$updater"; then + relative_path="''${updater#"$updaters"/}" + echo "FAIL: $relative_path does not refresh its package's fixed-output hash." >&2 + failures=$((failures + 1)) + fi + done + + if [ "$failures" -ne 0 ]; then + echo "Updater scripts must resolve the repository root with 'git rev-parse --show-toplevel', then address pkgs/by-name/ under that root." >&2 + exit 1 + fi + touch $out ''; }; From 9518598116f630d201cd0715329dcfff359c4c7f Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Tue, 1 Sep 2026 00:38:49 -0400 Subject: [PATCH 12/13] fix(linear-cli): remove store references from vendored dependencies --- pkgs/by-name/linear-cli/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/by-name/linear-cli/package.nix b/pkgs/by-name/linear-cli/package.nix index fec7ded9e..4316fff7a 100644 --- a/pkgs/by-name/linear-cli/package.nix +++ b/pkgs/by-name/linear-cli/package.nix @@ -87,7 +87,7 @@ let # regenerable artifacts: remote/ (now vendored), analysis & v8 caches, the # path-keyed gen/ tree, scripts-warned nonces, and npm registry metadata. ( cd "$DENO_DIR" - rm -rf remote gen + rm -rf remote gen node_compat_bin find . -name '*_analysis_cache_v2*' -delete find . -name 'v8_code_cache_v2*' -delete find . -name '.scripts-warned-*' -delete From bd9a7eaf0a7088c4dfe8da9943e8fca4a1c62410 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Tue, 1 Sep 2026 00:38:49 -0400 Subject: [PATCH 13/13] fix(linear-cli): update to 2.5.0 --- pkgs/by-name/linear-cli/package.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkgs/by-name/linear-cli/package.nix b/pkgs/by-name/linear-cli/package.nix index 4316fff7a..f0c908665 100644 --- a/pkgs/by-name/linear-cli/package.nix +++ b/pkgs/by-name/linear-cli/package.nix @@ -12,18 +12,18 @@ }: let - version = "2.0.0"; + version = "2.5.0"; # Prebuilt deno-compile binaries published per tag by cargo-dist. Only the # Darwin artifacts are usable (see the Linux note below). binaries = { aarch64-darwin = { url = "https://github.com/schpet/linear-cli/releases/download/v${version}/linear-aarch64-apple-darwin.tar.xz"; - hash = "sha256-Eh/h7ubZCyLnbk6Yy7YkR07s2XCkpMYi/U1QiJtX2sw="; + hash = "sha256-bRHrWg7iqhDSRiXSPGBb9WmHqUdB5gu5gX+HsSOpoAs="; }; x86_64-darwin = { url = "https://github.com/schpet/linear-cli/releases/download/v${version}/linear-x86_64-apple-darwin.tar.xz"; - hash = "sha256-cp5nFmxQlMiVFQtnLNOkRh+omYl+HyTbzQfBO7O0jBM="; + hash = "sha256-9Xx6zJdMG8AcCsFBZYov04DjZ3a4XxXan8vy6CTAgjw="; }; }; @@ -35,7 +35,7 @@ let owner = "schpet"; repo = "linear-cli"; rev = "v${version}"; - hash = "sha256-FR6WuTKws75i0T00ASxr6wTHYH8MNOdboJcDYD0aYVM="; + hash = "sha256-iNAJ0vaEMkPkYq0I9SLEl3GJY/B0MazOgAm3PgkIb/o="; }; # Darwin: the upstream prebuilt deno-compile single-file executable. @@ -112,7 +112,7 @@ let outputHashMode = "recursive"; outputHashAlgo = "sha256"; - outputHash = "sha256-r5480RsOe3Su7opj5V76nBgm3cVmlzckNAUXRayHLMg="; + outputHash = "sha256-eYm8+y/l4X2NFLIjny1r86Wf88gXq+m4TN53DI9fuiQ="; }; in stdenv.mkDerivation (finalAttrs: {