From 305a0e61f839de12f5aebaee6e1469e138c86fd7 Mon Sep 17 00:00:00 2001 From: "Simen A. W. Olsen" Date: Tue, 27 Oct 2026 11:50:21 +0100 Subject: feat: gate updates behind vet supply-chain checks update now holds inputs and skills back by a cooldown, has Claude review third-party diffs, and reports CVE changes before switching. Homebrew taps are pinned through nix-homebrew, and npm/pnpm get install policies. --- INSTALL.md | 44 +++- README.md | 6 +- flake.lock | 122 ++++++++++- flake.nix | 48 +++++ modules/darwin/default.nix | 4 + modules/darwin/homebrew.nix | 11 +- modules/home/default.nix | 1 + modules/home/supply-chain.nix | 26 +++ modules/home/zsh.nix | 28 ++- pkgs/vet/default.nix | 30 +++ pkgs/vet/prompt.md | 45 +++++ pkgs/vet/vet.sh | 460 ++++++++++++++++++++++++++++++++++++++++++ vet.json | 15 ++ 13 files changed, 807 insertions(+), 33 deletions(-) create mode 100644 modules/home/supply-chain.nix create mode 100644 pkgs/vet/default.nix create mode 100644 pkgs/vet/prompt.md create mode 100644 pkgs/vet/vet.sh create mode 100644 vet.json diff --git a/INSTALL.md b/INSTALL.md index 1938748..83ec0e0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -34,15 +34,11 @@ Installing plain upstream Nix here leaves the daemon unmanaged. Open a new shell afterwards so `nix` is on `PATH`. -### 3. Install Homebrew +### 3. Homebrew -nix-darwin declares Homebrew packages but does **not** install Homebrew itself. -The first switch fails without it. - -```shell -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -eval "$(/opt/homebrew/bin/brew shellenv)" -``` +Nothing to do: nix-homebrew installs Homebrew during the first switch, and its +taps are pinned in `flake.lock` like every other input. Brew installs from those +pinned taps, not from its JSON API. ### 4. Clone the repository @@ -230,15 +226,41 @@ Aliases defined in `modules/home/zsh.nix` (macOS): | Alias | Does | | --------- | ------------------------------------------------- | | `rebuild` | `darwin-rebuild switch` against this flake | -| `update` | `nix flake update`, then rebuild | +| `update` | Vetted input update, build, diff, switch, commit | | `nix-gc` | Garbage-collect store paths older than 14 days | `scripts/cleanup.sh` is a separate macOS cleanup utility (caches, logs, trash). +### Updating + +`update` runs `vet` (`pkgs/vet`) before anything is switched: + +1. `vet lock` moves flake inputs and skills to the newest revisions at least + `cooldownDays` (`vet.json`) old. nixpkgs moves to a channel release, so it + stays in the binary cache. Own sources go to their newest revision, and + inputs pinned to a commit in `flake.nix` are left alone. +2. `vet check` compares the locks with `HEAD`. Changes inside the cooldown are + blocked. Third-party sources (anything not listed as own or trusted in + `vet.json`) have their diff reviewed by Claude, which runs with no tools. + A `review` verdict asks before continuing; a `block` stops the update. +3. The new system is built and compared with `nvd diff`, and `vet cves` lists + the CVEs it adds and fixes (vulnix against the NVD; the first run downloads + the whole NVD and takes a while). Add known false positives to `vulnix.toml`. +4. After you confirm, it switches and commits both locks with the vet report. + +Set `VET_COOLDOWN_DAYS=0` to let an urgent fix through. Run `vet check` by hand +after adding a new input or skill source to have it reviewed before committing. + +npm and pnpm get matching defaults in `modules/home/supply-chain.nix`: a +seven-day minimum release age, no trust downgrades and no exotic transitive +dependencies for pnpm, and no install scripts for npm. Scan a project's +lockfiles for known vulnerabilities with `osv-scanner scan -r .`. + ## Troubleshooting -**`error: Homebrew is not installed`** — step 3 was skipped, or `brew` is not on -`PATH` for the shell running the switch. Run `eval "$(/opt/homebrew/bin/brew shellenv)"`. +**`An existing /opt/homebrew/Library/Taps is in the way`** — Homebrew was +installed before nix-homebrew took over. Move the old taps aside and switch +again: `sudo mv /opt/homebrew/Library/Taps /opt/homebrew/Library/Taps.old`. **Mac App Store apps fail to install** — sign in to the App Store, then re-run the switch. `mas` cannot authenticate on its own. diff --git a/README.md b/README.md index dbca951..b8f3adc 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ My Development Setup, managed with [Nix](https://nixos.org), [nix-darwin](https: ## Quickstart Setting up a new machine? Follow [INSTALL.md](INSTALL.md) — it covers the -prerequisites (Homebrew, App Store, 1Password SSH agent) that the commands below +prerequisites (Nix, App Store, 1Password SSH agent) that the commands below assume are already in place. ### 1. Install Determinate Nix @@ -63,6 +63,7 @@ home-manager switch --flake .#simenandre@aarch64-linux ``` flake.nix # Entry point – inputs & all configurations +vet.json # Cooldown and own/trusted sources for vet INSTALL.md # Full setup guide for a new machine hosts/ nixbox/ @@ -77,6 +78,7 @@ modules/ claude.nix # Claude Code instructions and skills git.nix # Git configuration (platform-aware SSH signing path) zsh.nix # Zsh configuration (zshenv, zshrc, aliases, path, completions) + supply-chain.nix # npm/pnpm install policy, osv-scanner starship.nix # Starship prompt tmux.nix # Tmux configuration lazygit.nix # Lazygit configuration @@ -84,6 +86,8 @@ modules/ ghostty.nix # Ghostty terminal configuration nixos/ default.nix # NixOS system configuration +pkgs/ + vet/ # Cooldown and Claude review gate for `update` scripts/ cleanup.sh # macOS cleanup utility ``` diff --git a/flake.lock b/flake.lock index 25c0b59..e378c61 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,22 @@ { "nodes": { + "brew-src": { + "flake": false, + "locked": { + "lastModified": 1789661079, + "narHash": "sha256-geVTQ51PGZ1mLULM5k7wO7qt/6xenOR/YG8X7Q3eCaI=", + "owner": "Homebrew", + "repo": "brew", + "rev": "edb70f031e4170c780799633a1226ff73e1077f4", + "type": "github" + }, + "original": { + "owner": "Homebrew", + "ref": "7.0.4", + "repo": "brew", + "type": "github" + } + }, "config-nvim": { "flake": false, "locked": { @@ -63,11 +80,11 @@ ] }, "locked": { - "lastModified": 1792000010, - "narHash": "sha256-4jtkwQlReQdXsKJJfB9yT7nIf0TlwgZOk2XtxjUgDS8=", + "lastModified": 1792467281, + "narHash": "sha256-l1aACVdR3Wm6V0MGoTKqUg2tPKVNKGAVoz22jJ4QDw4=", "owner": "nix-community", "repo": "home-manager", - "rev": "5a9d300bd619c03857521b50c8877a19dbad7078", + "rev": "5bf02bcfede52ca95f693069ac28ace376942c05", "type": "github" }, "original": { @@ -76,6 +93,70 @@ "type": "github" } }, + "homebrew-anomalyco": { + "flake": false, + "locked": { + "lastModified": 1792480541, + "narHash": "sha256-Yf8ybJF9DpAbvorkfGVq5LqqN5668BRNzFn+/SuoxJ0=", + "owner": "anomalyco", + "repo": "homebrew-tap", + "rev": "6754df9897acc1a92eef20fcde2642eb09a9d38a", + "type": "github" + }, + "original": { + "owner": "anomalyco", + "repo": "homebrew-tap", + "type": "github" + } + }, + "homebrew-cask": { + "flake": false, + "locked": { + "lastModified": 1792491162, + "narHash": "sha256-Qi7NTorL4xUGK4iVNZdK3xyVpe3UXCrhw9DM6m1MKus=", + "owner": "homebrew", + "repo": "homebrew-cask", + "rev": "e1a4508aa8edac67325408102f36e263423ec967", + "type": "github" + }, + "original": { + "owner": "homebrew", + "repo": "homebrew-cask", + "type": "github" + } + }, + "homebrew-core": { + "flake": false, + "locked": { + "lastModified": 1792490933, + "narHash": "sha256-EplZvltojStAdOjfqIIxglKGBDoVD9LSAW0MpfaA6Fc=", + "owner": "homebrew", + "repo": "homebrew-core", + "rev": "1af6884527776780af682145835ef448c77b5e06", + "type": "github" + }, + "original": { + "owner": "homebrew", + "repo": "homebrew-core", + "type": "github" + } + }, + "homebrew-simenandre": { + "flake": false, + "locked": { + "lastModified": 1741815616, + "narHash": "sha256-BuRIGO0FGS/qxV5RyYsNe5X06cR+TRjN+PtQ6ZPKyHQ=", + "owner": "simenandre", + "repo": "homebrew-tools", + "rev": "9f8021e74c62d0100f1e29d689b2588b42d9c415", + "type": "github" + }, + "original": { + "owner": "simenandre", + "repo": "homebrew-tools", + "type": "github" + } + }, "nix-darwin": { "inputs": { "nixpkgs": [ @@ -83,11 +164,11 @@ ] }, "locked": { - "lastModified": 1786845137, - "narHash": "sha256-oQFip+v0luP8NIxJzmiW4Wu8bILsbFWom5l0zonl8hQ=", + "lastModified": 1792366139, + "narHash": "sha256-jto4pdsl0bgErQ5vaMc3gOyj//PILLRJuqX3I7fUk6s=", "owner": "LnL7", "repo": "nix-darwin", - "rev": "4cff07de74b50e64bdd68cd4e722ab5b6b35ee48", + "rev": "f0ec2b3fae4df92ea7788359aef9e07bb88d720f", "type": "github" }, "original": { @@ -96,13 +177,31 @@ "type": "github" } }, + "nix-homebrew": { + "inputs": { + "brew-src": "brew-src" + }, + "locked": { + "lastModified": 1789928041, + "narHash": "sha256-iV37ieTcE4ZLOYEe0VznW6dWMBe4lEL8AbK1vblyZGM=", + "owner": "zhaofengli", + "repo": "nix-homebrew", + "rev": "c11cccfdd36dd69b5323d70d354b2853379c2426", + "type": "github" + }, + "original": { + "owner": "zhaofengli", + "repo": "nix-homebrew", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1791637130, - "narHash": "sha256-TuvwvSjJrpqyIYdjhQ7Wu+ADVfeOpO2Um21/V7TOubw=", + "lastModified": 1792423257, + "narHash": "sha256-Kp5HNbXZHMtns98essAFsFEyYR98eIqWJzN6/mT/plU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ca1659429c950a11a565b5764e37a1b704e17ec8", + "rev": "682ba80136e2c8dacd6b8900f578a72329458901", "type": "github" }, "original": { @@ -117,7 +216,12 @@ "config-nvim": "config-nvim", "dina": "dina", "home-manager": "home-manager", + "homebrew-anomalyco": "homebrew-anomalyco", + "homebrew-cask": "homebrew-cask", + "homebrew-core": "homebrew-core", + "homebrew-simenandre": "homebrew-simenandre", "nix-darwin": "nix-darwin", + "nix-homebrew": "nix-homebrew", "nixpkgs": "nixpkgs", "skills": "skills" } diff --git a/flake.nix b/flake.nix index b23079e..d9004d0 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,28 @@ url = "git+https://git.simenandre.no/skills"; inputs.nixpkgs.follows = "nixpkgs"; }; + + nix-homebrew.url = "github:zhaofengli/nix-homebrew"; + + # Homebrew taps are pinned here so that tap updates land in flake.lock and + # go through vet like every other input. With homebrew-core pinned, + # nix-homebrew makes brew install from these instead of its JSON API. + homebrew-core = { + url = "github:homebrew/homebrew-core"; + flake = false; + }; + homebrew-cask = { + url = "github:homebrew/homebrew-cask"; + flake = false; + }; + homebrew-anomalyco = { + url = "github:anomalyco/homebrew-tap"; + flake = false; + }; + homebrew-simenandre = { + url = "github:simenandre/homebrew-tools"; + flake = false; + }; }; outputs = @@ -41,6 +63,11 @@ config-nvim, dina, skills, + nix-homebrew, + homebrew-core, + homebrew-cask, + homebrew-anomalyco, + homebrew-simenandre, }: let # flakeAttr must match the darwinConfigurations attribute this machine is @@ -67,6 +94,25 @@ modules = [ ./modules/darwin home-manager.darwinModules.home-manager + nix-homebrew.darwinModules.nix-homebrew + { + nix-homebrew = { + enable = true; + user = username; + autoMigrate = true; + mutableTaps = false; + taps = { + "homebrew/homebrew-core" = homebrew-core; + "homebrew/homebrew-cask" = homebrew-cask; + "anomalyco/homebrew-tap" = homebrew-anomalyco; + "simenandre/homebrew-tools" = homebrew-simenandre; + }; + trust.taps = [ + "anomalyco/tap" + "simenandre/tools" + ]; + }; + } { # Primary user for system defaults and homebrew system.primaryUser = username; @@ -90,6 +136,8 @@ }; in { + packages.aarch64-darwin.vet = nixpkgs.legacyPackages.aarch64-darwin.callPackage ./pkgs/vet { }; + darwinConfigurations = { "makkie" = mkDarwin { username = "simenandre"; diff --git a/modules/darwin/default.nix b/modules/darwin/default.nix index d23e795..1ccb7c9 100644 --- a/modules/darwin/default.nix +++ b/modules/darwin/default.nix @@ -144,5 +144,9 @@ # Specific toolsets rclone + + # Supply-chain checks: vet gates `update`, nvd shows what a switch changes + (pkgs.callPackage ../../pkgs/vet { }) + nvd ]; } diff --git a/modules/darwin/homebrew.nix b/modules/darwin/homebrew.nix index 51749e1..3175e97 100644 --- a/modules/darwin/homebrew.nix +++ b/modules/darwin/homebrew.nix @@ -1,19 +1,18 @@ -{ ... }: +{ config, ... }: { homebrew = { enable = true; + # Taps are pinned in flake.lock (nix-homebrew in flake.nix), so upgrading + # moves to the versions vet let through and never past them. onActivation = { - autoUpdate = true; + autoUpdate = false; upgrade = true; cleanup = "zap"; }; - taps = [ - "simenandre/tools" - "anomalyco/tap" - ]; + taps = builtins.attrNames config.nix-homebrew.taps; # Note: most CLI tools live in nixpkgs (modules/darwin/default.nix). # These stay in brew on purpose: shell completions, the macOS container diff --git a/modules/home/default.nix b/modules/home/default.nix index 5331684..e815c7c 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -10,6 +10,7 @@ ./herdr.nix ./onepassword.nix ./starship.nix + ./supply-chain.nix ./tmux.nix ./lazygit.nix ./twm.nix diff --git a/modules/home/supply-chain.nix b/modules/home/supply-chain.nix new file mode 100644 index 0000000..43c5f3b --- /dev/null +++ b/modules/home/supply-chain.nix @@ -0,0 +1,26 @@ +{ pkgs, lib, ... }: + +let + # Most registry compromises are caught and unpublished within days, so a + # week-old version has had time to be noticed. Lifecycle scripts are + # already blocked by pnpm unless a project allows them. + pnpmConfig = '' + minimumReleaseAge: 10080 + trustPolicy: no-downgrade + blockExoticSubdeps: true + ''; +in +{ + # pnpm reads $XDG_CONFIG_HOME/pnpm/config.yaml, and on macOS falls back to + # ~/Library/Preferences/pnpm when XDG_CONFIG_HOME is unset (GUI apps, editors). + xdg.configFile."pnpm/config.yaml".text = pnpmConfig; + home.file."Library/Preferences/pnpm/config.yaml" = lib.mkIf pkgs.stdenv.isDarwin { + text = pnpmConfig; + }; + + # An env var rather than ~/.npmrc, which `npm login` must be able to write. + # pnpm ignores it. Explicit `npm run