From 8e0c32c62fcc37ef0e330db364a096cf98f3d28c Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Mon, 19 Oct 2026 16:46:28 -0600 Subject: [PATCH 01/22] fixes --- docs/OPS.md | 3 ++- docs/TEST-AND-DEPLOY.md | 9 +++++---- docs/packages-and-forks.md | 2 +- flake.lock | 8 ++++---- flake.nix | 5 +++-- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/OPS.md b/docs/OPS.md index 673a447..0cae69a 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -1838,8 +1838,9 @@ management-ui or on :443. `surmount.grokOss.enable` stays default false. Enable only from private host-local. The module does not start a boot TUI unit. Do not set `Nice=` on grok-oss (sshd class). Flake input grok-oss is commit pin -`182293cb30fa5414dd47686b92708103519b7462` on +`525538172dc21391bc690a4cc4a8706a5c554e47` on `github:SurmountSystems/grok-oss`. Prior lock was +`182293cb30fa5414dd47686b92708103519b7462`. Earlier lock was `decb052744d25b63dbca8a95ca65b4ad794b9ad5`. Operator 2026-10-14 locked `b7ab91532c9e8008081b4f281794be43ecae73a6` (HB-1 merged as GitHub pull 167, crate version 1.0.45). Operator bumps: diff --git a/docs/TEST-AND-DEPLOY.md b/docs/TEST-AND-DEPLOY.md index d30c6d9..0ccc912 100644 --- a/docs/TEST-AND-DEPLOY.md +++ b/docs/TEST-AND-DEPLOY.md @@ -36,10 +36,11 @@ just deploy ``` `nix flake update` moves branch inputs: nixpkgs `nixos-26.05`, -`advisory-db`, grok-oss `main`, Splora, and `surmount-site`. It does -not move commit-pinned `nixpkgs-rust`, `crane`, or `sops-nix`. grok-oss -stays on `main` until `flake.nix` names another ref. Stalwart versions -are the fixed-output files under `nix/packages/`, not this command. +`advisory-db`, Splora, and `surmount-site`. It does +not move commit-pinned `nixpkgs-rust`, `crane`, `sops-nix`, or grok-oss. +grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`, not +floating `main`. Stalwart versions are the fixed-output files under +`nix/packages/`, not this command. `cargo update` uses the menhera index already configured on the laptop. `just audit` and `just audit-remote` run the same RustSec check. diff --git a/docs/packages-and-forks.md b/docs/packages-and-forks.md index 7e98c0c..55f5085 100644 --- a/docs/packages-and-forks.md +++ b/docs/packages-and-forks.md @@ -4,7 +4,7 @@ Plain English. No assumption that the reader already knows Fix ladder codes. Ladder detail still lives in [fix-and-fixos.md](fix-and-fixos.md) if you want depth. -**Last updated:** 2026-10-18 (nixpkgs-rust is commit `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1, `pkgs.arti` 2.7.0. grok-oss is commit pin `182293cb30fa5414dd47686b92708103519b7462`. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false.) Prior 2026-10-17 (nixpkgs-rust moved to nixpkgs master `c46e6c9a80d68a8b92e75a2d2cb078a8f6d8dd81`, rustc 1.99.0, `pkgs.arti` 2.7.0. Crane moved to `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`, one internal flake.lock commit after `47b6b27`. sops-nix stayed `44931708`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5` on `github:SurmountSystems/grok-oss`, not floating `main`. nixpkgs nixos-26.05 stayed `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora stayed `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db stayed `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. Stalwart server 0.16.26, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12 were still the latest GitHub tags. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-17 (nixpkgs-rust `93b590222c0327b3d952e8e30905e2bd5fed3293`, rustc 1.99.0, `pkgs.arti` 2.7.0. Stalwart server 0.16.26, vandelay 1.0.12. sops-nix `44931708`. Crane stayed `47b6b27`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5`, crate version still 1.0.45 in `xai-grok-version`. nixpkgs nixos-26.05 is `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora is `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db is `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-14 (grok-oss `main` moved to `b7ab91532c9e8008081b4f281794be43ecae73a6`, GitHub pull 167, crate version 1.0.45 in `xai-grok-version`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-10 (nixpkgs-rust moved off nixos-unstable to `c9f19b6be67332ce2ee7520c1630c804685df838`, rustc 1.99.0, `pkgs.arti` still 2.6.0. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. grok-oss was `github:SurmountSystems/grok-oss/main` at `d1d3b7377e85c760caf17b74cf6d319301a77d1e`. Crane stayed `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`.) Prior 2026-10-03 (flake lock moved: nixpkgs nixos-26.05 `774debe7a0d1b496e35677ad955a1011c6ff74f3`, nixpkgs-rust `c59305bab2065cfecc4944690d9eedbb56f3a9fa` with rustc 1.98.1 and `pkgs.arti` 2.6.0, advisory-db `ef6173cbc5c50ec8166f9a5b28f07834144373ee`, splora `32f52e8da3ae860a36d39bce334e80e7e624d5a8`, crane `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`, sops-nix `5efb5a6f4f5ab192817d28557dd4d650fa14d866`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. grok-oss stayed `6edf5fda9507c9e3c9c9f5a67871ebb3ce6cca7d` because branch `remote-1` is gone. Stalwart FODs are server 0.16.24, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.11. The Arti package stayed 2.6.0.) Prior 2026-09-19 (`surmount.sploraIndexer` is the host-local single-knob wrap over imported `nixosModules.splora`; flake input `splora` on the `surmount` branch, then locked rev `be3603dbd8e6ef0c24e37fe07beaf8067bfa2d0b`; prior lock `22d6dcf7f76c1cacc23220d80203099018cde3aa`; instance options already include cookieFile, daemonRpcAddr, jsonrpcImport, daemonDir = null, publicHealth, db cache 24, httpSocketFile default `/run/splora/${name}.http.sock`; this repo does not wrap crane src). Prior 2026-09-01 (this tree briefly wrapped crane src; that wrap is deleted). Prior 2026-09-01 (flake input `splora` from `github:SurmountSystems/splora` on the `surmount` branch; not in-tree). Prior 2026-08-27 (workspace clippy style allows in `crates/Cargo.toml`; flake clippy is `-D warnings` only. Overlay/packages/`just` still `nix run` only.) +**Last updated:** 2026-10-19 (grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. Host channel stayed nixos-26.05. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (nixpkgs-rust is commit `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1, `pkgs.arti` 2.7.0. grok-oss is commit pin `182293cb30fa5414dd47686b92708103519b7462`. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false.) Prior 2026-10-17 (nixpkgs-rust moved to nixpkgs master `c46e6c9a80d68a8b92e75a2d2cb078a8f6d8dd81`, rustc 1.99.0, `pkgs.arti` 2.7.0. Crane moved to `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`, one internal flake.lock commit after `47b6b27`. sops-nix stayed `44931708`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5` on `github:SurmountSystems/grok-oss`, not floating `main`. nixpkgs nixos-26.05 stayed `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora stayed `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db stayed `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. Stalwart server 0.16.26, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12 were still the latest GitHub tags. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-17 (nixpkgs-rust `93b590222c0327b3d952e8e30905e2bd5fed3293`, rustc 1.99.0, `pkgs.arti` 2.7.0. Stalwart server 0.16.26, vandelay 1.0.12. sops-nix `44931708`. Crane stayed `47b6b27`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5`, crate version still 1.0.45 in `xai-grok-version`. nixpkgs nixos-26.05 is `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora is `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db is `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-14 (grok-oss `main` moved to `b7ab91532c9e8008081b4f281794be43ecae73a6`, GitHub pull 167, crate version 1.0.45 in `xai-grok-version`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-10 (nixpkgs-rust moved off nixos-unstable to `c9f19b6be67332ce2ee7520c1630c804685df838`, rustc 1.99.0, `pkgs.arti` still 2.6.0. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. grok-oss was `github:SurmountSystems/grok-oss/main` at `d1d3b7377e85c760caf17b74cf6d319301a77d1e`. Crane stayed `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`.) Prior 2026-10-03 (flake lock moved: nixpkgs nixos-26.05 `774debe7a0d1b496e35677ad955a1011c6ff74f3`, nixpkgs-rust `c59305bab2065cfecc4944690d9eedbb56f3a9fa` with rustc 1.98.1 and `pkgs.arti` 2.6.0, advisory-db `ef6173cbc5c50ec8166f9a5b28f07834144373ee`, splora `32f52e8da3ae860a36d39bce334e80e7e624d5a8`, crane `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`, sops-nix `5efb5a6f4f5ab192817d28557dd4d650fa14d866`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. grok-oss stayed `6edf5fda9507c9e3c9c9f5a67871ebb3ce6cca7d` because branch `remote-1` is gone. Stalwart FODs are server 0.16.24, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.11. The Arti package stayed 2.6.0.) Prior 2026-09-19 (`surmount.sploraIndexer` is the host-local single-knob wrap over imported `nixosModules.splora`; flake input `splora` on the `surmount` branch, then locked rev `be3603dbd8e6ef0c24e37fe07beaf8067bfa2d0b`; prior lock `22d6dcf7f76c1cacc23220d80203099018cde3aa`; instance options already include cookieFile, daemonRpcAddr, jsonrpcImport, daemonDir = null, publicHealth, db cache 24, httpSocketFile default `/run/splora/${name}.http.sock`; this repo does not wrap crane src). Prior 2026-09-01 (this tree briefly wrapped crane src; that wrap is deleted). Prior 2026-09-01 (flake input `splora` from `github:SurmountSystems/splora` on the `surmount` branch; not in-tree). Prior 2026-08-27 (workspace clippy style allows in `crates/Cargo.toml`; flake clippy is `-D warnings` only. Overlay/packages/`just` still `nix run` only.) **Operator direction:** [operator-direction.md](operator-direction.md) --- diff --git a/flake.lock b/flake.lock index 5a8895d..964c697 100644 --- a/flake.lock +++ b/flake.lock @@ -3295,17 +3295,17 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1792384011, - "narHash": "sha256-sR+fs1wvU2Kw58IZeK16W9+10/9PKPGfoiXOfld0m/s=", + "lastModified": 1792446292, + "narHash": "sha256-fS7jeduc9h4kTQN+7j8eo1H73OCTgCUhxpbP2HFIf+c=", "owner": "SurmountSystems", "repo": "grok-oss", - "rev": "182293cb30fa5414dd47686b92708103519b7462", + "rev": "525538172dc21391bc690a4cc4a8706a5c554e47", "type": "github" }, "original": { "owner": "SurmountSystems", "repo": "grok-oss", - "rev": "182293cb30fa5414dd47686b92708103519b7462", + "rev": "525538172dc21391bc690a4cc4a8706a5c554e47", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 8bf283f..a372689 100644 --- a/flake.nix +++ b/flake.nix @@ -82,12 +82,13 @@ # Grok OSS TUI (bin/grok-oss). # Commit pin, not a floating main ref. The repo stays # github:SurmountSystems/grok-oss. This rev is - # 182293cb30fa5414dd47686b92708103519b7462. Prior lock was + # 525538172dc21391bc690a4cc4a8706a5c554e47. Prior lock was + # 182293cb30fa5414dd47686b92708103519b7462. Earlier lock was # decb052744d25b63dbca8a95ca65b4ad794b9ad5. Earlier lock was # 7dc3685277bb65c4a0634e91a6e2596e48ad882f (main). # Do not retarget this input to a side branch. # Default-off NixOS module; package is fail-closed when enable=true. - grok-oss.url = "github:SurmountSystems/grok-oss/182293cb30fa5414dd47686b92708103519b7462"; + grok-oss.url = "github:SurmountSystems/grok-oss/525538172dc21391bc690a4cc4a8706a5c554e47"; # Tau (bin/tau). SurmountSystems/tau master at # d2e1955b0491f538a486007d0d3ab779d95a828f (committer From d812e2752c9a6b42763d72efc31f18048b6b46fe Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Tue, 20 Oct 2026 16:19:28 -0600 Subject: [PATCH 02/22] add speedtest, scc, update grok-oss --- docs/OPS.md | 3 +- docs/TEST-AND-DEPLOY.md | 4 +- docs/packages-and-forks.md | 2 +- flake.lock | 8 +-- flake.nix | 30 ++++++++- hosts/mail-vps/configuration.nix | 4 ++ modules/default.nix | 2 + modules/speedtest.nix | 111 +++++++++++++++++++++++++++++++ nix/packages/speedtest.nix | 23 +++++++ 9 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 modules/speedtest.nix create mode 100644 nix/packages/speedtest.nix diff --git a/docs/OPS.md b/docs/OPS.md index 0cae69a..464e34a 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -1838,8 +1838,9 @@ management-ui or on :443. `surmount.grokOss.enable` stays default false. Enable only from private host-local. The module does not start a boot TUI unit. Do not set `Nice=` on grok-oss (sshd class). Flake input grok-oss is commit pin -`525538172dc21391bc690a4cc4a8706a5c554e47` on +`6703706cc7419ba71af549c1562017f302173c19` on `github:SurmountSystems/grok-oss`. Prior lock was +`525538172dc21391bc690a4cc4a8706a5c554e47`. Earlier lock was `182293cb30fa5414dd47686b92708103519b7462`. Earlier lock was `decb052744d25b63dbca8a95ca65b4ad794b9ad5`. Operator 2026-10-14 locked `b7ab91532c9e8008081b4f281794be43ecae73a6` (HB-1 merged as GitHub pull diff --git a/docs/TEST-AND-DEPLOY.md b/docs/TEST-AND-DEPLOY.md index 0ccc912..a3f2b3c 100644 --- a/docs/TEST-AND-DEPLOY.md +++ b/docs/TEST-AND-DEPLOY.md @@ -38,8 +38,8 @@ just deploy `nix flake update` moves branch inputs: nixpkgs `nixos-26.05`, `advisory-db`, Splora, and `surmount-site`. It does not move commit-pinned `nixpkgs-rust`, `crane`, `sops-nix`, or grok-oss. -grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`, not -floating `main`. Stalwart versions are the fixed-output files under +grok-oss is commit pin `6703706cc7419ba71af549c1562017f302173c19`, not +floating `main`. Prior pin was `525538172dc21391bc690a4cc4a8706a5c554e47`. Stalwart versions are the fixed-output files under `nix/packages/`, not this command. `cargo update` uses the menhera index already configured on the laptop. diff --git a/docs/packages-and-forks.md b/docs/packages-and-forks.md index 55f5085..7fbd9b8 100644 --- a/docs/packages-and-forks.md +++ b/docs/packages-and-forks.md @@ -4,7 +4,7 @@ Plain English. No assumption that the reader already knows Fix ladder codes. Ladder detail still lives in [fix-and-fixos.md](fix-and-fixos.md) if you want depth. -**Last updated:** 2026-10-19 (grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. Host channel stayed nixos-26.05. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (nixpkgs-rust is commit `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1, `pkgs.arti` 2.7.0. grok-oss is commit pin `182293cb30fa5414dd47686b92708103519b7462`. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false.) Prior 2026-10-17 (nixpkgs-rust moved to nixpkgs master `c46e6c9a80d68a8b92e75a2d2cb078a8f6d8dd81`, rustc 1.99.0, `pkgs.arti` 2.7.0. Crane moved to `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`, one internal flake.lock commit after `47b6b27`. sops-nix stayed `44931708`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5` on `github:SurmountSystems/grok-oss`, not floating `main`. nixpkgs nixos-26.05 stayed `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora stayed `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db stayed `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. Stalwart server 0.16.26, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12 were still the latest GitHub tags. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-17 (nixpkgs-rust `93b590222c0327b3d952e8e30905e2bd5fed3293`, rustc 1.99.0, `pkgs.arti` 2.7.0. Stalwart server 0.16.26, vandelay 1.0.12. sops-nix `44931708`. Crane stayed `47b6b27`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5`, crate version still 1.0.45 in `xai-grok-version`. nixpkgs nixos-26.05 is `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora is `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db is `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-14 (grok-oss `main` moved to `b7ab91532c9e8008081b4f281794be43ecae73a6`, GitHub pull 167, crate version 1.0.45 in `xai-grok-version`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-10 (nixpkgs-rust moved off nixos-unstable to `c9f19b6be67332ce2ee7520c1630c804685df838`, rustc 1.99.0, `pkgs.arti` still 2.6.0. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. grok-oss was `github:SurmountSystems/grok-oss/main` at `d1d3b7377e85c760caf17b74cf6d319301a77d1e`. Crane stayed `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`.) Prior 2026-10-03 (flake lock moved: nixpkgs nixos-26.05 `774debe7a0d1b496e35677ad955a1011c6ff74f3`, nixpkgs-rust `c59305bab2065cfecc4944690d9eedbb56f3a9fa` with rustc 1.98.1 and `pkgs.arti` 2.6.0, advisory-db `ef6173cbc5c50ec8166f9a5b28f07834144373ee`, splora `32f52e8da3ae860a36d39bce334e80e7e624d5a8`, crane `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`, sops-nix `5efb5a6f4f5ab192817d28557dd4d650fa14d866`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. grok-oss stayed `6edf5fda9507c9e3c9c9f5a67871ebb3ce6cca7d` because branch `remote-1` is gone. Stalwart FODs are server 0.16.24, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.11. The Arti package stayed 2.6.0.) Prior 2026-09-19 (`surmount.sploraIndexer` is the host-local single-knob wrap over imported `nixosModules.splora`; flake input `splora` on the `surmount` branch, then locked rev `be3603dbd8e6ef0c24e37fe07beaf8067bfa2d0b`; prior lock `22d6dcf7f76c1cacc23220d80203099018cde3aa`; instance options already include cookieFile, daemonRpcAddr, jsonrpcImport, daemonDir = null, publicHealth, db cache 24, httpSocketFile default `/run/splora/${name}.http.sock`; this repo does not wrap crane src). Prior 2026-09-01 (this tree briefly wrapped crane src; that wrap is deleted). Prior 2026-09-01 (flake input `splora` from `github:SurmountSystems/splora` on the `surmount` branch; not in-tree). Prior 2026-08-27 (workspace clippy style allows in `crates/Cargo.toml`; flake clippy is `-D warnings` only. Overlay/packages/`just` still `nix run` only.) +**Last updated:** 2026-10-20 (grok-oss is commit pin `6703706cc7419ba71af549c1562017f302173c19`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Host channel stayed nixos-26.05. Wrapped `speedtest` is `pkgs.ookla-speedtest` plus license/GDPR flags. `scc` is the nixpkgs package.) Prior 2026-10-19 (grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. Host channel stayed nixos-26.05. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (nixpkgs-rust is commit `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1, `pkgs.arti` 2.7.0. grok-oss is commit pin `182293cb30fa5414dd47686b92708103519b7462`. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false.) Prior 2026-10-17 (nixpkgs-rust moved to nixpkgs master `c46e6c9a80d68a8b92e75a2d2cb078a8f6d8dd81`, rustc 1.99.0, `pkgs.arti` 2.7.0. Crane moved to `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`, one internal flake.lock commit after `47b6b27`. sops-nix stayed `44931708`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5` on `github:SurmountSystems/grok-oss`, not floating `main`. nixpkgs nixos-26.05 stayed `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora stayed `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db stayed `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. Stalwart server 0.16.26, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12 were still the latest GitHub tags. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-17 (nixpkgs-rust `93b590222c0327b3d952e8e30905e2bd5fed3293`, rustc 1.99.0, `pkgs.arti` 2.7.0. Stalwart server 0.16.26, vandelay 1.0.12. sops-nix `44931708`. Crane stayed `47b6b27`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5`, crate version still 1.0.45 in `xai-grok-version`. nixpkgs nixos-26.05 is `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora is `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db is `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-14 (grok-oss `main` moved to `b7ab91532c9e8008081b4f281794be43ecae73a6`, GitHub pull 167, crate version 1.0.45 in `xai-grok-version`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-10 (nixpkgs-rust moved off nixos-unstable to `c9f19b6be67332ce2ee7520c1630c804685df838`, rustc 1.99.0, `pkgs.arti` still 2.6.0. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. grok-oss was `github:SurmountSystems/grok-oss/main` at `d1d3b7377e85c760caf17b74cf6d319301a77d1e`. Crane stayed `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`.) Prior 2026-10-03 (flake lock moved: nixpkgs nixos-26.05 `774debe7a0d1b496e35677ad955a1011c6ff74f3`, nixpkgs-rust `c59305bab2065cfecc4944690d9eedbb56f3a9fa` with rustc 1.98.1 and `pkgs.arti` 2.6.0, advisory-db `ef6173cbc5c50ec8166f9a5b28f07834144373ee`, splora `32f52e8da3ae860a36d39bce334e80e7e624d5a8`, crane `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`, sops-nix `5efb5a6f4f5ab192817d28557dd4d650fa14d866`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. grok-oss stayed `6edf5fda9507c9e3c9c9f5a67871ebb3ce6cca7d` because branch `remote-1` is gone. Stalwart FODs are server 0.16.24, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.11. The Arti package stayed 2.6.0.) Prior 2026-09-19 (`surmount.sploraIndexer` is the host-local single-knob wrap over imported `nixosModules.splora`; flake input `splora` on the `surmount` branch, then locked rev `be3603dbd8e6ef0c24e37fe07beaf8067bfa2d0b`; prior lock `22d6dcf7f76c1cacc23220d80203099018cde3aa`; instance options already include cookieFile, daemonRpcAddr, jsonrpcImport, daemonDir = null, publicHealth, db cache 24, httpSocketFile default `/run/splora/${name}.http.sock`; this repo does not wrap crane src). Prior 2026-09-01 (this tree briefly wrapped crane src; that wrap is deleted). Prior 2026-09-01 (flake input `splora` from `github:SurmountSystems/splora` on the `surmount` branch; not in-tree). Prior 2026-08-27 (workspace clippy style allows in `crates/Cargo.toml`; flake clippy is `-D warnings` only. Overlay/packages/`just` still `nix run` only.) **Operator direction:** [operator-direction.md](operator-direction.md) --- diff --git a/flake.lock b/flake.lock index 964c697..bd7cc7f 100644 --- a/flake.lock +++ b/flake.lock @@ -3295,17 +3295,17 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1792446292, - "narHash": "sha256-fS7jeduc9h4kTQN+7j8eo1H73OCTgCUhxpbP2HFIf+c=", + "lastModified": 1792529825, + "narHash": "sha256-T1tHT+d8v6NuZHeJnUJd1UHQgQHe5XhiNeZYbEoahcM=", "owner": "SurmountSystems", "repo": "grok-oss", - "rev": "525538172dc21391bc690a4cc4a8706a5c554e47", + "rev": "6703706cc7419ba71af549c1562017f302173c19", "type": "github" }, "original": { "owner": "SurmountSystems", "repo": "grok-oss", - "rev": "525538172dc21391bc690a4cc4a8706a5c554e47", + "rev": "6703706cc7419ba71af549c1562017f302173c19", "type": "github" } }, diff --git a/flake.nix b/flake.nix index a372689..4ae2cf0 100644 --- a/flake.nix +++ b/flake.nix @@ -10,6 +10,9 @@ # GHA: .github/workflows/ci.yml job display name is `just ci` (required-check footgun) # End-to-end: nix run .#e2e (local) / nix run .#e2e-host (env-gated; never in ci) # Operator bins: nix run .# -- args (just aliases only nix-run). +# Speedtest: nix run .#speedtest (Ookla CLI; --accept-license --accept-gdpr). +# Module: surmount.speedtest.enable ; timer: surmount.speedtest.periodic.enable +# (default off, interval 6h, JSON to the journal, optional logFile). # Heavy: mail-vm-test, mail-vps-eval, stalwart-mail package are optional. { @@ -82,13 +85,14 @@ # Grok OSS TUI (bin/grok-oss). # Commit pin, not a floating main ref. The repo stays # github:SurmountSystems/grok-oss. This rev is - # 525538172dc21391bc690a4cc4a8706a5c554e47. Prior lock was + # 6703706cc7419ba71af549c1562017f302173c19. Prior lock was + # 525538172dc21391bc690a4cc4a8706a5c554e47. Earlier lock was # 182293cb30fa5414dd47686b92708103519b7462. Earlier lock was # decb052744d25b63dbca8a95ca65b4ad794b9ad5. Earlier lock was # 7dc3685277bb65c4a0634e91a6e2596e48ad882f (main). # Do not retarget this input to a side branch. # Default-off NixOS module; package is fail-closed when enable=true. - grok-oss.url = "github:SurmountSystems/grok-oss/525538172dc21391bc690a4cc4a8706a5c554e47"; + grok-oss.url = "github:SurmountSystems/grok-oss/6703706cc7419ba71af549c1562017f302173c19"; # Tau (bin/tau). SurmountSystems/tau master at # d2e1955b0491f538a486007d0d3ab779d95a828f (committer @@ -197,6 +201,8 @@ # (laptop cargo still uses Menhera). Do not wrap src again here. splora = self.packages.${system}.splora; splora-liquid = self.packages.${system}.splora-liquid; + # Wrapped Ookla CLI. Unfree allowlist is only ookla-speedtest. + speedtest = self.packages.${system}.speedtest; }; mkPkgs = @@ -474,6 +480,18 @@ # Flake-pinned just for GHA (`nix shell .#just -c just ci`) and local # bootstrap without a host just install. Same pattern as grok-oss. just = pkgs.just; + # Ookla Speedtest CLI with --accept-license --accept-gdpr. + # Unfree predicate is only ookla-speedtest. nix run .#speedtest + speedtest = + let + pkgsOokla = import nixpkgs { + inherit system; + config.allowUnfreePredicate = pkg: lib.getName pkg == "ookla-speedtest"; + }; + in + pkgsOokla.callPackage ./nix/packages/speedtest.nix { }; + # boyter/scc from the host nixpkgs channel. + scc = pkgs.scc; default = self.packages.${system}.management-ui; } ); @@ -647,6 +665,14 @@ type = "app"; program = "${self.packages.${system}.surmount-rekey}/bin/surmount-rekey"; }; + speedtest = { + type = "app"; + program = "${self.packages.${system}.speedtest}/bin/speedtest"; + }; + scc = { + type = "app"; + program = "${self.packages.${system}.scc}/bin/scc"; + }; }); checks = forAllSystems ( diff --git a/hosts/mail-vps/configuration.nix b/hosts/mail-vps/configuration.nix index 04005ec..1a3e9b2 100644 --- a/hosts/mail-vps/configuration.nix +++ b/hosts/mail-vps/configuration.nix @@ -339,8 +339,12 @@ dig tcpdump git + scc ]; + # Wrapped Ookla CLI on PATH. Periodic JSON timer stays off. + surmount.speedtest.enable = true; + # Journal persistence + size cap live in modules/logging.nix # (surmount.logging; Storage=persistent, SystemMaxUse scaffold 1G). # Do not duplicate extraConfig here. diff --git a/modules/default.nix b/modules/default.nix index 48fde5c..c85b386 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -42,5 +42,7 @@ ./grok-oss.nix # Optional Tau coding agent: default off, no daemon, no extensions. ./tau.nix + # Wrapped Ookla Speedtest CLI. Mail host enables the package; timer stays off. + ./speedtest.nix ]; } diff --git a/modules/speedtest.nix b/modules/speedtest.nix new file mode 100644 index 0000000..742b741 --- /dev/null +++ b/modules/speedtest.nix @@ -0,0 +1,111 @@ +# Ookla Speedtest CLI (wrapped). Default off except the mail host sets enable. +# Periodic JSON timer stays off until surmount.speedtest.periodic.enable. +# nix run .#speedtest +# Module: surmount.speedtest.enable +# Timer: surmount.speedtest.periodic.enable (default every 6 hours) + +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.surmount.speedtest; + inherit (lib) + mkEnableOption + mkIf + mkMerge + mkOption + types + optionals + escapeShellArgs + optionalString + ; + speedtestBin = "${cfg.package}/bin/speedtest"; + jsonArgs = [ + "-f" + "json" + ] + ++ optionals (cfg.periodic.serverId != null) [ + "--server-id" + (toString cfg.periodic.serverId) + ] + ++ cfg.periodic.extraFlags; +in +{ + options.surmount.speedtest = { + enable = mkEnableOption "wrapped Ookla Speedtest CLI on PATH"; + + package = mkOption { + type = types.package; + default = + pkgs.speedtest or (throw "pkgs.speedtest missing (fail-closed); enable the flake overlay"); + defaultText = "pkgs.speedtest"; + description = "Wrapped speedtest package (license and GDPR flags already accepted)."; + }; + + periodic = { + enable = mkEnableOption "systemd timer that runs a JSON speedtest"; + + interval = mkOption { + type = types.str; + default = "6h"; + description = "systemd OnUnitActiveSec for the timer (default every 6 hours)."; + }; + + serverId = mkOption { + type = types.nullOr types.ints.positive; + default = null; + description = "Optional Ookla server id passed as --server-id."; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Extra flags after -f json (and --server-id when set)."; + }; + + logFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If set, append each JSON result to this path as well as the journal. + The parent directory must exist and be writable by root. + ''; + }; + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + nixpkgs.config.allowUnfreePredicate = pkg: lib.getName pkg == "ookla-speedtest"; + environment.systemPackages = [ cfg.package ]; + }) + (mkIf (cfg.enable && cfg.periodic.enable) { + systemd.services.surmount-speedtest = { + description = "Ookla Speedtest JSON probe"; + serviceConfig = { + Type = "oneshot"; + User = "root"; + }; + script = '' + set -euo pipefail + ${lib.escapeShellArg speedtestBin} ${escapeShellArgs jsonArgs} ${ + optionalString (cfg.periodic.logFile != null) "| tee -a ${lib.escapeShellArg cfg.periodic.logFile}" + } + ''; + }; + systemd.timers.surmount-speedtest = { + description = "Periodic Ookla Speedtest JSON probe"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "5min"; + OnUnitActiveSec = cfg.periodic.interval; + Persistent = true; + Unit = "surmount-speedtest.service"; + }; + }; + }) + ]; +} diff --git a/nix/packages/speedtest.nix b/nix/packages/speedtest.nix new file mode 100644 index 0000000..70d595d --- /dev/null +++ b/nix/packages/speedtest.nix @@ -0,0 +1,23 @@ +# Wrapped Ookla Speedtest CLI (pkgs.ookla-speedtest). +# Always passes --accept-license --accept-gdpr. Extra args still pass through. +# nix run .#speedtest +{ + ookla-speedtest, + makeWrapper, + symlinkJoin, +}: +symlinkJoin { + pname = "speedtest"; + name = "speedtest"; + paths = [ ookla-speedtest ]; + nativeBuildInputs = [ makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/speedtest \ + --add-flags "--accept-license" \ + --add-flags "--accept-gdpr" + ''; + meta = ookla-speedtest.meta // { + description = "Ookla Speedtest CLI with license and GDPR flags accepted"; + mainProgram = "speedtest"; + }; +} From f020b934c13686e76c94fa9982e9fe7c9d10268e Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Tue, 20 Oct 2026 16:25:18 -0600 Subject: [PATCH 03/22] fixes --- flake.nix | 3 ++- modules/speedtest.nix | 4 +++- nix/packages/speedtest.nix | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 4ae2cf0..4c00aa4 100644 --- a/flake.nix +++ b/flake.nix @@ -486,7 +486,8 @@ let pkgsOokla = import nixpkgs { inherit system; - config.allowUnfreePredicate = pkg: lib.getName pkg == "ookla-speedtest"; + config.allowUnfreePredicate = + pkg: builtins.elem (lib.getName pkg) [ "ookla-speedtest" "speedtest" ]; }; in pkgsOokla.callPackage ./nix/packages/speedtest.nix { }; diff --git a/modules/speedtest.nix b/modules/speedtest.nix index 742b741..14d3231 100644 --- a/modules/speedtest.nix +++ b/modules/speedtest.nix @@ -79,7 +79,9 @@ in config = mkMerge [ (mkIf cfg.enable { - nixpkgs.config.allowUnfreePredicate = pkg: lib.getName pkg == "ookla-speedtest"; + # Wrapper getName is speedtest; upstream attr is ookla-speedtest. + nixpkgs.config.allowUnfreePredicate = + pkg: builtins.elem (lib.getName pkg) [ "ookla-speedtest" "speedtest" ]; environment.systemPackages = [ cfg.package ]; }) (mkIf (cfg.enable && cfg.periodic.enable) { diff --git a/nix/packages/speedtest.nix b/nix/packages/speedtest.nix index 70d595d..918728c 100644 --- a/nix/packages/speedtest.nix +++ b/nix/packages/speedtest.nix @@ -19,5 +19,6 @@ symlinkJoin { meta = ookla-speedtest.meta // { description = "Ookla Speedtest CLI with license and GDPR flags accepted"; mainProgram = "speedtest"; + outputsToInstall = [ "out" ]; }; } From 0bfc4799d32587acdf544cc732ed55ba396469f3 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Tue, 20 Oct 2026 18:29:05 -0600 Subject: [PATCH 04/22] fixes --- crates/surmount-deploy-host/src/run.rs | 9 ++++++++- nix/packages/grok-oss.nix | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/surmount-deploy-host/src/run.rs b/crates/surmount-deploy-host/src/run.rs index 54ee00f..3f77cde 100644 --- a/crates/surmount-deploy-host/src/run.rs +++ b/crates/surmount-deploy-host/src/run.rs @@ -538,7 +538,12 @@ fn switch_remote_command(flake_ref: &str) -> String { let flake_q = shell_quote(flake_ref); [ "before=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)", - &format!("nixos-rebuild switch --flake {flake_q}"), + // Live nix.conf still has max-jobs 8 until this generation + // switches. 32 matches surmount.remoteBuilder.maxJobs. cores 0 + // is auto so one drv can use the machine. + &format!( + "nixos-rebuild switch --flake {flake_q} --option max-jobs 32 --option cores 0" + ), "rc=$?", "after=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)", "if [ -n \"$after\" ] && [ \"$after\" != \"$before\" ]; then printf '%s\\n' \"synthetic-built-generation: $after\"; else printf '%s\\n' \"synthetic-built-generation: unchanged\"; fi", @@ -951,6 +956,8 @@ mod tests { assert!(PROFILE_GENERATION_REMOTE.contains("readlink")); let cmd = switch_remote_command("path:/tmp/repo#mail-vps"); assert!(cmd.contains("nixos-rebuild switch --flake")); + assert!(cmd.contains("--option max-jobs 32")); + assert!(cmd.contains("--option cores 0")); assert!(cmd.contains("path:/tmp/repo#mail-vps")); assert!(cmd.contains("synthetic-built-generation:")); assert!(!cmd.contains("post-switch-smoke")); diff --git a/nix/packages/grok-oss.nix b/nix/packages/grok-oss.nix index 69fcfdf..ebd2a36 100644 --- a/nix/packages/grok-oss.nix +++ b/nix/packages/grok-oss.nix @@ -24,7 +24,8 @@ let -e 's/pkgs\.stdenv\.isLinux/pkgs.stdenv.hostPlatform.isLinux/g' \ -e 's/pkgs\.stdenv\.isDarwin/pkgs.stdenv.hostPlatform.isDarwin/g' \ -e 's/stdenv\.isLinux/stdenv.hostPlatform.isLinux/g' \ - -e 's/stdenv\.isDarwin/stdenv.hostPlatform.isDarwin/g' + -e 's/stdenv\.isDarwin/stdenv.hostPlatform.isDarwin/g' \ + -e 's/CARGO_BUILD_JOBS = "2";/CARGO_BUILD_JOBS = "32";/' ''; craneLib = (grokOssFlake.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; built = import "${patchedSrc}/flake/grok-oss.nix" { From 4d6d995c537d5c614e43e8858294d6675085bcd9 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Fri, 23 Oct 2026 23:51:32 -0600 Subject: [PATCH 05/22] fixes --- Cargo.lock | 158 +- Cargo.toml | 30 + crates/management-ui/src/api.rs | 15 +- crates/management-ui/src/config.rs | 26 +- crates/management-ui/src/main.rs | 142 +- crates/management-ui/src/onion_discovery.rs | 12 +- crates/management-ui/src/redirect.rs | 108 +- crates/management-ui/src/status.rs | 295 + crates/surmount-acme-namecheap/src/laptop.rs | 14 +- .../host-profile/sample-host-profile.toml | 1 + crates/surmount-acme-namecheap/tests/cli.rs | 11 +- .../host-profile/sample-host-profile.toml | 1 + docs/COMPACTION-PIN.md | 1 + docs/EDGE_AND_TLS.md | 20 +- docs/OPS.md | 70 +- docs/SEARCH_AND_UI.md | 8 +- docs/SECRETS.md | 2 +- docs/SECURITY.md | 27 +- docs/TEST-AND-DEPLOY.md | 14 +- docs/packages-and-forks.md | 11 +- flake.lock | 6017 +---------------- flake.nix | 55 +- hosts/mail-vps/configuration.nix | 8 +- justfile | 3 + modules/default.nix | 2 + modules/grok-oss.nix | 77 +- modules/management-ui.nix | 13 +- modules/perf-tools.nix | 22 + modules/speedtest.nix | 6 +- modules/tau.nix | 28 +- nix/packages/grok-oss.nix | 37 - script/testdata/host-profile/README.md | 2 +- .../host-profile/expected-host-local-acme.nix | 2 +- .../host-profile/sample-host-profile.toml | 12 +- tests/module-eval.nix | 104 +- tests/perf-tools-contract.nix | 62 + 36 files changed, 1106 insertions(+), 6310 deletions(-) create mode 100644 crates/management-ui/src/status.rs create mode 100644 modules/perf-tools.nix delete mode 100644 nix/packages/grok-oss.nix create mode 100644 tests/perf-tools-contract.nix diff --git a/Cargo.lock b/Cargo.lock index b1c98c6..cfaee58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,7 +243,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -262,7 +262,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -278,7 +278,7 @@ dependencies = [ "proc-macro2", "quote", "quote-use", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -289,9 +289,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -426,15 +426,6 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - [[package]] name = "bech32" version = "0.11.1" @@ -595,9 +586,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "jobserver", @@ -694,7 +685,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -922,9 +913,8 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +version = "0.4.5" +source = "git+https://github.com/RustCrypto/utils?rev=88d6e90d4cfd2fc3333d00e13c99241507d2138f#88d6e90d4cfd2fc3333d00e13c99241507d2138f" dependencies = [ "cmov", ] @@ -1027,9 +1017,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" [[package]] name = "derive-where" @@ -1039,7 +1029,7 @@ checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1073,7 +1063,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1125,9 +1115,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "erased" @@ -1346,7 +1336,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -1787,14 +1777,12 @@ dependencies = [ [[package]] name = "i18n-config" version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" +source = "git+https://github.com/kellpossible/cargo-i18n?rev=2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4#2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4" dependencies = [ - "basic-toml", "log", "serde", - "serde_derive", - "thiserror 1.0.69", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", "unic-langid", ] @@ -2258,7 +2246,7 @@ dependencies = [ "quote", "rstml", "serde", - "syn 3.0.6", + "syn 3.0.9", "walkdir", ] @@ -2281,7 +2269,7 @@ dependencies = [ "rstml", "rustc_version", "server_fn_macro", - "syn 3.0.6", + "syn 3.0.9", "uuid", ] @@ -2340,21 +2328,21 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "manyhow" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c69ea6e469ca785c846b25dd69eac9e9c916b51d8ada1f212866787ee1da12d" +checksum = "f1704b073316272a6785b70599f91f73b49022c2e290b065591ee6b9109ed8c7" dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "manyhow-macros" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc5c1b2c4000056dae3619fea36e79ce79dabfee4907d2dcad6612b189e370" +checksum = "31601db71f1f5fb6f831a18aa3052d13a62528ee4f2cba7e8c9a5e258d953725" dependencies = [ "proc-macro-utils", "proc-macro2", @@ -2738,7 +2726,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2769,7 +2757,7 @@ dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -2800,7 +2788,7 @@ checksum = "df921e1ef974ebc8dd9effdb0c037a96928ae8cbf871438e02d6fd67204b595e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "version_check", ] @@ -2890,7 +2878,7 @@ dependencies = [ "proc-macro-utils", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3136,7 +3124,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 3.0.6", + "syn 3.0.9", "thiserror 2.0.21", ] @@ -3181,7 +3169,7 @@ version = "8.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" dependencies = [ - "sha2 0.11.0", + "sha2 0.11.1", "walkdir", ] @@ -3225,9 +3213,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3274,9 +3262,8 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +version = "0.103.16" +source = "git+https://github.com/rustls/webpki?rev=8ecdcc76f56b68fda67a6adf7976f5eed5006a4c#8ecdcc76f56b68fda67a6adf7976f5eed5006a4c" dependencies = [ "aws-lc-rs", "ring", @@ -3453,14 +3440,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -3557,7 +3544,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 3.0.6", + "syn 3.0.9", "xxhash-rust", ] @@ -3568,7 +3555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9a4f81a0d855f279c3448243138a55cbfdd9851da9341b9c1e5971b90089e61" dependencies = [ "server_fn_macro", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3584,9 +3571,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures 0.3.1", @@ -3661,9 +3648,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "socket2" @@ -3780,7 +3767,7 @@ dependencies = [ "rustls-pki-types", "serde", "serde_json", - "sha2 0.11.0", + "sha2 0.11.1", "time", "tokio", "tokio-rustls", @@ -3865,9 +3852,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -3902,7 +3889,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -3974,7 +3961,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4044,9 +4031,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.2" +version = "1.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" dependencies = [ "bytes", "libc", @@ -4066,7 +4053,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] @@ -4081,9 +4068,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" dependencies = [ "bytes", "futures-core", @@ -4335,9 +4322,9 @@ checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" @@ -4415,9 +4402,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.27.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4504,7 +4491,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -4574,9 +4561,8 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +version = "1.0.11" +source = "git+https://github.com/rustls/webpki-roots?rev=7ebfa0b377fd0c7e0a2848f9d9b2867a4c2198cf#7ebfa0b377fd0c7e0a2848f9d9b2867a4c2198cf" dependencies = [ "rustls-pki-types", ] @@ -4759,24 +4745,24 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure 0.14.0", ] [[package]] name = "zerocopy" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", @@ -4800,7 +4786,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", "synstructure 0.14.0", ] @@ -4855,7 +4841,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.6", + "syn 3.0.9", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7088c0e..501e1bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,36 @@ age = { version = "0.12", default-features = false } # rustls 0.23.46 is not on menhera 10d yet (10d stops at 0.23.45). Public # git tag v/0.23.46, commit f04804fa13341a9b1dc02b5d1dd8a746f2c4b7fb. # When menhera 10d lists 0.23.46, drop this rustls line. +# +# ctutils 0.4.4+ is not on menhera 10d yet (10d stops at 0.4.3). Public +# git tag ctutils-v0.4.5, commit 88d6e90d4cfd2fc3333d00e13c99241507d2138f. +# RUSTSEC-2026-0385 patched list is >=0.4.4. +# https://rustsec.org/advisories/RUSTSEC-2026-0385 (accessed: 2026-10-24). +# When menhera 10d lists 0.4.4 or later, drop this ctutils line. +# +# rustls-webpki 0.103.16 is not on menhera 10d yet (10d stops at 0.103.15). +# Public git tag v/0.103.16, commit 8ecdcc76f56b68fda67a6adf7976f5eed5006a4c. +# RUSTSEC-2026-0382 patched list is >=0.103.16, <0.104.0-alpha.1. +# https://rustsec.org/advisories/RUSTSEC-2026-0382 (accessed: 2026-10-24). +# When menhera 10d lists 0.103.16, drop this rustls-webpki line. +# +# webpki-root-certs 1.0.10+ is not on menhera 10d yet (10d stops at 1.0.9). +# Public git tag v/1.0.11, commit 7ebfa0b377fd0c7e0a2848f9d9b2867a4c2198cf. +# RUSTSEC-2026-0383 patched list is >=1.0.10. +# https://rustsec.org/advisories/RUSTSEC-2026-0383 (accessed: 2026-10-24). +# When menhera 10d lists 1.0.10 or later, drop this webpki-root-certs line. +# +# i18n-config 0.4.8 on crates.io / menhera still depends on basic-toml +# (RUSTSEC-2026-0374, unmaintained, no patched versions). git master +# 2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4 is still version 0.4.8 and +# uses the toml crate. Do not ignore RUSTSEC-2026-0374. +# https://rustsec.org/advisories/RUSTSEC-2026-0374 (accessed: 2026-10-24). +# When a crates.io i18n-config without basic-toml is on menhera 10d, +# drop this i18n-config line. [patch.crates-io] rustls = { git = "https://github.com/rustls/rustls", rev = "f04804fa13341a9b1dc02b5d1dd8a746f2c4b7fb" } +ctutils = { git = "https://github.com/RustCrypto/utils", rev = "88d6e90d4cfd2fc3333d00e13c99241507d2138f" } +rustls-webpki = { git = "https://github.com/rustls/webpki", rev = "8ecdcc76f56b68fda67a6adf7976f5eed5006a4c" } +webpki-root-certs = { git = "https://github.com/rustls/webpki-roots", rev = "7ebfa0b377fd0c7e0a2848f9d9b2867a4c2198cf" } +i18n-config = { git = "https://github.com/kellpossible/cargo-i18n", rev = "2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4" } diff --git a/crates/management-ui/src/api.rs b/crates/management-ui/src/api.rs index 427f1fa..bbfd5b3 100644 --- a/crates/management-ui/src/api.rs +++ b/crates/management-ui/src/api.rs @@ -49,8 +49,9 @@ pub struct DomainsInventory { /// Build config-backed domain inventory (not a live Stalwart directory list). /// /// Union, dedupe, stable-sort: primary + mail + services, derived -/// `www.{primary}` and `mta-sts.{primary}`, extra mail hostnames, then -/// every `static_vhosts` key. First role wins when a name appears twice. +/// `www.{primary}`, `mta-sts.{primary}`, and `status.{primary}`, extra +/// mail hostnames, then every `static_vhosts` key. First role wins when +/// a name appears twice. pub fn domains_inventory(config: &AppConfig) -> DomainsInventory { let mut by_name: BTreeMap = BTreeMap::new(); let mut insert = |name: &str, role: &str| { @@ -65,6 +66,10 @@ pub fn domains_inventory(config: &AppConfig) -> DomainsInventory { insert(&config.services_hostname, "services_hostname"); insert(&format!("www.{}", config.primary_domain), "www"); insert(&format!("mta-sts.{}", config.primary_domain), "mta_sts"); + insert( + &crate::redirect::status_hostname(&config.primary_domain), + "status_hostname", + ); for host in &config.extra_mail_hostnames { insert(host, "extra_mail_hostname"); } @@ -1151,6 +1156,7 @@ mod tests { "example.test", "www.example.test", "mta-sts.example.test", + "status.example.test", "mail.example.test", "services.example.test", "yiffa.app", @@ -1184,6 +1190,11 @@ mod tests { .iter() .any(|d| d.role == "mta_sts" && d.name == "mta-sts.example.test") ); + assert!( + inv.domains + .iter() + .any(|d| d.role == "status_hostname" && d.name == "status.example.test") + ); assert!( inv.domains .iter() diff --git a/crates/management-ui/src/config.rs b/crates/management-ui/src/config.rs index 0f50363..c9e9b67 100644 --- a/crates/management-ui/src/config.rs +++ b/crates/management-ui/src/config.rs @@ -512,13 +512,14 @@ impl AppConfig { .unwrap_or_else(|| { // Default open-redirect allowlist: services (operator console), // apex + www (public COMING SOON; same-host HTTPS upgrade), - // mail name, MTA-STS policy host. + // mail name, MTA-STS policy host, systems dashboard Host. vec![ services_hostname.clone(), primary_domain.clone(), format!("www.{primary_domain}"), mail_hostname.clone(), format!("mta-sts.{primary_domain}"), + crate::redirect::status_hostname(&primary_domain), ] }); @@ -554,6 +555,7 @@ impl AppConfig { let extra_mail_hostnames = extra_mail_hostnames_from_env(); let redirect_allowed_hosts = union_static_vhost_hosts(redirect_allowed_hosts, &static_vhosts); + let redirect_allowed_hosts = union_status_hostname(redirect_allowed_hosts, &primary_domain); // Vaultwarden: operator-published URL only. Never invent; no admin token. let vaultwarden_url = resolve_vaultwarden_url_from_env(); // Path proxy to loopback Rocket (default off). Fail-closed on bad prefix/upstream. @@ -900,6 +902,20 @@ pub fn extra_onion_hosts_union( hosts } +fn union_status_hostname(mut allowed: Vec, primary_domain: &str) -> Vec { + let status = crate::redirect::status_hostname(primary_domain); + if status.is_empty() { + return allowed; + } + if !allowed + .iter() + .any(|a| crate::redirect::host_is_allowlisted(&status, std::slice::from_ref(a))) + { + allowed.push(status); + } + allowed +} + pub fn union_static_vhost_hosts( mut allowed: Vec, vhosts: &BTreeMap, @@ -1094,6 +1110,13 @@ mod tests { fn splora_proxy_and_http3_default_off_on_plain_http() { let _g = EnvGuard::acquire(); let cfg = AppConfig::from_env().unwrap(); + assert!( + cfg.redirect_allowed_hosts + .iter() + .any(|h| h == "status.surmount.systems"), + "default :80 allowlist must include systems dashboard Host: {:?}", + cfg.redirect_allowed_hosts + ); assert!(!cfg.splora_proxy.enable); assert!(cfg.splora_proxy.portal_host.is_none()); assert!(!cfg.http3.enable); @@ -1811,6 +1834,7 @@ mod tests { ); assert!(!hosts.contains(&"www.example.test")); assert!(!hosts.contains(&"mta-sts.example.test")); + assert!(!hosts.contains(&"status.example.test")); assert!(!hosts.contains(&"mail.example.test")); let services = cfg.onion_discovery.lookup("services.example.test").unwrap(); assert_eq!(services.onion_port, 443); diff --git a/crates/management-ui/src/main.rs b/crates/management-ui/src/main.rs index 1212130..71dbfc9 100644 --- a/crates/management-ui/src/main.rs +++ b/crates/management-ui/src/main.rs @@ -14,7 +14,8 @@ //! when that directory has index.html; otherwise UNDER CONSTRUCTION //! (same-host HTTP->HTTPS). Extra Hosts from `SURMOUNT_STATIC_VHOSTS` (or //! `_FILE`) use the same path/MIME/CSP rules with a Host -> root map. -//! Operator console only on the services hostname. +//! Operator console only on the services hostname. `status.{primary}` +//! (production: status.surmount.systems) is the public systems dashboard. //! - Optional MTA-STS policy at `/.well-known/mta-sts.txt` (default off) //! - Optional loopback cleartext **full API** listener (`SURMOUNT_LOCAL_CLEARTEXT_LISTEN`) //! for local reverse-proxies (Arti HS lean path) when primary is https @@ -36,6 +37,7 @@ mod onion_discovery; mod pages; mod proxy_vaultwarden; mod redirect; +mod status; mod tls; use std::net::SocketAddr; @@ -68,7 +70,7 @@ use crate::mta_sts::{mta_sts_policy_body, should_serve_mta_sts_policy}; use crate::redirect::{ HostSurface, HttpToHttps, RedirectBindDecision, RedirectStatus, apex_public_path_is_edge_exception, classify_host_surface, https_port_for_redirect, - is_public_static_surface, redirect_bind_decision, redirect_http_to_https, + is_public_non_console_surface, redirect_bind_decision, redirect_http_to_https, request_authority_host, should_reject_public_static_api, should_serve_apex_coming_soon, static_vhost_document_root, }; @@ -735,7 +737,7 @@ async fn security_headers_middleware( }; let vault_proxy = state.config.vaultwarden_proxy.enable && state.config.vaultwarden_proxy.matches_path(&vault_path); - let public_static = is_public_static_surface(classify_host_surface( + let public_static = is_public_non_console_surface(classify_host_surface( &csp_host, state.config.primary_domain.as_str(), state.config.services_hostname.as_str(), @@ -1648,6 +1650,14 @@ async fn apex_public_host_middleware( return (StatusCode::NOT_FOUND, "not found").into_response(); } let surface = classify_host_surface(&host, primary, services, &state.config.static_vhosts); + if surface == HostSurface::SystemsStatus && path_only != "/health" && path_only != "/api/health" + { + return crate::status::serve_systems_status( + &state.config, + path_only, + header_str(request.headers(), "accept"), + ); + } if surface == HostSurface::ApexPublic && !apex_public_path_is_edge_exception(path) && let Some(root) = state.config.apex_public_root.as_deref() @@ -2063,6 +2073,7 @@ mod edge_wire_tests { "example.test".into(), "www.example.test".into(), "mta-sts.example.test".into(), + "status.example.test".into(), ], https_allow_cleartext_escape: false, acme: crate::acme::AcmeConfig::default(), @@ -2126,6 +2137,7 @@ mod edge_wire_tests { "example.test".into(), "www.example.test".into(), "mta-sts.example.test".into(), + "status.example.test".into(), ], https_allow_cleartext_escape: false, acme: crate::acme::AcmeConfig::default(), @@ -3435,6 +3447,126 @@ mod edge_wire_tests { let _ = serve.await; } + /// Named contract: Host status.{primary} is the systems dashboard on Axum. + /// Hermetic GET calls the shipped handler (build_router), not a stub. + #[tokio::test] + async fn status_host_serves_systems_dashboard_not_console() { + let state = test_state(0); + let app = build_router(state.clone()); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let serve = tokio::spawn(async move { + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .await + .ok(); + }); + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .timeout(Duration::from_secs(2)) + .build() + .unwrap(); + + let res = client + .get(format!("http://{addr}/")) + .header("Host", "status.example.test") + .send() + .await + .unwrap(); + assert_eq!(res.status(), reqwest::StatusCode::OK); + let body = res.text().await.unwrap(); + assert!(!body.is_empty(), "systems dashboard body must be non-empty"); + assert!( + body.contains("data-surface=\"systems-status\"") || body.contains("systems_status"), + "must mark systems status surface: {body}" + ); + for field in ["mail", "edge", "tls", "mta_sts", "console"] { + assert!( + body.contains(field), + "handler must emit named system {field}: {body}" + ); + } + let lower = body.to_ascii_lowercase(); + assert!( + !lower.contains("dashboard overview"), + "must not be the operator console: {body}" + ); + assert!( + !body.contains("UNDER CONSTRUCTION"), + "status Host is not coming-soon: {body}" + ); + + let json_res = client + .get(format!("http://{addr}/status.json")) + .header("Host", "status.example.test") + .send() + .await + .unwrap(); + assert_eq!(json_res.status(), reqwest::StatusCode::OK); + let json: serde_json::Value = json_res.json().await.unwrap(); + assert_eq!(json["surface"], "systems_status"); + assert_eq!(json["service"], "surmount"); + assert_eq!(json["host"], "status.example.test"); + let systems = json["systems"].as_array().expect("systems array"); + assert!( + !systems.is_empty(), + "systems array must be non-empty: {json}" + ); + let names: Vec<&str> = systems.iter().filter_map(|s| s["name"].as_str()).collect(); + for must in ["mail", "edge", "tls", "mta_sts", "console"] { + assert!( + names.contains(&must), + "JSON must name system {must}: {json}" + ); + } + + let api = client + .get(format!("http://{addr}/api/v1/system")) + .header("Host", "status.example.test") + .send() + .await + .unwrap(); + assert_eq!( + api.status(), + reqwest::StatusCode::NOT_FOUND, + "status Host must not serve the operator system API" + ); + let api_body = api.text().await.unwrap(); + assert!( + !api_body.contains("onion_discovery"), + "status Host must not dump operator system JSON: {api_body}" + ); + + let console = client + .get(format!("http://{addr}/")) + .header("Host", "services.example.test") + .send() + .await + .unwrap(); + let console_body = console.text().await.unwrap(); + assert!( + !console_body.contains("data-surface=\"systems-status\""), + "services Host must not serve the public systems dashboard" + ); + + let other = client + .get(format!("http://{addr}/")) + .header("Host", "mail.example.test") + .send() + .await + .unwrap(); + let other_body = other.text().await.unwrap(); + assert!( + !other_body.contains("data-surface=\"systems-status\""), + "mail Host must not map to the systems dashboard" + ); + + serve.abort(); + let _ = serve.await; + } + /// Named contract: primary edge apex/www serve UNDER CONSTRUCTION; services keeps console. #[tokio::test] async fn primary_edge_apex_serves_document_root_smoke() { @@ -5626,6 +5758,10 @@ mod edge_wire_tests { hosts.contains(&"mta-sts.example.test"), "auto-map dump must include mta-sts: {hosts:?}" ); + assert!( + hosts.contains(&"status.example.test"), + "auto-map dump must include systems dashboard Host: {hosts:?}" + ); serve.abort(); let _ = serve.await; } diff --git a/crates/management-ui/src/onion_discovery.rs b/crates/management-ui/src/onion_discovery.rs index 756732a..9214959 100644 --- a/crates/management-ui/src/onion_discovery.rs +++ b/crates/management-ui/src/onion_discovery.rs @@ -262,8 +262,9 @@ fn sanitize_protocols(protocols: Vec) -> Vec { /// Public HTTP Hosts that may receive a per-site v3 onion. /// -/// Apex, `www.{apex}`, services, `mta-sts.{apex}`, plus extra static Hosts. -/// Never mail. Never onion names. De-duplicated, insertion order. +/// Apex, `www.{apex}`, services, `mta-sts.{apex}`, `status.{apex}`, plus +/// extra static Hosts. Never mail. Never onion names. De-duplicated, +/// insertion order. #[cfg(test)] pub fn auto_clearnet_hosts( primary_domain: &str, @@ -300,6 +301,10 @@ pub fn auto_clearnet_hosts_excluding_mail( if !out.iter().any(|h| h == &mta) { out.push(mta); } + let status = format!("status.{apex}"); + if !out.iter().any(|h| h == &status) { + out.push(status); + } } for raw in extra_static_hosts { let host = normalize_clearnet_host(raw.as_ref()); @@ -972,6 +977,7 @@ mod tests { "www.example.test".to_string(), "services.example.test".to_string(), "mta-sts.example.test".to_string(), + "status.example.test".to_string(), ] ); } @@ -986,6 +992,7 @@ mod tests { assert!(hosts.contains(&"extra.test".to_string())); assert!(hosts.contains(&"www.extra.test".to_string())); assert!(hosts.contains(&"mta-sts.example.test".to_string())); + assert!(hosts.contains(&"status.example.test".to_string())); assert!( !hosts.contains(&"mail.example.test".to_string()), "mail is not a public site and must not auto-map: {hosts:?}" @@ -1345,6 +1352,7 @@ mod tests { assert!(hosts.contains(&"www.example.test")); assert!(hosts.contains(&"services.example.test")); assert!(hosts.contains(&"mta-sts.example.test")); + assert!(hosts.contains(&"status.example.test")); assert!(!hosts.contains(&"mail.example.test")); } diff --git a/crates/management-ui/src/redirect.rs b/crates/management-ui/src/redirect.rs index b670cc0..21f3fe5 100644 --- a/crates/management-ui/src/redirect.rs +++ b/crates/management-ui/src/redirect.rs @@ -108,6 +108,29 @@ pub fn is_services_host(host_header: &str, services_hostname: &str) -> bool { host_for_url_authority(host_header).eq_ignore_ascii_case(&services) } +/// Derived systems-dashboard Host: `status.{primary}`. +/// +/// Empty primary => empty string. Production primary `surmount.systems` +/// yields `status.surmount.systems`. +pub fn status_hostname(primary_domain: &str) -> String { + let primary = host_for_url_authority(primary_domain); + if primary.is_empty() { + return String::new(); + } + format!("status.{}", primary.to_ascii_lowercase()) +} + +/// True when Host is the public systems dashboard (`status.{primary}`). +/// +/// Case-insensitive DNS labels; port stripped. Empty primary => never true. +pub fn is_status_host(host_header: &str, primary_domain: &str) -> bool { + let want = status_hostname(primary_domain); + if want.is_empty() { + return false; + } + host_for_url_authority(host_header).eq_ignore_ascii_case(&want) +} + /// Which public product surface a request Host maps to. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HostSurface { @@ -117,6 +140,8 @@ pub enum HostSurface { StaticVhost, /// Services hostname: operator management console. ServicesConsole, + /// `status.{primary}`: public systems dashboard (not operator console). + SystemsStatus, /// Other Host (mail, mta-sts, lab IP, empty, unknown): no public remap. Other, } @@ -157,7 +182,8 @@ pub fn host_surface( /// Classify Host including extra static vhosts (Host -> document root map). /// /// Services wins over everything. Apex/www stay ApexPublic even if listed in -/// the extra map (do not overload those names). Unknown Host stays Other +/// the extra map (do not overload those names). `status.{primary}` is +/// SystemsStatus even if listed as a static vhost. Unknown Host stays Other /// (no static leak onto the console). pub fn classify_host_surface( host_header: &str, @@ -171,6 +197,9 @@ pub fn classify_host_surface( if is_apex_or_www(host_header, primary_domain) { return HostSurface::ApexPublic; } + if is_status_host(host_header, primary_domain) { + return HostSurface::SystemsStatus; + } if static_vhost_mapped(host_header, static_vhosts) { return HostSurface::StaticVhost; } @@ -200,6 +229,14 @@ pub fn is_public_static_surface(surface: HostSurface) -> bool { matches!(surface, HostSurface::ApexPublic | HostSurface::StaticVhost) } +/// True when this surface is public and must not use operator-console CSP. +pub fn is_public_non_console_surface(surface: HostSurface) -> bool { + matches!( + surface, + HostSurface::ApexPublic | HostSurface::StaticVhost | HostSurface::SystemsStatus + ) +} + /// Paths that stay available on apex/www (health probes, ACME/MTA-STS well-known). /// /// Everything else on apex/www is the public COMING SOON page (HTML) or a @@ -691,6 +728,75 @@ mod tests { host_surface("evil.example", "example.test", "services.example.test"), HostSurface::Other ); + assert_eq!( + host_surface( + "status.example.test", + "example.test", + "services.example.test" + ), + HostSurface::SystemsStatus + ); + assert_eq!( + host_surface( + "status.surmount.systems", + "surmount.systems", + "services.surmount.systems" + ), + HostSurface::SystemsStatus + ); + } + + /// Named contract: dashboard Host maps to SystemsStatus, not Other/console. + #[test] + fn host_surface_status_is_systems_status() { + assert_eq!( + status_hostname("surmount.systems"), + "status.surmount.systems" + ); + assert_eq!(status_hostname("Example.TEST"), "status.example.test"); + assert!(is_status_host("STATUS.example.test:443", "example.test")); + assert!(!is_status_host("services.example.test", "example.test")); + let mut map = BTreeMap::new(); + map.insert( + "status.example.test".into(), + std::path::PathBuf::from("/var/lib/surmount/static-sites/wrong"), + ); + assert_eq!( + classify_host_surface( + "status.example.test", + "example.test", + "services.example.test", + &map + ), + HostSurface::SystemsStatus + ); + assert_eq!( + classify_host_surface( + "status.example.test", + "example.test", + "services.example.test", + &BTreeMap::new() + ), + HostSurface::SystemsStatus + ); + assert_ne!( + classify_host_surface( + "status.example.test", + "example.test", + "services.example.test", + &BTreeMap::new() + ), + HostSurface::Other + ); + assert_ne!( + classify_host_surface( + "status.example.test", + "example.test", + "services.example.test", + &BTreeMap::new() + ), + HostSurface::ServicesConsole + ); } /// Named contract: extra static Host is StaticVhost; services/apex win; diff --git a/crates/management-ui/src/status.rs b/crates/management-ui/src/status.rs new file mode 100644 index 0000000..ea058d6 --- /dev/null +++ b/crates/management-ui/src/status.rs @@ -0,0 +1,295 @@ +//! Public systems dashboard for Host `status.{primary}` (Axum, not nginx). +//! +//! Not the operator console. Payload fields come from process config +//! (listen mode, mail hostname present, MTA-STS mode). No RAM, disk, IP, +//! onion, or other live host facts. + +use axum::http::{StatusCode, header}; +use axum::response::{IntoResponse, Response}; +use serde::Serialize; + +use crate::config::AppConfig; +use crate::mta_sts::MtaStsMode; +use crate::redirect::status_hostname; + +/// One named systems row the dashboard emits (name/role/state, not live facts). +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SystemsComponent { + pub name: &'static str, + pub role: &'static str, + pub state: String, +} + +/// Representative systems status body. Tests assert these field names exist. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct SystemsStatusPayload { + pub surface: &'static str, + pub service: &'static str, + pub version: &'static str, + pub host: String, + pub primary_domain: String, + pub systems: Vec, +} + +/// Build the payload the status Host handler actually emits. +pub fn systems_status_from_config(config: &AppConfig) -> SystemsStatusPayload { + let host = status_hostname(&config.primary_domain); + let edge_state = if config.listen_mode.is_https() { + "https" + } else { + "http" + }; + let tls_state = if config.listen_mode.is_https() { + "leaf" + } else { + "plain_http" + }; + let mail_state = if config.mail_hostname.trim().is_empty() { + "unset" + } else { + "configured" + }; + let mta_state = match config.mta_sts_mode { + MtaStsMode::Off => "off", + MtaStsMode::Testing => "testing", + MtaStsMode::Enforce => "enforce", + }; + let console_state = if config.services_hostname.trim().is_empty() { + "unset" + } else { + "services_hostname" + }; + SystemsStatusPayload { + surface: "systems_status", + service: "surmount", + version: env!("CARGO_PKG_VERSION"), + host, + primary_domain: config.primary_domain.clone(), + systems: vec![ + SystemsComponent { + name: "mail", + role: "smtp_imap", + state: mail_state.to_string(), + }, + SystemsComponent { + name: "edge", + role: "https_axum", + state: edge_state.to_string(), + }, + SystemsComponent { + name: "tls", + role: "certificate", + state: tls_state.to_string(), + }, + SystemsComponent { + name: "mta_sts", + role: "policy", + state: mta_state.to_string(), + }, + SystemsComponent { + name: "console", + role: "operator", + state: console_state.to_string(), + }, + ], + } +} + +/// Serve HTML dashboard, JSON (`/status.json` or Accept), or closed 404. +pub fn serve_systems_status(config: &AppConfig, path: &str, accept: Option<&str>) -> Response { + let path_only = path.split('?').next().unwrap_or(path); + let payload = systems_status_from_config(config); + let want_json = path_only == "/status.json" + || (path_only == "/" + && accept.is_some_and(|a| a.to_ascii_lowercase().contains("application/json"))); + if path_only == "/status.json" || (path_only == "/" && want_json) { + let body = serde_json::to_string(&payload).unwrap_or_else(|_| { + r#"{"surface":"systems_status","service":"surmount","systems":[]}"#.into() + }); + return ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/json; charset=utf-8")], + body, + ) + .into_response(); + } + if path_only == "/" { + return ( + StatusCode::OK, + [(header::CONTENT_TYPE, "text/html; charset=utf-8")], + render_systems_status_html(&payload), + ) + .into_response(); + } + (StatusCode::NOT_FOUND, "not found").into_response() +} + +fn html_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +/// Public HTML dashboard. Named systems fields appear in the body. +pub fn render_systems_status_html(payload: &SystemsStatusPayload) -> String { + let host = html_escape(&payload.host); + let primary = html_escape(&payload.primary_domain); + let version = html_escape(payload.version); + let mut rows = String::new(); + for c in &payload.systems { + let name = html_escape(c.name); + let role = html_escape(c.role); + let state = html_escape(&c.state); + rows.push_str(&format!( + "{name}{role}\ +{state}\n" + )); + } + format!( + r#" + + + + + + +Surmount systems status · {host} + + + +
+

Surmount

+

Systems status

+

{host}

+

Primary domain {primary}. Public systems dashboard on the Axum HTTPS edge. Not the operator console.

+ + + +{rows} +
systemrolestate
+
+
surmount {version} · surface systems_status · service surmount
+ + +"# + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::AppConfig; + + /// Named contract: payload is non-empty and names the systems the handler emits. + #[test] + fn systems_status_payload_emits_named_fields() { + let cfg = AppConfig::example_test(); + let payload = systems_status_from_config(&cfg); + assert_eq!(payload.surface, "systems_status"); + assert_eq!(payload.service, "surmount"); + assert!(!payload.version.is_empty()); + assert_eq!(payload.host, "status.example.test"); + assert_eq!(payload.primary_domain, "example.test"); + assert!( + !payload.systems.is_empty(), + "systems list must be non-empty" + ); + let names: Vec<&str> = payload.systems.iter().map(|c| c.name).collect(); + for must in ["mail", "edge", "tls", "mta_sts", "console"] { + assert!( + names.contains(&must), + "handler must emit system {must}; got {names:?}" + ); + } + let html = render_systems_status_html(&payload); + assert!(html.contains("data-surface=\"systems-status\"")); + assert!(html.contains("Systems status")); + assert!(html.contains("status.example.test")); + assert!(html.contains("data-system=\"mail\"")); + assert!(html.contains("data-system=\"edge\"")); + assert!(html.contains("data-system=\"tls\"")); + assert!(!html.to_ascii_lowercase().contains("operator console nav")); + assert!(!html.contains("/mail")); + assert!(!html.contains("Dashboard Overview")); + let json = serde_json::to_value(&payload).unwrap(); + assert_eq!(json["surface"], "systems_status"); + assert!(json["systems"].as_array().unwrap().len() >= 5); + } + + /// Named contract: GET / calls the shipped handler and is 200 non-empty. + #[tokio::test] + async fn serve_systems_status_root_is_200_from_real_handler() { + let cfg = AppConfig::example_test(); + let payload = systems_status_from_config(&cfg); + let resp = serve_systems_status(&cfg, "/", None); + assert_eq!(resp.status(), StatusCode::OK); + let bytes = axum::body::to_bytes(resp.into_body(), 64 * 1024) + .await + .unwrap(); + let body = String::from_utf8(bytes.to_vec()).unwrap(); + assert!(!body.is_empty(), "systems status body must be non-empty"); + assert_eq!(body, render_systems_status_html(&payload)); + assert!(body.contains(&payload.host)); + assert!(body.contains("data-surface=\"systems-status\"")); + assert!(!body.contains("Dashboard Overview")); + } + + /// Named contract: JSON is the handler payload, not a hardcoded live blob. + #[tokio::test] + async fn serve_systems_status_json_matches_handler_payload() { + let cfg = AppConfig::example_test(); + let expected = serde_json::to_value(systems_status_from_config(&cfg)).unwrap(); + let resp = serve_systems_status(&cfg, "/status.json", None); + assert_eq!(resp.status(), StatusCode::OK); + let bytes = axum::body::to_bytes(resp.into_body(), 64 * 1024) + .await + .unwrap(); + assert!(!bytes.is_empty(), "JSON body must be non-empty"); + let got: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(got, expected, "JSON must be the live handler payload"); + assert_eq!(got["surface"], "systems_status"); + assert!(got["systems"].as_array().unwrap().len() >= 5); + } + + /// Named contract: operator console API on the status Host is closed 404. + #[tokio::test] + async fn serve_systems_status_console_api_is_404() { + let cfg = AppConfig::example_test(); + let resp = serve_systems_status(&cfg, "/api/v1/system", None); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} diff --git a/crates/surmount-acme-namecheap/src/laptop.rs b/crates/surmount-acme-namecheap/src/laptop.rs index 9e3c9b3..d105718 100644 --- a/crates/surmount-acme-namecheap/src/laptop.rs +++ b/crates/surmount-acme-namecheap/src/laptop.rs @@ -46,10 +46,11 @@ needs acme-dns-hook-namecheap-dispatch. When --hook is omitted and --domains includes a zone besides surmount.systems, wrap defaults to that dispatcher (sibling of this binary when present). -Host-profile without --domains floors the intended 20-name leaf (includes -exophiles.org and www.exophiles.org) and strips Cloudflare extras -(btcdragonlord.com, btckitties.com). Production --issue that names any -intended extra zone must include the full 20-name list (no silent drop). +Host-profile without --domains floors the intended leaf (includes +status.surmount.systems, exophiles.org, and www.exophiles.org) and +strips Cloudflare extras (btcdragonlord.com, btckitties.com). +Production --issue that names any intended extra zone must include +the full intended list (no silent drop). "#; /// Intended production leaf (web + mail). One PEM, with_single_cert. @@ -59,6 +60,7 @@ const INTENDED_LEAF_DOMAINS: &[&str] = &[ "mail.surmount.systems", "services.surmount.systems", "mta-sts.surmount.systems", + "status.surmount.systems", "baxterartworks.com", "www.baxterartworks.com", "btcfur.com", @@ -352,7 +354,7 @@ where } } if domains.is_empty() { - domains = "services.surmount.systems,mail.surmount.systems,surmount.systems,www.surmount.systems,mta-sts.surmount.systems".into(); + domains = "services.surmount.systems,mail.surmount.systems,surmount.systems,www.surmount.systems,mta-sts.surmount.systems,status.surmount.systems".into(); } if issue || live { refuse_dropped_intended_leaf(&domains)?; @@ -818,7 +820,7 @@ fn refuse_dropped_intended_leaf(domains: &str) -> anyhow::Result<()> { return Ok(()); } Err(blocked(format!( - "production extra-zone issue must keep the intended 20-name leaf (missing {}); do not drop Namecheap static zones such as exophiles.org", + "production extra-zone issue must keep the intended leaf (missing {}); do not drop Namecheap static zones such as exophiles.org or status.surmount.systems", missing.join(",") ))) } diff --git a/crates/surmount-acme-namecheap/testdata/host-profile/sample-host-profile.toml b/crates/surmount-acme-namecheap/testdata/host-profile/sample-host-profile.toml index b55673d..dd1593b 100644 --- a/crates/surmount-acme-namecheap/testdata/host-profile/sample-host-profile.toml +++ b/crates/surmount-acme-namecheap/testdata/host-profile/sample-host-profile.toml @@ -9,4 +9,5 @@ acme_domains = [ "mail.example.invalid", "example.invalid", "www.example.invalid", + "status.example.invalid", ] diff --git a/crates/surmount-acme-namecheap/tests/cli.rs b/crates/surmount-acme-namecheap/tests/cli.rs index cbcc81b..0e5a872 100644 --- a/crates/surmount-acme-namecheap/tests/cli.rs +++ b/crates/surmount-acme-namecheap/tests/cli.rs @@ -316,6 +316,7 @@ fn render_sample_and_refuse() { assert!(text.contains("services.example.invalid")); assert!(text.contains("mail.example.invalid")); assert!(text.contains("www.example.invalid")); + assert!(text.contains("status.example.invalid")); assert!(text.contains("ops@example.invalid")); assert!(text.contains("acme-staging-v02.api.letsencrypt.org")); assert!(text.contains("dnsProvider = \"external-hook\"")); @@ -612,7 +613,9 @@ fn laptop_issue_wrap_execs_stored_hook_not_one_sld_only() { fs::set_permissions(&fake_ui, fs::Permissions::from_mode(0o755)).unwrap(); }; let one_sld_exec = "exec acme-dns-hook-namecheap \"$@\""; - let extra_domains = "surmount.systems,www.surmount.systems,mail.surmount.systems,services.surmount.systems,mta-sts.surmount.systems,baxterartworks.com,www.baxterartworks.com,btcfur.com,www.btcfur.com,exophiles.org,www.exophiles.org,iantuckerstudios.com,www.iantuckerstudios.com,nostrfurs.com,www.nostrfurs.com,yiffa.app,www.yiffa.app,cryptoquick.com,www.cryptoquick.com,mail.cryptoquick.com"; + // Full intended leaf so extra-zone --issue is not refused for a dropped + // dashboard Host. The wrap contract is dispatcher exec, not omit-status. + let extra_domains = "surmount.systems,www.surmount.systems,mail.surmount.systems,services.surmount.systems,mta-sts.surmount.systems,status.surmount.systems,baxterartworks.com,www.baxterartworks.com,btcfur.com,www.btcfur.com,exophiles.org,www.exophiles.org,iantuckerstudios.com,www.iantuckerstudios.com,nostrfurs.com,www.nostrfurs.com,yiffa.app,www.yiffa.app,cryptoquick.com,www.cryptoquick.com,mail.cryptoquick.com"; let stored = PathBuf::from(dispatch()); let issue_hook = root.join("issue-hook"); @@ -757,7 +760,7 @@ fn laptop_issue_fails_when_ui_child_exits_nonzero() { } /// Host-profile Cloudflare extras must not replace Namecheap static zones. -/// Profile without --domains floors the intended 20-name leaf. +/// Profile without --domains floors the intended leaf (status + extras). #[test] fn host_profile_floors_intended_leaf_and_strips_cloudflare_extras() { let root = scratch("laptop-profile-floor"); @@ -793,6 +796,10 @@ fn host_profile_floors_intended_leaf_and_strips_cloudflare_extras() { t.contains("exophiles.org") && t.contains("www.exophiles.org"), "host-profile must floor exophiles even when the profile omitted them: {t}" ); + assert!( + t.contains("status.surmount.systems"), + "host-profile must floor systems dashboard Host: {t}" + ); assert!( !t.contains("btcdragonlord.com") && !t.contains("btckitties.com"), "host-profile must strip Cloudflare extras: {t}" diff --git a/crates/surmount-host-cutover/testdata/host-profile/sample-host-profile.toml b/crates/surmount-host-cutover/testdata/host-profile/sample-host-profile.toml index b55673d..dd1593b 100644 --- a/crates/surmount-host-cutover/testdata/host-profile/sample-host-profile.toml +++ b/crates/surmount-host-cutover/testdata/host-profile/sample-host-profile.toml @@ -9,4 +9,5 @@ acme_domains = [ "mail.example.invalid", "example.invalid", "www.example.invalid", + "status.example.invalid", ] diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index a9babf6..c708ae3 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -91,6 +91,7 @@ flake-owned host. |-------|------| | Primary domain | `surmount.systems` | | Management UI host | `services.surmount.systems` | +| Systems dashboard host | `status.surmount.systems` (Axum, not nginx) | | Mail host / MX | `mail.surmount.systems` | | Form factor | **Single operator-chosen VPS** until the operator says otherwise | | Public awareness | Treat the tree as **public-domain / public**. Zero secrets in git. | diff --git a/docs/EDGE_AND_TLS.md b/docs/EDGE_AND_TLS.md index 0080a0d..fcec432 100644 --- a/docs/EDGE_AND_TLS.md +++ b/docs/EDGE_AND_TLS.md @@ -4,7 +4,10 @@ How HTTPS reaches the management UI and (optionally) Stalwart HTTP. Mail protocol ports are **not** edge-proxied; they terminate on Stalwart. See [STACK.md](STACK.md) for the full path map. -**Last updated:** 2026-09-22 (one public Host `splora.surmount.systems`. +**Last updated:** 2026-10-23 (Host `status.surmount.systems` is the public +systems dashboard on the product Axum HTTPS edge, not nginx and not the +operator console. Intended certificate hostname list includes that name.) +Prior 2026-09-22 (one public Host `splora.surmount.systems`. Network paths on that Host, not per-network esplora Hosts. The network prefix is stripped before the Unix socket. The query string is kept. `/{network}/api/v1/ws` (mainnet: `/api/v1/ws`) is the WebSocket upgrade. @@ -286,8 +289,18 @@ nginx features. Module file remains until operators no longer need dual-run. nonce CSP. Open-redirect safe: empty allowlist or non-listed Host never builds a Location from untrusted input. Default allowlist: services, apex, www, - mail, mta-sts., plus any extra static Hosts from + mail, mta-sts., status., plus any extra static Hosts from `SURMOUNT_STATIC_VHOSTS` / `SURMOUNT_STATIC_VHOSTS_FILE`. +- **Systems dashboard Host (2026-10-23):** `status.{primaryDomain}` + (production: `status.surmount.systems`) is a public systems status + surface on this same Axum process. GET `/` is HTML; GET `/status.json` + is JSON. Named fields: `surface`, `service`, `version`, `host`, + `primary_domain`, and `systems` rows `mail`, `edge`, `tls`, `mta_sts`, + `console` (config-derived states, not RAM/disk/IP). Not the operator + console. `/health` and `/api/health` stay edge probes. Other `/api/*` + is 404. Laptop DNS-01 intended leaf includes `status.surmount.systems`. + Live HTTPS after switch is operator-owned. Do not put nginx back as + product edge. - **Extra static Hosts (DS3018xs trees, 2026-08-19):** proven public HTML sites are served from the same Axum process via a Host -> document root map (`SURMOUNT_STATIC_VHOSTS` JSON, or Nix @@ -297,7 +310,8 @@ nginx features. Module file remains until operators no longer need dual-run. never the operator console. Do **not** overload `SURMOUNT_APEX_PUBLIC_ROOT`. `services.surmount.systems` stays the operator console. Extra Hosts each get a distinct v3 onion (2026-09-11), not a shared onion. - Onion-Location for extra Hosts, apex/www, and `mta-sts.{primary}` is + Onion-Location for extra Hosts, apex/www, `mta-sts.{primary}`, and + `status.{primary}` is `http://{that-host-onion}{path}` (empty path becomes `/`). The services onion root stays the console. Mail stays unmapped. Do not put nginx back as product edge. diff --git a/docs/OPS.md b/docs/OPS.md index 464e34a..34db7df 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -4,7 +4,8 @@ How the Surmount mail VPS should be operated, observed, and checked end-to-end. Aligned with [hygiene.md](hygiene.md). Architecture: [STACK.md](STACK.md). -**Last updated:** 2026-10-10 (primary SPF is +**Last updated:** 2026-10-23 (grok-oss and tau are not mail-host +packages; do not enable them from host-local). Prior 2026-10-10 (primary SPF is `v=spf1 a:mail.surmount.systems include:_spf.google.com -all`. DMARC stays `p=quarantine`. Detail: [DNS.md](DNS.md) section *Google send (operator direction 2026-10-10)*). @@ -958,9 +959,7 @@ missing system features. `extra-` keeps NixOS auto-detected `surmount-remote`. After a module change, rebuild and switch this host (`just deploy-host` or host `nixos-rebuild switch`) so `/etc/nix/nix.conf` picks up the extra feature; then restart is -implied by the switch. `/var/cache/grok-oss-cargo-target` is an extra -sandbox path (`nix.settings.extra-sandbox-paths` on the mail host) so -sandboxed builds can see that cargo cache. Do not advertise a fake +implied by the switch. Do not advertise a fake high slot count. Speed factor is not a substitute for `MemoryMax`. Disk refuse at `diskGuardPercent` (default 95). Enable from private host-local. Host-local may set a @@ -1796,57 +1795,16 @@ just et -- --target root@surmount-1 # or: nix run .#surmount-et -- --target root@surmount-1 ``` -Remote grok-oss attach is SSH plus tmux as user grok (`just grok-oss` / -`nix run .#surmount-grok-oss`): `ssh -t` then `runuser -u grok` so -`tmux attach -t grok-oss` or `tmux new -s grok-oss grok-oss` runs as that -user (module uid, default grok). Root tmux is a miss: `user-1988.slice` -MemoryMax applies only when the process uid is grok. It is **not** Eternal -Terminal (`et -p 2022`). Nested guest `just grok-oss` attaches that same -local tmux session as user grok and does not SSH to itself. `just et -- --status` -prints whether TCP 22 and TCP 2022 answer, without addresses. If 2022 does not -answer, that status tells you to use SSH/tmux (`just grok-oss`). - -Guest grok-oss on surmount-1 uses a **machine xAI console API key**. That -key spends **console API credits** (console team prepaid). It is **not** -included SuperGrok period limits. It is **not** SuperGrok dollar credits. -Path only (never key bytes in git): `/home/grok/.grok/console.env` -(`XAI_API_KEY`, mode 0600) and `/home/grok/.grok/machine-console-auth.toml` -(`[auth] preferred_method = "api_key"`; that file does not hold the key). -The guest has **no git** and **no GitHub SSH**. Signed commits stay on -the laptop. Laptop L0 (`surmount-coordinator-gui -set-remote-host-console-api-key`) lives in grok-oss `remote-1`; the -operator already tasked that tree. Dual-pin [SECRETS.md](SECRETS.md). - -Grok OSS has three distinct surfaces. Do not merge them. - -The pager `/dashboard` (also `grok-oss dashboard`, or Ctrl+backslash in the TUI) is -the Agent Dashboard inside **this** grok-oss TUI process. It is not a web -page, not management-ui, and not L0. - -`/running` (alias `/windows`, CLI `grok-oss running` and -`grok-oss running --json`) lists live TUI windows on **this** `$GROK_HOME` -from `active_sessions.json`. That list is not L0. `just grok-oss-running` -SSHes to the mail host and prints that JSON as user grok. It does not -allocate a TTY, does not bind HTTP, and does not print secrets. - -L0 is the laptop Surmount coordinator GUI (`surmount-coordinator-gui`) -tagging `grok-oss running --json` with a host name such as surmount-1. -It does not bind HTTP and it does not listen on the mail host. The guest -`grok-oss` package does not install that GUI. Do not serve L0 on -management-ui or on :443. - -`surmount.grokOss.enable` stays default false. Enable only from private -host-local. The module does not start a boot TUI unit. Do not set `Nice=` -on grok-oss (sshd class). Flake input grok-oss is commit pin -`6703706cc7419ba71af549c1562017f302173c19` on -`github:SurmountSystems/grok-oss`. Prior lock was -`525538172dc21391bc690a4cc4a8706a5c554e47`. Earlier lock was -`182293cb30fa5414dd47686b92708103519b7462`. Earlier lock was -`decb052744d25b63dbca8a95ca65b4ad794b9ad5`. Operator 2026-10-14 locked -`b7ab91532c9e8008081b4f281794be43ecae73a6` (HB-1 merged as GitHub pull -167, crate version 1.0.45). Operator bumps: -`nix flake update grok-oss`. After a host switch that includes this -package, attach with `just grok-oss` as user grok. +`just et -- --status` prints whether TCP 22 and TCP 2022 answer, without +addresses. + +Grok OSS and tau are **not** mail-host packages. This flake does not +take those inputs. `surmount.grokOss.enable` and `surmount.tau.enable` +stay false. Named eval fails if either is true or if those packages +appear in `environment.systemPackages`. Do not re-enable them from +host-local. Do not install `pkgs.grok-oss` or `pkgs.tau` on the guest. +Laptop grok-oss (the operator TUI on this machine) is a different +tree. Do not put it back on the mail host. For a rebuild that must finish even if every laptop tunnel dies, SSH or `just et` in, then `tmux new -s switch` and run the command inside tmux. @@ -1888,8 +1846,6 @@ exceptions*. | `nix run .#surmount-private-data` / `just check-private-data` | Private-data pattern scan (`--staged` / `--tree` / `--paths`). | | `nix run .#surmount-host-logs` / `just host-logs` | Host journal follow and `--status`. | | `nix run .#surmount-et` / `just et` | Eternal Terminal client. Reconnects after sleep and network change. Does not replace Mullvad. After the host switch, `etserver` listens on TCP 2022. Long commands still belong in tmux on the guest. | -| `nix run .#surmount-grok-oss` / `just grok-oss` | Attach grok-oss on the mail host via SSH + tmux as user grok. Not Eternal Terminal. Nested guest is local tmux as grok. Live TTY required. MemoryMax only if uid grok. | -| `just grok-oss-running` | SSH (or local on the guest) `grok-oss running --json` as user grok. No TTY, no HTTP bind, no secrets. Input for laptop L0 tagging, not a dashboard. | | `nix run .#surmount-rekey` / `just rekey` | SHC Backups PGP paste: mint age identity (age/rage crate), wrap with gpg, print `age1...` for type PGP. Identity stays 0600 on disk. | | `nix run .#surmount-shc` / `just rdns-shc` | SHC customer user-api (rDNS PTR + tickets). No python3 in the crate. | | `nix run .#surmount-tls-hybrid` / `just check-tls-hybrid` | **D1** host hybrid TLS negotiation probe after B1. Requires `SURMOUNT_E2E_BASE_URL=https://...` or exit **2** BLOCKED. Never a flake check. | diff --git a/docs/SEARCH_AND_UI.md b/docs/SEARCH_AND_UI.md index 62a9857..54fd82b 100644 --- a/docs/SEARCH_AND_UI.md +++ b/docs/SEARCH_AND_UI.md @@ -119,8 +119,9 @@ the Leptos app. Live apex/www serve the packaged `surmount.systems` / `www.surmount.systems`). `just publish` publishes static sites; `just deploy` is the NixOS generation (the host switch). `just deploy-host` is that switch with no default target. -Operator console stays on `services.surmount.systems`. COMING SOON -leftover is closed. +Operator console stays on `services.surmount.systems`. Public systems +dashboard is Host `status.surmount.systems` on the same Axum edge. +COMING SOON leftover is closed. ## Phases @@ -147,7 +148,8 @@ leftover is closed. **Discovery headers (2026-08-17, every public Host 2026-08-20; per-site v3 2026-09-11):** Onion-Location + Alt-Svc on mapped HTTPS 2xx/3xx. Map: apex, www, - services, `mta-sts.{apex}`, extra static Hosts; one v3 per Host. + services, `mta-sts.{apex}`, `status.{apex}`, extra static Hosts; one + v3 per Host. Onion-Location is `{that-host-onion}{path}`. Mail unmapped. Mapping loaded at process start; restart the unit after hostname/env/map/static-vhost change (no hot-reload). Optional env: `SURMOUNT_ONION_LOCATION_ENABLED`, diff --git a/docs/SECRETS.md b/docs/SECRETS.md index edbf36c..88416fa 100644 --- a/docs/SECRETS.md +++ b/docs/SECRETS.md @@ -222,7 +222,7 @@ render into private host-local. | Store | Holds | Does not hold | |-------|-------|----------------| -| **Host profile** (e.g. `~/.local/share/surmount/host-profile.toml`) | `acme_domains` (certificate hostnames: services + mail + apex + www + mta-sts), `acme_email`, `acme_directory` (staging default; production only with explicit flag or profile field), optional `host_id`, path overrides | Namecheap ApiKey, PEMs, session/Stalwart/VW tokens | +| **Host profile** (e.g. `~/.local/share/surmount/host-profile.toml`) | `acme_domains` (certificate hostnames: services + mail + apex + www + mta-sts + status), `acme_email`, `acme_directory` (staging default; production only with explicit flag or profile field), optional `host_id`, path overrides | Namecheap ApiKey, PEMs, session/Stalwart/VW tokens | | **Secret Service / staging (Domain A)** | Secret **values** + labels (`namecheap-api`, `stalwart-token`, …) | Edge FQDNs as the SoT for Nix ACME (use profile / host-local) | | **Domain B files** | Installed secrets + ACME parents; hook reads `namecheap.env` | Git-tracked host config | | **Vaultwarden (Domain C)** | Human notes / optional copies after S7b | Activation; profile fields as boot SoT | diff --git a/docs/SECURITY.md b/docs/SECURITY.md index e5f6f58..8b881de 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -4,7 +4,11 @@ Defense-in-depth notes for the Surmount mail VPS. Design notes: [open-choices.md](open-choices.md). Secrets detail: [SECRETS.md](SECRETS.md). Data plane: [DATASTORES.md](DATASTORES.md). Ops: [OPS.md](OPS.md). -**Last updated:** 2026-08-26 (Cargo.lock SHA-1/MD5 inventory: no sha1/md5 +**Last updated:** 2026-10-24 (Cargo.lock git patches for ctutils 0.4.5, +rustls-webpki 0.103.16, webpki-root-certs 1.0.11, and i18n-config 0.4.8 +without basic-toml, because menhera 10d still stops at the unpatched +registry versions; do not ignore those advisories; do not fetch crates.io +to skip the cooldown). Prior 2026-08-26 (Cargo.lock SHA-1/MD5 inventory: no sha1/md5 crates; product TLS is rustls+aws-lc-rs TLS 1.3; ring is locked as a rustls optional dep). Prior 2026-08-20 (extra mailbox domains get the same mail-auth DNS **and** the same hosted DNSSEC as the primary; leftover @@ -65,6 +69,27 @@ RUSTSEC-2026-0173 is proc-macro-error2, not age. `instant` (RUSTSEC-2024-0384) left the lock with nostr 0.45 (universal-time, not instant). +On 2026-10-24, menhera 10d still stopped at ctutils 0.4.3, rustls-webpki +0.103.15, and webpki-root-certs 1.0.9. The lock uses git +`[patch.crates-io]` of ctutils tag `ctutils-v0.4.5` (rev +`88d6e90d4cfd2fc3333d00e13c99241507d2138f`) for +[RUSTSEC-2026-0385](https://rustsec.org/advisories/RUSTSEC-2026-0385) +(accessed: 2026-10-24), rustls-webpki tag `v/0.103.16` (rev +`8ecdcc76f56b68fda67a6adf7976f5eed5006a4c`) for +[RUSTSEC-2026-0382](https://rustsec.org/advisories/RUSTSEC-2026-0382) +(accessed: 2026-10-24), and webpki-root-certs tag `v/1.0.11` (rev +`7ebfa0b377fd0c7e0a2848f9d9b2867a4c2198cf`) for +[RUSTSEC-2026-0383](https://rustsec.org/advisories/RUSTSEC-2026-0383) +(accessed: 2026-10-24). i18n-config 0.4.8 on menhera still depends on +basic-toml ([RUSTSEC-2026-0374](https://rustsec.org/advisories/RUSTSEC-2026-0374), +accessed: 2026-10-24; unmaintained, no patched versions). The lock +patches i18n-config to kellpossible/cargo-i18n rev +`2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4`, still version 0.4.8, which +uses the toml crate. Do not ignore those four advisories. Do not fetch +crates.io to skip menhera. Drop each git line when 10d lists the +patched registry version. The only cargo-audit ignore stays +RUSTSEC-2024-0436 (`paste`). + On 2026-09-26 the lock replaced registry faster-hex 0.10.0 with git tag v0.10.1 (rev `c817979830ea5be2eafe0b0c99cb83b017ae2b8f`) for RUSTSEC-2026-0306. `faster_hex::hex_decode_unchecked` on x86 and x86_64, diff --git a/docs/TEST-AND-DEPLOY.md b/docs/TEST-AND-DEPLOY.md index a3f2b3c..ac99541 100644 --- a/docs/TEST-AND-DEPLOY.md +++ b/docs/TEST-AND-DEPLOY.md @@ -9,7 +9,8 @@ Those failures belong on the agent. The operator runs the real switch in section 5 after the agent has already reported those gates green, and keeps Eternal Terminal open so activation cannot strand the box. -**Last updated:** 2026-10-12. Update, audit, test, and switch is the +**Last updated:** 2026-10-23 (this flake does not take grok-oss or tau +inputs). Prior 2026-10-12. Update, audit, test, and switch is the command block below. `just publish` publishes static sites. `just deploy` is the host switch. `just deploy-dry-run` is the agent dry-run. `just deploy-host` stays the explicit equivalent. @@ -37,10 +38,9 @@ just deploy `nix flake update` moves branch inputs: nixpkgs `nixos-26.05`, `advisory-db`, Splora, and `surmount-site`. It does -not move commit-pinned `nixpkgs-rust`, `crane`, `sops-nix`, or grok-oss. -grok-oss is commit pin `6703706cc7419ba71af549c1562017f302173c19`, not -floating `main`. Prior pin was `525538172dc21391bc690a4cc4a8706a5c554e47`. Stalwart versions are the fixed-output files under -`nix/packages/`, not this command. +not move commit-pinned `nixpkgs-rust`, `crane`, or `sops-nix`. +This flake does not take grok-oss or tau inputs. Stalwart versions are +the fixed-output files under `nix/packages/`, not this command. `cargo update` uses the menhera index already configured on the laptop. `just audit` and `just audit-remote` run the same RustSec check. @@ -368,8 +368,8 @@ just deploy-host -- --target root@surmount-1 --host-local /home/hunter/.local/sh ``` That rsyncs, then runs `nixos-rebuild switch` on the guest. A generation -that compiles grok-oss or Splora can sit on one `building ...drv` line -for a long time. Check guest rustc before killing it. +that compiles Splora can sit on one `building ...drv` line for a long +time. Check guest rustc before killing it. After rebuild, the driver always runs `surmount-deploy-host-post-switch-smoke` on the guest, even if rebuild diff --git a/docs/packages-and-forks.md b/docs/packages-and-forks.md index 7fbd9b8..9c0bb7f 100644 --- a/docs/packages-and-forks.md +++ b/docs/packages-and-forks.md @@ -4,7 +4,7 @@ Plain English. No assumption that the reader already knows Fix ladder codes. Ladder detail still lives in [fix-and-fixos.md](fix-and-fixos.md) if you want depth. -**Last updated:** 2026-10-20 (grok-oss is commit pin `6703706cc7419ba71af549c1562017f302173c19`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Host channel stayed nixos-26.05. Wrapped `speedtest` is `pkgs.ookla-speedtest` plus license/GDPR flags. `scc` is the nixpkgs package.) Prior 2026-10-19 (grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. Host channel stayed nixos-26.05. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (nixpkgs-rust is commit `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1, `pkgs.arti` 2.7.0. grok-oss is commit pin `182293cb30fa5414dd47686b92708103519b7462`. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false.) Prior 2026-10-17 (nixpkgs-rust moved to nixpkgs master `c46e6c9a80d68a8b92e75a2d2cb078a8f6d8dd81`, rustc 1.99.0, `pkgs.arti` 2.7.0. Crane moved to `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`, one internal flake.lock commit after `47b6b27`. sops-nix stayed `44931708`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5` on `github:SurmountSystems/grok-oss`, not floating `main`. nixpkgs nixos-26.05 stayed `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora stayed `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db stayed `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. Stalwart server 0.16.26, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12 were still the latest GitHub tags. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-17 (nixpkgs-rust `93b590222c0327b3d952e8e30905e2bd5fed3293`, rustc 1.99.0, `pkgs.arti` 2.7.0. Stalwart server 0.16.26, vandelay 1.0.12. sops-nix `44931708`. Crane stayed `47b6b27`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5`, crate version still 1.0.45 in `xai-grok-version`. nixpkgs nixos-26.05 is `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora is `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db is `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-14 (grok-oss `main` moved to `b7ab91532c9e8008081b4f281794be43ecae73a6`, GitHub pull 167, crate version 1.0.45 in `xai-grok-version`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-10 (nixpkgs-rust moved off nixos-unstable to `c9f19b6be67332ce2ee7520c1630c804685df838`, rustc 1.99.0, `pkgs.arti` still 2.6.0. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. grok-oss was `github:SurmountSystems/grok-oss/main` at `d1d3b7377e85c760caf17b74cf6d319301a77d1e`. Crane stayed `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`.) Prior 2026-10-03 (flake lock moved: nixpkgs nixos-26.05 `774debe7a0d1b496e35677ad955a1011c6ff74f3`, nixpkgs-rust `c59305bab2065cfecc4944690d9eedbb56f3a9fa` with rustc 1.98.1 and `pkgs.arti` 2.6.0, advisory-db `ef6173cbc5c50ec8166f9a5b28f07834144373ee`, splora `32f52e8da3ae860a36d39bce334e80e7e624d5a8`, crane `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`, sops-nix `5efb5a6f4f5ab192817d28557dd4d650fa14d866`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. grok-oss stayed `6edf5fda9507c9e3c9c9f5a67871ebb3ce6cca7d` because branch `remote-1` is gone. Stalwart FODs are server 0.16.24, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.11. The Arti package stayed 2.6.0.) Prior 2026-09-19 (`surmount.sploraIndexer` is the host-local single-knob wrap over imported `nixosModules.splora`; flake input `splora` on the `surmount` branch, then locked rev `be3603dbd8e6ef0c24e37fe07beaf8067bfa2d0b`; prior lock `22d6dcf7f76c1cacc23220d80203099018cde3aa`; instance options already include cookieFile, daemonRpcAddr, jsonrpcImport, daemonDir = null, publicHealth, db cache 24, httpSocketFile default `/run/splora/${name}.http.sock`; this repo does not wrap crane src). Prior 2026-09-01 (this tree briefly wrapped crane src; that wrap is deleted). Prior 2026-09-01 (flake input `splora` from `github:SurmountSystems/splora` on the `surmount` branch; not in-tree). Prior 2026-08-27 (workspace clippy style allows in `crates/Cargo.toml`; flake clippy is `-D warnings` only. Overlay/packages/`just` still `nix run` only.) +**Last updated:** 2026-10-23 (grok-oss and tau are not mail-host packages. The flake does not take those inputs and does not wire `packages.grok-oss` or `packages.tau`. Default mail-vps does not enable `surmount.grokOss` or `surmount.tau`. Host-local must not re-enable them. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Host channel stayed nixos-26.05. Wrapped `speedtest` is `pkgs.ookla-speedtest` plus license/GDPR flags. `scc` is the nixpkgs package. `fio` and `sysstat` are nixpkgs packages on the mail-vps PATH.) Prior 2026-10-20 (grok-oss is commit pin `6703706cc7419ba71af549c1562017f302173c19`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Host channel stayed nixos-26.05. Wrapped `speedtest` is `pkgs.ookla-speedtest` plus license/GDPR flags. `scc` is the nixpkgs package.) Prior 2026-10-19 (grok-oss is commit pin `525538172dc21391bc690a4cc4a8706a5c554e47`. nixpkgs-rust stayed `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. Host channel stayed nixos-26.05. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (nixpkgs-rust is commit `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1, `pkgs.arti` 2.7.0. grok-oss is commit pin `182293cb30fa5414dd47686b92708103519b7462`. Crane stayed `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`. sops-nix stayed `44931708c62937c2b1bf7953902989fa5bb9ee1b`. tau stayed `d2e1955b0491f538a486007d0d3ab779d95a828f`. The package build uses `surmount-rust-1.99.1`.) Prior 2026-10-18 (flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false.) Prior 2026-10-17 (nixpkgs-rust moved to nixpkgs master `c46e6c9a80d68a8b92e75a2d2cb078a8f6d8dd81`, rustc 1.99.0, `pkgs.arti` 2.7.0. Crane moved to `bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a`, one internal flake.lock commit after `47b6b27`. sops-nix stayed `44931708`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5` on `github:SurmountSystems/grok-oss`, not floating `main`. nixpkgs nixos-26.05 stayed `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora stayed `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db stayed `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. Stalwart server 0.16.26, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12 were still the latest GitHub tags. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-17 (nixpkgs-rust `93b590222c0327b3d952e8e30905e2bd5fed3293`, rustc 1.99.0, `pkgs.arti` 2.7.0. Stalwart server 0.16.26, vandelay 1.0.12. sops-nix `44931708`. Crane stayed `47b6b27`. grok-oss is commit pin `decb052744d25b63dbca8a95ca65b4ad794b9ad5`, crate version still 1.0.45 in `xai-grok-version`. nixpkgs nixos-26.05 is `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8`. splora is `5164864cb7349fabc961b5e6c9da2829106bff5f`. advisory-db is `999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-14 (grok-oss `main` moved to `b7ab91532c9e8008081b4f281794be43ecae73a6`, GitHub pull 167, crate version 1.0.45 in `xai-grok-version`. The package build still uses `surmount-rust-1.99.0`.) Prior 2026-10-10 (nixpkgs-rust moved off nixos-unstable to `c9f19b6be67332ce2ee7520c1630c804685df838`, rustc 1.99.0, `pkgs.arti` still 2.6.0. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. grok-oss was `github:SurmountSystems/grok-oss/main` at `d1d3b7377e85c760caf17b74cf6d319301a77d1e`. Crane stayed `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`.) Prior 2026-10-03 (flake lock moved: nixpkgs nixos-26.05 `774debe7a0d1b496e35677ad955a1011c6ff74f3`, nixpkgs-rust `c59305bab2065cfecc4944690d9eedbb56f3a9fa` with rustc 1.98.1 and `pkgs.arti` 2.6.0, advisory-db `ef6173cbc5c50ec8166f9a5b28f07834144373ee`, splora `32f52e8da3ae860a36d39bce334e80e7e624d5a8`, crane `47b6b27ed9a3a9181415e4367d0c30ab2a0e0250`, sops-nix `5efb5a6f4f5ab192817d28557dd4d650fa14d866`. surmount-site stayed `82e07195e6aa221efe838e652ed8d2d8535b5cac`. grok-oss stayed `6edf5fda9507c9e3c9c9f5a67871ebb3ce6cca7d` because branch `remote-1` is gone. Stalwart FODs are server 0.16.24, cli 1.0.13, webui 1.0.11, spam-filter 3.0.2, and vandelay 1.0.11. The Arti package stayed 2.6.0.) Prior 2026-09-19 (`surmount.sploraIndexer` is the host-local single-knob wrap over imported `nixosModules.splora`; flake input `splora` on the `surmount` branch, then locked rev `be3603dbd8e6ef0c24e37fe07beaf8067bfa2d0b`; prior lock `22d6dcf7f76c1cacc23220d80203099018cde3aa`; instance options already include cookieFile, daemonRpcAddr, jsonrpcImport, daemonDir = null, publicHealth, db cache 24, httpSocketFile default `/run/splora/${name}.http.sock`; this repo does not wrap crane src). Prior 2026-09-01 (this tree briefly wrapped crane src; that wrap is deleted). Prior 2026-09-01 (flake input `splora` from `github:SurmountSystems/splora` on the `surmount` branch; not in-tree). Prior 2026-08-27 (workspace clippy style allows in `crates/Cargo.toml`; flake clippy is `-D warnings` only. Overlay/packages/`just` still `nix run` only.) **Operator direction:** [operator-direction.md](operator-direction.md) --- @@ -86,7 +86,14 @@ HTTP/3 stay on this-tree Axum. Do not start five indexers. Do not set `services.splora.enable` or `surmount.sploraIndexer.enable` on the public sample host. -Flake input tau is the operator-synced repo SurmountSystems/tau at rev d2e1955b0491f538a486007d0d3ab779d95a828f, and surmount.tau.enable defaults false. +Tau is not a mail-host package. This flake does not take a tau input +and does not overlay `pkgs.tau`. `surmount.tau.enable` defaults false +and eval fails if it is true. Do not re-enable from host-local. + +Grok OSS is not a mail-host package. This flake does not take a +grok-oss input and does not overlay `pkgs.grok-oss`. +`surmount.grokOss.enable` defaults false and eval fails if it is true. +Do not re-enable from host-local. ### Upstream crane omits Menhera config from Nix src (Splora) diff --git a/flake.lock b/flake.lock index bd7cc7f..a15d543 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "advisory-db": { "flake": false, "locked": { - "lastModified": 1792250639, - "narHash": "sha256-NsRcWUOIJ2Qy/aVB1oeSoUw5Q7PEQZuMNJxD5R+l5Nk=", + "lastModified": 1792766536, + "narHash": "sha256-IO2oOHRZ3yKjhBLzUYWsIV1gfwIrEZ77XDefHM98T3A=", "owner": "rustsec", "repo": "advisory-db", - "rev": "999ec89d5d3f10f20be14750ec8c2d8b44e0ac9b", + "rev": "84d943a6deeb8cc6c6f632531aaf4f1c62275239", "type": "github" }, "original": { @@ -16,6002 +16,189 @@ "type": "github" } }, - "android-nixpkgs": { - "inputs": { - "devshell": "devshell", - "flake-utils": [ - "tau", - "dpc-public-skills", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "dpc-public-skills", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - } - }, - "android-nixpkgs_10": { - "inputs": { - "devshell": "devshell_10", - "flake-utils": [ - "tau", - "tau-ext-swarm", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - } - }, - "android-nixpkgs_11": { - "inputs": { - "devshell": "devshell_11", - "flake-utils": "flake-utils_25", - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", - "type": "github" - } - }, - "android-nixpkgs_12": { - "inputs": { - "devshell": "devshell_12", - "flake-utils": [ - "tau", - "tau-ext-telegram", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - } - }, - "android-nixpkgs_13": { - "inputs": { - "devshell": "devshell_13", - "flake-utils": "flake-utils_30", - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", - "type": "github" - } - }, - "android-nixpkgs_14": { - "inputs": { - "devshell": "devshell_14", - "flake-utils": [ - "tau", - "tau-ext-xmpp", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - } - }, - "android-nixpkgs_15": { - "inputs": { - "devshell": "devshell_15", - "flake-utils": "flake-utils_35", - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", - "type": "github" - } - }, - "android-nixpkgs_16": { - "inputs": { - "devshell": "devshell_16", - "flake-utils": [ - "tau", - "tau-ext-zulip", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "flakebox", - "nixpkgs" - ] - }, + "asset-registry": { + "flake": false, "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "lastModified": 1789841305, + "narHash": "sha256-tQAI6b8iqBh9WNa1r+4cT1ZjlQTv1fq/JvGWsnRonQU=", + "owner": "Blockstream", + "repo": "asset_registry_db", + "rev": "95e9c0afb8c8d6798b921257c7bfc2b2863018bb", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "owner": "Blockstream", + "repo": "asset_registry_db", "type": "github" } }, - "android-nixpkgs_17": { - "inputs": { - "devshell": "devshell_17", - "flake-utils": "flake-utils_40", - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "selfci", - "flakebox", - "nixpkgs" - ] - }, + "crane": { "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "lastModified": 1791754810, + "narHash": "sha256-cN0Mgk+sQtCJcGWs9H4M0CnLRAuC4I8aCwiBTcBJs6E=", + "owner": "ipetkov", + "repo": "crane", + "rev": "bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "owner": "ipetkov", + "repo": "crane", + "rev": "bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a", "type": "github" } }, - "android-nixpkgs_2": { - "inputs": { - "devshell": "devshell_2", - "flake-utils": [ - "tau", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "flakebox", - "nixpkgs" - ] - }, + "crane_2": { "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "lastModified": 1789760033, + "narHash": "sha256-jtT4yxZpR8seYnlCMMWSSPlFN92zO6ICuZ8pJrmi86k=", + "owner": "ipetkov", + "repo": "crane", + "rev": "73b980519cefc727a5f6cc8e5c0947a2f9be6edd", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "owner": "ipetkov", + "repo": "crane", "type": "github" } }, - "android-nixpkgs_3": { - "inputs": { - "devshell": "devshell_3", - "flake-utils": "flake-utils_5", - "nixpkgs": [ - "tau", - "selfci", - "flakebox", - "nixpkgs" - ] - }, + "nixpkgs": { "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "lastModified": 1792615934, + "narHash": "sha256-MlvRfa1O5Cqc2/a+gG+l9TSuowEoCKCmrJ1d9oAVm0I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da6dc0e7b26060f2f71ce3ef7088fb56ee44fa36", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", "type": "github" } }, - "android-nixpkgs_4": { - "inputs": { - "devshell": "devshell_4", - "flake-utils": [ - "tau", - "tau-ext-pim", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "tau-ext-pim", - "flakebox", - "nixpkgs" - ] - }, + "nixpkgs-rust": { "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "lastModified": 1792092144, + "narHash": "sha256-ayn2dNaEAGbxEUYNc1CUVYgqIePEcxwvj1XpVCOksYg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9ecc0a672e9ef2ca4eec8e87dbee653607a7d332", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "9ecc0a672e9ef2ca4eec8e87dbee653607a7d332", "type": "github" } }, - "android-nixpkgs_5": { - "inputs": { - "devshell": "devshell_5", - "flake-utils": "flake-utils_10", - "nixpkgs": [ - "tau", - "tau-ext-pim", - "selfci", - "flakebox", - "nixpkgs" - ] - }, + "nixpkgs_2": { "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "lastModified": 1789785513, + "narHash": "sha256-B44WL6h0XoLjJ41bUPJk0X5SDinLCII//6EcBLXKiJ0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "20b1ddd1aa5ace70c9468305030aa4f9ef79671b", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", "type": "github" } }, - "android-nixpkgs_6": { + "root": { "inputs": { - "devshell": "devshell_6", - "flake-utils": [ - "tau", - "tau-ext-rostra", - "flakebox", - "flake-utils" - ], - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" - }, - "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", - "type": "github" + "advisory-db": "advisory-db", + "crane": "crane", + "nixpkgs": "nixpkgs", + "nixpkgs-rust": "nixpkgs-rust", + "sops-nix": "sops-nix", + "splora": "splora", + "surmount-site": "surmount-site" } }, - "android-nixpkgs_7": { + "rust-overlay": { "inputs": { - "devshell": "devshell_7", - "flake-utils": "flake-utils_15", "nixpkgs": [ - "tau", - "tau-ext-rostra", - "selfci", - "flakebox", + "splora", "nixpkgs" ] }, "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "lastModified": 1789802016, + "narHash": "sha256-QVFfArbYaQMGK4dFV+5RUL72Ci+hNHVhaF/R9qJfDhs=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "26a71e661c47bd21a05d06fec749f3f7c75e9d12", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "owner": "oxalica", + "repo": "rust-overlay", "type": "github" } }, - "android-nixpkgs_8": { + "sops-nix": { "inputs": { - "devshell": "devshell_8", - "flake-utils": [ - "tau", - "tau-ext-slack", - "flakebox", - "flake-utils" - ], "nixpkgs": [ - "tau", - "tau-ext-slack", - "flakebox", "nixpkgs" ] }, "locked": { - "lastModified": 1774654405, - "narHash": "sha256-71jT3XLRInSfbXiBgucWwEufe79N3i5FgAAJ1jCiQWg=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "lastModified": 1792056300, + "narHash": "sha256-YU1LjDe1o2Ymg5zEiCHbmMXLkDCtxHQRiVqZ3nC5GGk=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "0afc63ee6baf0914918169427744cc09ee651ebc", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b", "type": "github" } }, - "android-nixpkgs_9": { + "splora": { "inputs": { - "devshell": "devshell_9", - "flake-utils": "flake-utils_20", - "nixpkgs": [ - "tau", - "tau-ext-slack", - "selfci", - "flakebox", - "nixpkgs" - ] + "asset-registry": "asset-registry", + "crane": "crane_2", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1762287806, - "narHash": "sha256-tmF7Fm2VNukLV32w60QiFLDJB4cO26lhKdDLR8k1BX0=", - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "lastModified": 1792681480, + "narHash": "sha256-3reNjiQDJ32ENFhqbb+f6AM2JX6of7r24vRsZy8tFDg=", + "owner": "SurmountSystems", + "repo": "splora", + "rev": "94d8903fde17fb4f888fd32548919d7be469a95f", "type": "github" }, "original": { - "owner": "tadfisher", - "repo": "android-nixpkgs", - "rev": "d3cbdd2a82b3d054b4e283d28ba27548186a88e7", + "owner": "SurmountSystems", + "ref": "surmount", + "repo": "splora", "type": "github" } }, - "asset-registry": { + "surmount-site": { "flake": false, "locked": { - "lastModified": 1789841305, - "narHash": "sha256-tQAI6b8iqBh9WNa1r+4cT1ZjlQTv1fq/JvGWsnRonQU=", - "owner": "Blockstream", - "repo": "asset_registry_db", - "rev": "95e9c0afb8c8d6798b921257c7bfc2b2863018bb", - "type": "github" - }, - "original": { - "owner": "Blockstream", - "repo": "asset_registry_db", - "type": "github" - } - }, - "bundlers": { - "inputs": { - "nix-bundle": "nix-bundle", - "nix-utils": "nix-utils", - "nixpkgs": [ - "tau", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_2": { - "inputs": { - "nix-bundle": "nix-bundle_2", - "nix-utils": "nix-utils_2", - "nixpkgs": [ - "tau", - "tau-ext-pim", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_3": { - "inputs": { - "nix-bundle": "nix-bundle_3", - "nix-utils": "nix-utils_3", - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_4": { - "inputs": { - "nix-bundle": "nix-bundle_4", - "nix-utils": "nix-utils_4", - "nixpkgs": [ - "tau", - "tau-ext-slack", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_5": { - "inputs": { - "nix-bundle": "nix-bundle_5", - "nix-utils": "nix-utils_5", - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_6": { - "inputs": { - "nix-bundle": "nix-bundle_6", - "nix-utils": "nix-utils_6", - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_7": { - "inputs": { - "nix-bundle": "nix-bundle_7", - "nix-utils": "nix-utils_7", - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "bundlers_8": { - "inputs": { - "nix-bundle": "nix-bundle_8", - "nix-utils": "nix-utils_8", - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "selfci", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1756736145, - "narHash": "sha256-gKlLhzQ6PUTeC0Mi7aaX0tZ0wl6QB/slHLlk7jy0IN4=", - "owner": "NixOS", - "repo": "bundlers", - "rev": "dce9249b74500ef75110153e6da455ffec2532ba", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "bundlers", - "type": "github" - } - }, - "crane": { - "locked": { - "lastModified": 1791754810, - "narHash": "sha256-cN0Mgk+sQtCJcGWs9H4M0CnLRAuC4I8aCwiBTcBJs6E=", - "owner": "ipetkov", - "repo": "crane", - "rev": "bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "rev": "bd63bf1d0a0f6dcacd7549c714a671ee63b0db6a", - "type": "github" - } - }, - "crane_10": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_11": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_12": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_13": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_14": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_15": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_16": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_17": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_18": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_19": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_2": { - "locked": { - "lastModified": 1787326676, - "narHash": "sha256-lWhBbBvC05/xwivKBBiM2YNizpmgqCgyOIzomvRuwxs=", - "owner": "ipetkov", - "repo": "crane", - "rev": "692f7e9ef2ece8125b466f66f2af532b3edaed0d", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_20": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_21": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_22": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_23": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_24": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_25": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_26": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_27": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_28": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_3": { - "locked": { - "lastModified": 1789760033, - "narHash": "sha256-jtT4yxZpR8seYnlCMMWSSPlFN92zO6ICuZ8pJrmi86k=", - "owner": "ipetkov", - "repo": "crane", - "rev": "73b980519cefc727a5f6cc8e5c0947a2f9be6edd", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_4": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_5": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_6": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "crane_7": { - "locked": { - "lastModified": 1755993354, - "narHash": "sha256-FCRRAzSaL/+umLIm3RU3O/+fJ2ssaPHseI2SSFL8yZU=", - "owner": "ipetkov", - "repo": "crane", - "rev": "25bd41b24426c7734278c2ff02e53258851db914", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "crane_8": { - "locked": { - "lastModified": 1776394852, - "narHash": "sha256-K0i9GoNk2To1RQkW348EY4c7RYf3mD74hWlGSc/9sk0=", - "owner": "ipetkov", - "repo": "crane", - "rev": "db220b8709cea4bd43c9adcd4192f212fb6768d1", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.23.3", - "repo": "crane", - "type": "github" - } - }, - "crane_9": { - "locked": { - "lastModified": 1758758545, - "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", - "owner": "ipetkov", - "repo": "crane", - "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "ref": "v0.21.1", - "repo": "crane", - "type": "github" - } - }, - "devshell": { - "inputs": { - "nixpkgs": [ - "tau", - "dpc-public-skills", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_10": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_11": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_12": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_13": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_14": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_15": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_16": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_17": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_2": { - "inputs": { - "nixpkgs": [ - "tau", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_3": { - "inputs": { - "nixpkgs": [ - "tau", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_4": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-pim", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_5": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-pim", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_6": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_7": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_8": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-slack", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1768818222, - "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", - "owner": "numtide", - "repo": "devshell", - "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "devshell_9": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-slack", - "selfci", - "flakebox", - "android-nixpkgs", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1741473158, - "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", - "owner": "numtide", - "repo": "devshell", - "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, - "dpc-public-skills": { - "inputs": { - "flakebox": "flakebox", - "nixpkgs": [ - "tau", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1790279893, - "narHash": "sha256-+fjgU6L628g4Mf03mHOSFbXkwCnZLgTZHuMLfEu/UQ0=", - "ref": "refs/heads/master", - "rev": "30965a2b0cf6ed7bf6a9bf9ac9f214818e86e05a", - "revCount": 124, - "type": "git", - "url": "https://radicle.dpc.pw/z2HR882B4c4mTdAgdt4SozpdeTuMf.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2HR882B4c4mTdAgdt4SozpdeTuMf.git" - } - }, - "fenix": { - "inputs": { - "nixpkgs": [ - "grok-oss", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1787851824, - "narHash": "sha256-hkUiwirNA/E0ZM/jkyl1KWh4tBwhwS/aesUjp6y0ARU=", - "owner": "nix-community", - "repo": "fenix", - "rev": "2b240cd75dabb93338e1a3e6029898022bad0f52", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_10": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-slack", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_10" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_11": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_11" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_12": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_12" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_13": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_13" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_14": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_14" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_15": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_15" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_16": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_16" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_17": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_17" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_18": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_18" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_2": { - "inputs": { - "nixpkgs": [ - "tau", - "dpc-public-skills", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_2" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_3": { - "inputs": { - "nixpkgs": [ - "tau", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_3" - }, - "locked": { - "lastModified": 1788691119, - "narHash": "sha256-grir5cmE/yZKmMQwQrbADf8KJKJGp+gd82Vk0NmKETE=", - "owner": "nix-community", - "repo": "fenix", - "rev": "a557691643f3b1ab520474d20ba2491d957ef729", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_4": { - "inputs": { - "nixpkgs": [ - "tau", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_4" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_5": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-pim", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_5" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_6": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-pim", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_6" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_7": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_7" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_8": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "selfci", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_8" - }, - "locked": { - "lastModified": 1767941162, - "narHash": "sha256-7qJDycrXto4xrQWHbj5BkrRWt/hcfZtjlCstEJTyfJ8=", - "owner": "nix-community", - "repo": "fenix", - "rev": "80b1a19a713e2558c411f3259fecb1edd4b5b327", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "fenix_9": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-slack", - "flakebox", - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src_9" - }, - "locked": { - "lastModified": 1783844668, - "narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": [ - "tau", - "dpc-public-skills", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_10": { - "inputs": { - "systems": "systems_10" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_11": { - "inputs": { - "systems": [ - "tau", - "tau-ext-pim", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_12": { - "inputs": { - "systems": "systems_12" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_13": { - "inputs": { - "systems": [ - "tau", - "tau-ext-rostra", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_14": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_15": { - "inputs": { - "systems": "systems_15" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_16": { - "inputs": { - "systems": [ - "tau", - "tau-ext-rostra", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_17": { - "inputs": { - "systems": "systems_17" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_18": { - "inputs": { - "systems": [ - "tau", - "tau-ext-slack", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_19": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_20": { - "inputs": { - "systems": "systems_20" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_21": { - "inputs": { - "systems": [ - "tau", - "tau-ext-slack", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_22": { - "inputs": { - "systems": "systems_22" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_23": { - "inputs": { - "systems": [ - "tau", - "tau-ext-swarm", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_24": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_25": { - "inputs": { - "systems": "systems_25" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_26": { - "inputs": { - "systems": [ - "tau", - "tau-ext-swarm", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_27": { - "inputs": { - "systems": "systems_27" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_28": { - "inputs": { - "systems": [ - "tau", - "tau-ext-telegram", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_29": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_3": { - "inputs": { - "systems": [ - "tau", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_30": { - "inputs": { - "systems": "systems_30" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_31": { - "inputs": { - "systems": [ - "tau", - "tau-ext-telegram", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_32": { - "inputs": { - "systems": "systems_32" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_33": { - "inputs": { - "systems": [ - "tau", - "tau-ext-xmpp", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_34": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_35": { - "inputs": { - "systems": "systems_35" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_36": { - "inputs": { - "systems": [ - "tau", - "tau-ext-xmpp", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_37": { - "inputs": { - "systems": "systems_37" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_38": { - "inputs": { - "systems": [ - "tau", - "tau-ext-zulip", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_39": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_4": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_40": { - "inputs": { - "systems": "systems_40" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_41": { - "inputs": { - "systems": [ - "tau", - "tau-ext-zulip", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_5": { - "inputs": { - "systems": "systems_5" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_6": { - "inputs": { - "systems": [ - "tau", - "selfci", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_7": { - "inputs": { - "systems": "systems_7" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_8": { - "inputs": { - "systems": [ - "tau", - "tau-ext-pim", - "flakebox", - "systems" - ] - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_9": { - "locked": { - "lastModified": 1623875721, - "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flakebox": { - "inputs": { - "android-nixpkgs": "android-nixpkgs", - "crane": "crane_4", - "fenix": "fenix_2", - "flake-utils": "flake-utils", - "nixpkgs": [ - "tau", - "dpc-public-skills", - "nixpkgs" - ], - "systems": "systems" - }, - "locked": { - "lastModified": 1785785249, - "narHash": "sha256-ihNXCsLG8plE7SRvh3+Q+ZEq6skluNoZix6+X9hY4vE=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "2b8d6aaaf27300eda8f3a6241940d0480a7f798b", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "type": "github" - } - }, - "flakebox_10": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_10", - "crane": "crane_17", - "fenix": "fenix_11", - "flake-utils": "flake-utils_23", - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "nixpkgs" - ], - "systems": "systems_23" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_11": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_11", - "crane": "crane_18", - "fenix": "fenix_12", - "flake-utils": "flake-utils_26", - "nixpkgs": "nixpkgs_18", - "systems": "systems_26", - "wild": "wild_5" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_12": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_12", - "crane": "crane_20", - "fenix": "fenix_13", - "flake-utils": "flake-utils_28", - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "nixpkgs" - ], - "systems": "systems_28" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_13": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_13", - "crane": "crane_21", - "fenix": "fenix_14", - "flake-utils": "flake-utils_31", - "nixpkgs": "nixpkgs_21", - "systems": "systems_31", - "wild": "wild_6" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_14": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_14", - "crane": "crane_23", - "fenix": "fenix_15", - "flake-utils": "flake-utils_33", - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "nixpkgs" - ], - "systems": "systems_33" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_15": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_15", - "crane": "crane_24", - "fenix": "fenix_16", - "flake-utils": "flake-utils_36", - "nixpkgs": "nixpkgs_24", - "systems": "systems_36", - "wild": "wild_7" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_16": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_16", - "crane": "crane_26", - "fenix": "fenix_17", - "flake-utils": "flake-utils_38", - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "nixpkgs" - ], - "systems": "systems_38" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_17": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_17", - "crane": "crane_27", - "fenix": "fenix_18", - "flake-utils": "flake-utils_41", - "nixpkgs": "nixpkgs_27", - "systems": "systems_41", - "wild": "wild_8" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_2": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_2", - "crane": "crane_5", - "fenix": "fenix_3", - "flake-utils": "flake-utils_3", - "nixpkgs": [ - "tau", - "nixpkgs" - ], - "systems": "systems_3" - }, - "locked": { - "lastModified": 1788740722, - "narHash": "sha256-Tc4CulizH5JPBawWcNU5SREUH0OmJ1a81nZMjK9BjHM=", - "owner": "dpc", - "repo": "flakebox", - "rev": "105902c66930a9e6a874d27ea8430e8f8db864d7", - "type": "github" - }, - "original": { - "owner": "dpc", - "repo": "flakebox", - "rev": "105902c66930a9e6a874d27ea8430e8f8db864d7", - "type": "github" - } - }, - "flakebox_3": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_3", - "crane": "crane_6", - "fenix": "fenix_4", - "flake-utils": "flake-utils_6", - "nixpkgs": "nixpkgs_6", - "systems": "systems_6", - "wild": "wild" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_4": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_4", - "crane": "crane_8", - "fenix": "fenix_5", - "flake-utils": "flake-utils_8", - "nixpkgs": [ - "tau", - "tau-ext-pim", - "nixpkgs" - ], - "systems": "systems_8" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_5": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_5", - "crane": "crane_9", - "fenix": "fenix_6", - "flake-utils": "flake-utils_11", - "nixpkgs": "nixpkgs_9", - "systems": "systems_11", - "wild": "wild_2" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_6": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_6", - "crane": "crane_11", - "fenix": "fenix_7", - "flake-utils": "flake-utils_13", - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "nixpkgs" - ], - "systems": "systems_13" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_7": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_7", - "crane": "crane_12", - "fenix": "fenix_8", - "flake-utils": "flake-utils_16", - "nixpkgs": "nixpkgs_12", - "systems": "systems_16", - "wild": "wild_3" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "flakebox_8": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_8", - "crane": "crane_14", - "fenix": "fenix_9", - "flake-utils": "flake-utils_18", - "nixpkgs": [ - "tau", - "tau-ext-slack", - "nixpkgs" - ], - "systems": "systems_18" - }, - "locked": { - "lastModified": 1785524886, - "narHash": "sha256-TfVuYBOkbl1TZ4oG43+ICHGykBvr6LfRkSgqwFDsxBY=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "e402950dbedc4021d41222c913bc9a7ef0a5fab2", - "type": "github" - } - }, - "flakebox_9": { - "inputs": { - "android-nixpkgs": "android-nixpkgs_9", - "crane": "crane_15", - "fenix": "fenix_10", - "flake-utils": "flake-utils_21", - "nixpkgs": "nixpkgs_15", - "systems": "systems_21", - "wild": "wild_4" - }, - "locked": { - "lastModified": 1768170748, - "narHash": "sha256-RZ1PQEVhNtlqBYCvAA8eRSm4zmtpxZEFa88stifg25o=", - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - }, - "original": { - "owner": "rustshop", - "repo": "flakebox", - "rev": "62af969ab344229d2a0d585a482293b3f186b221", - "type": "github" - } - }, - "grok-oss": { - "inputs": { - "crane": "crane_2", - "fenix": "fenix", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1792529825, - "narHash": "sha256-T1tHT+d8v6NuZHeJnUJd1UHQgQHe5XhiNeZYbEoahcM=", - "owner": "SurmountSystems", - "repo": "grok-oss", - "rev": "6703706cc7419ba71af549c1562017f302173c19", - "type": "github" - }, - "original": { - "owner": "SurmountSystems", - "repo": "grok-oss", - "rev": "6703706cc7419ba71af549c1562017f302173c19", - "type": "github" - } - }, - "nix-bundle": { - "inputs": { - "nixpkgs": [ - "tau", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_2": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-pim", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_2" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_3": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_3" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_4": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-slack", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_4" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_5": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_5" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_6": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_6" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_7": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_7" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-bundle_8": { - "inputs": { - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "selfci", - "bundlers", - "nixpkgs" - ], - "utils": "utils_8" - }, - "locked": { - "lastModified": 1756736056, - "narHash": "sha256-8YFhvulVX3iS4TYnKisA9zSImJeFN21G75HOUUFjzuE=", - "owner": "nix-community", - "repo": "nix-bundle", - "rev": "eff01593f62794d458ec714090091419194ab64d", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nix-bundle", - "type": "github" - } - }, - "nix-utils": { - "inputs": { - "flake-utils": "flake-utils_4", - "nixpkgs": "nixpkgs_5" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_2": { - "inputs": { - "flake-utils": "flake-utils_9", - "nixpkgs": "nixpkgs_8" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_3": { - "inputs": { - "flake-utils": "flake-utils_14", - "nixpkgs": "nixpkgs_11" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_4": { - "inputs": { - "flake-utils": "flake-utils_19", - "nixpkgs": "nixpkgs_14" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_5": { - "inputs": { - "flake-utils": "flake-utils_24", - "nixpkgs": "nixpkgs_17" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_6": { - "inputs": { - "flake-utils": "flake-utils_29", - "nixpkgs": "nixpkgs_20" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_7": { - "inputs": { - "flake-utils": "flake-utils_34", - "nixpkgs": "nixpkgs_23" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nix-utils_8": { - "inputs": { - "flake-utils": "flake-utils_39", - "nixpkgs": "nixpkgs_26" - }, - "locked": { - "lastModified": 1744222205, - "narHash": "sha256-di1eNHQdpvvyXv6i7Z+S79KF7cQyhTs7AdFHp7q1e3Q=", - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "rev": "53282197ad090c8cf47c96e99bf6c6c3b2cdc7c0", - "type": "github" - }, - "original": { - "owner": "juliosueiras-nix", - "repo": "nix-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1787736819, - "narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-rust": { - "locked": { - "lastModified": 1792092144, - "narHash": "sha256-ayn2dNaEAGbxEUYNc1CUVYgqIePEcxwvj1XpVCOksYg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "9ecc0a672e9ef2ca4eec8e87dbee653607a7d332", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "9ecc0a672e9ef2ca4eec8e87dbee653607a7d332", - "type": "github" - } - }, - "nixpkgs-unstable": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_2": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_3": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_4": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_5": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_6": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_7": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable_8": { - "locked": { - "lastModified": 1783776592, - "narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_10": { - "locked": { - "lastModified": 1788584326, - "narHash": "sha256-Fd3OB8J9JhgliQwOKcqx4M672CInxi1I5VnwsaXeSQo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6713828a351efa628b025a1adf7f43cbf8597513", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_11": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_12": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_13": { - "locked": { - "lastModified": 1784707089, - "narHash": "sha256-2V/6imsUgB7mPZlHY54oeVBRDoZbPKnvzwkAHUSSufk=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_14": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_15": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_16": { - "locked": { - "lastModified": 1788584326, - "narHash": "sha256-Fd3OB8J9JhgliQwOKcqx4M672CInxi1I5VnwsaXeSQo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6713828a351efa628b025a1adf7f43cbf8597513", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_17": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_18": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_19": { - "locked": { - "lastModified": 1784707089, - "narHash": "sha256-2V/6imsUgB7mPZlHY54oeVBRDoZbPKnvzwkAHUSSufk=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1792150307, - "narHash": "sha256-uFRCFzCN3OPC/cQMmzdIYiFKhakVVx2ZkXO9fOuYkGk=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ed583b5bcfb5f3b7bf6164b24a62fa133682cffe", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_20": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_21": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_22": { - "locked": { - "lastModified": 1788584326, - "narHash": "sha256-Fd3OB8J9JhgliQwOKcqx4M672CInxi1I5VnwsaXeSQo=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6713828a351efa628b025a1adf7f43cbf8597513", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_23": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_24": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_25": { - "locked": { - "lastModified": 1784707089, - "narHash": "sha256-2V/6imsUgB7mPZlHY54oeVBRDoZbPKnvzwkAHUSSufk=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_26": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_27": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1789785513, - "narHash": "sha256-B44WL6h0XoLjJ41bUPJk0X5SDinLCII//6EcBLXKiJ0=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "20b1ddd1aa5ace70c9468305030aa4f9ef79671b", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_4": { - "locked": { - "lastModified": 1790652569, - "narHash": "sha256-641r7xrlSOHYoktL9/aArPIYNgZJ9eM5URzSEOtPJDY=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b6c8664de9b6cc07fe5666a29f91884ba81197c4", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_5": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_6": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_7": { - "locked": { - "lastModified": 1788690626, - "narHash": "sha256-+v4I4LawmRD/mVxO7QIAerRrCkElp3YImzWkkUnvOTg=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c25784012c9982bca5b3e0de87e90bbdac8927d3", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_8": { - "locked": { - "lastModified": 1629252929, - "narHash": "sha256-Aj20gmGBs8TG7pyaQqgbsqAQ6cB+TVuL18Pk3DPBxcQ=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3788c68def67ca7949e0864c27638d484389363d", - "type": "github" - }, - "original": { - "owner": "nixos", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_9": { - "locked": { - "lastModified": 1767799921, - "narHash": "sha256-r4GVX+FToWVE2My8VVZH4V0pTIpnu2ZE8/Z4uxGEMBE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "d351d0653aeb7877273920cd3e823994e7579b0b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-25.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "advisory-db": "advisory-db", - "crane": "crane", - "grok-oss": "grok-oss", - "nixpkgs": "nixpkgs_2", - "nixpkgs-rust": "nixpkgs-rust", - "sops-nix": "sops-nix", - "splora": "splora", - "surmount-site": "surmount-site", - "tau": "tau" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1787768781, - "narHash": "sha256-PMVtb917DTWjGLTQHHI+ATUguk7Wxuqu6J+DINiU+qI=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "70d74f4d134c45b073c82167fb7e7d61334bd8f5", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_10": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_11": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_12": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_13": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_14": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_15": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_16": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_17": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_18": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_2": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_3": { - "flake": false, - "locked": { - "lastModified": 1788637890, - "narHash": "sha256-mA2f4nLHFVf4pVvqbnNLdd6DcFN6Qqb0t1gWxaFRHcU=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "04a5986d7b8d274cf93f071613e50d884e4056a0", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_4": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_5": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_6": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_7": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_8": { - "flake": false, - "locked": { - "lastModified": 1767905519, - "narHash": "sha256-mRU9VEhGQE9dnOU3pu1Rx3dZO4NpZO+cnC0rPMFcCqE=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ff9a2e88b14907562294838f83963e5966f717de", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-analyzer-src_9": { - "flake": false, - "locked": { - "lastModified": 1783779020, - "narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "5be5e89cf0145d73a85c805726821b4adfc3af48", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "splora", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1789802016, - "narHash": "sha256-QVFfArbYaQMGK4dFV+5RUL72Ci+hNHVhaF/R9qJfDhs=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "26a71e661c47bd21a05d06fec749f3f7c75e9d12", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "selfci": { - "inputs": { - "bundlers": "bundlers", - "flake-utils": [ - "tau", - "flake-utils" - ], - "flakebox": "flakebox_3", - "nixpkgs": [ - "tau", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable" - }, - "locked": { - "lastModified": 1787439117, - "narHash": "sha256-rrF5lmYOruM79hZptepsUcEWVocpRDmZgAGCsKab5yM=", - "ref": "refs/heads/master", - "rev": "7b59399d2f4bb55af42c488f5b8b8ab740d4647b", - "revCount": 138, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_2": { - "inputs": { - "bundlers": "bundlers_2", - "flake-utils": [ - "tau", - "tau-ext-pim", - "flake-utils" - ], - "flakebox": "flakebox_5", - "nixpkgs": [ - "tau", - "tau-ext-pim", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_2" - }, - "locked": { - "lastModified": 1787439117, - "narHash": "sha256-rrF5lmYOruM79hZptepsUcEWVocpRDmZgAGCsKab5yM=", - "ref": "refs/heads/master", - "rev": "7b59399d2f4bb55af42c488f5b8b8ab740d4647b", - "revCount": 138, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_3": { - "inputs": { - "bundlers": "bundlers_3", - "flake-utils": [ - "tau", - "tau-ext-rostra", - "flake-utils" - ], - "flakebox": "flakebox_7", - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_3" - }, - "locked": { - "lastModified": 1787439117, - "narHash": "sha256-rrF5lmYOruM79hZptepsUcEWVocpRDmZgAGCsKab5yM=", - "ref": "refs/heads/master", - "rev": "7b59399d2f4bb55af42c488f5b8b8ab740d4647b", - "revCount": 138, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_4": { - "inputs": { - "bundlers": "bundlers_4", - "flake-utils": [ - "tau", - "tau-ext-slack", - "flake-utils" - ], - "flakebox": "flakebox_9", - "nixpkgs": [ - "tau", - "tau-ext-slack", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_4" - }, - "locked": { - "lastModified": 1784067993, - "narHash": "sha256-WG+nqPXh2jSKSoALvAxJzjSrsNDtL7rPPsPecLOACqo=", - "ref": "refs/heads/master", - "rev": "b1847724e182ad5dee276aed80f6bc6b4e0cddf4", - "revCount": 133, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_5": { - "inputs": { - "bundlers": "bundlers_5", - "flake-utils": [ - "tau", - "tau-ext-swarm", - "flake-utils" - ], - "flakebox": "flakebox_11", - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_5" - }, - "locked": { - "lastModified": 1787439117, - "narHash": "sha256-rrF5lmYOruM79hZptepsUcEWVocpRDmZgAGCsKab5yM=", - "ref": "refs/heads/master", - "rev": "7b59399d2f4bb55af42c488f5b8b8ab740d4647b", - "revCount": 138, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_6": { - "inputs": { - "bundlers": "bundlers_6", - "flake-utils": [ - "tau", - "tau-ext-telegram", - "flake-utils" - ], - "flakebox": "flakebox_13", - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_6" - }, - "locked": { - "lastModified": 1784067993, - "narHash": "sha256-WG+nqPXh2jSKSoALvAxJzjSrsNDtL7rPPsPecLOACqo=", - "ref": "refs/heads/master", - "rev": "b1847724e182ad5dee276aed80f6bc6b4e0cddf4", - "revCount": 133, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_7": { - "inputs": { - "bundlers": "bundlers_7", - "flake-utils": [ - "tau", - "tau-ext-xmpp", - "flake-utils" - ], - "flakebox": "flakebox_15", - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_7" - }, - "locked": { - "lastModified": 1787439117, - "narHash": "sha256-rrF5lmYOruM79hZptepsUcEWVocpRDmZgAGCsKab5yM=", - "ref": "refs/heads/master", - "rev": "7b59399d2f4bb55af42c488f5b8b8ab740d4647b", - "revCount": 138, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "selfci_8": { - "inputs": { - "bundlers": "bundlers_8", - "flake-utils": [ - "tau", - "tau-ext-zulip", - "flake-utils" - ], - "flakebox": "flakebox_17", - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "nixpkgs" - ], - "nixpkgs-unstable": "nixpkgs-unstable_8" - }, - "locked": { - "lastModified": 1784067993, - "narHash": "sha256-WG+nqPXh2jSKSoALvAxJzjSrsNDtL7rPPsPecLOACqo=", - "ref": "refs/heads/master", - "rev": "b1847724e182ad5dee276aed80f6bc6b4e0cddf4", - "revCount": 133, - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - }, - "original": { - "type": "git", - "url": "https://radicle.dpc.pw/z2tDzYbAXxTQEKTGFVwiJPajkbeDU.git" - } - }, - "sops-nix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1792056300, - "narHash": "sha256-YU1LjDe1o2Ymg5zEiCHbmMXLkDCtxHQRiVqZ3nC5GGk=", - "owner": "Mic92", - "repo": "sops-nix", - "rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b", - "type": "github" - }, - "original": { - "owner": "Mic92", - "repo": "sops-nix", - "rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b", - "type": "github" - } - }, - "splora": { - "inputs": { - "asset-registry": "asset-registry", - "crane": "crane_3", - "nixpkgs": "nixpkgs_3", - "rust-overlay": "rust-overlay" - }, - "locked": { - "lastModified": 1792021004, - "narHash": "sha256-EXxdItfP4u/jGCmdrCwetThKw2WB9PE4Ljbmc3dRbLQ=", - "owner": "SurmountSystems", - "repo": "splora", - "rev": "5164864cb7349fabc961b5e6c9da2829106bff5f", - "type": "github" - }, - "original": { - "owner": "SurmountSystems", - "ref": "surmount", - "repo": "splora", - "type": "github" - } - }, - "surmount-site": { - "flake": false, - "locked": { - "lastModified": 1787774658, - "narHash": "sha256-q01WrGxa68AogmpFZ6YfsDiBw/To1fHtm1VKU2Dsg4w=", - "owner": "SurmountSystems", - "repo": "site", - "rev": "82e07195e6aa221efe838e652ed8d2d8535b5cac", - "type": "github" - }, - "original": { - "owner": "SurmountSystems", - "repo": "site", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_10": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_11": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_12": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_13": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_14": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_15": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_16": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_17": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_18": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_19": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_20": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_21": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_22": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_23": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_24": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_25": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_26": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_27": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_28": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_29": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_3": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_30": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_31": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_32": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_33": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_34": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_35": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_36": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_37": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_38": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_39": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_4": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_40": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_41": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_5": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_6": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_7": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_8": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_9": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "tau": { - "inputs": { - "dpc-public-skills": "dpc-public-skills", - "flake-utils": "flake-utils_2", - "flakebox": "flakebox_2", - "nixpkgs": "nixpkgs_4", - "selfci": "selfci", - "tau-ext-pim": "tau-ext-pim", - "tau-ext-rostra": "tau-ext-rostra", - "tau-ext-slack": "tau-ext-slack", - "tau-ext-swarm": "tau-ext-swarm", - "tau-ext-telegram": "tau-ext-telegram", - "tau-ext-xmpp": "tau-ext-xmpp", - "tau-ext-zulip": "tau-ext-zulip" - }, - "locked": { - "lastModified": 1791217127, - "narHash": "sha256-FtQmvCsLL1aEBIi1PobEu720kYLHNk7k84oFrtaLWWg=", - "owner": "SurmountSystems", - "repo": "tau", - "rev": "d2e1955b0491f538a486007d0d3ab779d95a828f", + "lastModified": 1787774658, + "narHash": "sha256-q01WrGxa68AogmpFZ6YfsDiBw/To1fHtm1VKU2Dsg4w=", + "owner": "SurmountSystems", + "repo": "site", + "rev": "82e07195e6aa221efe838e652ed8d2d8535b5cac", "type": "github" }, "original": { "owner": "SurmountSystems", - "repo": "tau", - "rev": "d2e1955b0491f538a486007d0d3ab779d95a828f", - "type": "github" - } - }, - "tau-ext-pim": { - "inputs": { - "flake-utils": "flake-utils_7", - "flakebox": "flakebox_4", - "nixpkgs": "nixpkgs_7", - "selfci": "selfci_2" - }, - "locked": { - "lastModified": 1790482451, - "narHash": "sha256-BYHxo8/xKzoN87UjYBY6rrflmLpF9UTgZbCOie9aOWg=", - "ref": "main", - "rev": "ba7c65b71dd2c86761513c01fe57d0b4932f9834", - "revCount": 34, - "type": "git", - "url": "https://radicle.dpc.pw/z4FCuiVzFns5iTquhsYCntZyVWCqi.git" - }, - "original": { - "ref": "main", - "type": "git", - "url": "https://radicle.dpc.pw/z4FCuiVzFns5iTquhsYCntZyVWCqi.git" - } - }, - "tau-ext-rostra": { - "inputs": { - "flake-utils": "flake-utils_12", - "flakebox": "flakebox_6", - "nixpkgs": "nixpkgs_10", - "selfci": "selfci_3" - }, - "locked": { - "lastModified": 1790352987, - "narHash": "sha256-BVkCxAifXdZYIp45EcI+pOfW4yeHmP1+z+6tHlEAgV0=", - "ref": "main", - "rev": "54952ce5ccb16d4fe98c32bc9afc4da3a959cab9", - "revCount": 52, - "type": "git", - "url": "https://radicle.dpc.pw/z4LrrRivcgNjJii5wzbjTvA8ttt6o.git" - }, - "original": { - "ref": "main", - "type": "git", - "url": "https://radicle.dpc.pw/z4LrrRivcgNjJii5wzbjTvA8ttt6o.git" - } - }, - "tau-ext-slack": { - "inputs": { - "flake-utils": "flake-utils_17", - "flakebox": "flakebox_8", - "nixpkgs": "nixpkgs_13", - "selfci": "selfci_4" - }, - "locked": { - "lastModified": 1790352838, - "narHash": "sha256-wg89p0XnOTWr3xW9yPaIgA49ugRtsFEl/nBrv925/Bs=", - "ref": "master", - "rev": "c6a6b978ff331fdc2d06f627750e0417e2e74bcb", - "revCount": 146, - "type": "git", - "url": "https://radicle.dpc.pw/z3NJhEtKWCbHPa28wDQSYJ8eEfBjg.git" - }, - "original": { - "ref": "master", - "type": "git", - "url": "https://radicle.dpc.pw/z3NJhEtKWCbHPa28wDQSYJ8eEfBjg.git" - } - }, - "tau-ext-swarm": { - "inputs": { - "flake-utils": "flake-utils_22", - "flakebox": "flakebox_10", - "nixpkgs": "nixpkgs_16", - "selfci": "selfci_5" - }, - "locked": { - "lastModified": 1790481726, - "narHash": "sha256-LosChWjx/urNIi2qwuOxaa8DE7+01alk5pwkIc6padQ=", - "ref": "master", - "rev": "b2f8462a41e87d21a87f08e1f7255ad9aea9cba2", - "revCount": 61, - "type": "git", - "url": "https://radicle.dpc.pw/z38my9x3Rmn6VYtDMiLKgjK3tRv8o.git" - }, - "original": { - "ref": "master", - "type": "git", - "url": "https://radicle.dpc.pw/z38my9x3Rmn6VYtDMiLKgjK3tRv8o.git" - } - }, - "tau-ext-telegram": { - "inputs": { - "flake-utils": "flake-utils_27", - "flakebox": "flakebox_12", - "nixpkgs": "nixpkgs_19", - "selfci": "selfci_6" - }, - "locked": { - "lastModified": 1790483660, - "narHash": "sha256-5MuNcsPcEKt4sy6I0KVFlSNGyUGcrqbXwaQ1mHb3ZPA=", - "ref": "main", - "rev": "070211310362575b67fe6cbc78ad3a4b0d61e9b4", - "revCount": 142, - "type": "git", - "url": "https://radicle.dpc.pw/z3sPdSePnxtBvP9pTLwUwVgpMU68r.git" - }, - "original": { - "ref": "main", - "type": "git", - "url": "https://radicle.dpc.pw/z3sPdSePnxtBvP9pTLwUwVgpMU68r.git" - } - }, - "tau-ext-xmpp": { - "inputs": { - "flake-utils": "flake-utils_32", - "flakebox": "flakebox_14", - "nixpkgs": "nixpkgs_22", - "selfci": "selfci_7" - }, - "locked": { - "lastModified": 1790353392, - "narHash": "sha256-SZuUzAAUzMTLl16N/8AnKNYiTjAkSmRDF/AYDIgGOME=", - "ref": "master", - "rev": "5ef637f7be0272e91e03b876c3198abb9b057661", - "revCount": 97, - "type": "git", - "url": "https://radicle.dpc.pw/zpN6uwkd6ok9qRAX5yZaF7w8xzDd.git" - }, - "original": { - "ref": "master", - "type": "git", - "url": "https://radicle.dpc.pw/zpN6uwkd6ok9qRAX5yZaF7w8xzDd.git" - } - }, - "tau-ext-zulip": { - "inputs": { - "flake-utils": "flake-utils_37", - "flakebox": "flakebox_16", - "nixpkgs": "nixpkgs_25", - "selfci": "selfci_8" - }, - "locked": { - "lastModified": 1790352813, - "narHash": "sha256-/hZW+qzJkrhFir2Zurq30IfbuoVNGI7DI9eMMMsuOtE=", - "ref": "main", - "rev": "aa2c28e70799e5b6567487f69fc5f2805325d86d", - "revCount": 72, - "type": "git", - "url": "https://radicle.dpc.pw/z2LFTBWK7VpAwC3Bpxohkh91aqXd.git" - }, - "original": { - "ref": "main", - "type": "git", - "url": "https://radicle.dpc.pw/z2LFTBWK7VpAwC3Bpxohkh91aqXd.git" - } - }, - "utils": { - "inputs": { - "systems": "systems_4" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_2": { - "inputs": { - "systems": "systems_9" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_3": { - "inputs": { - "systems": "systems_14" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_4": { - "inputs": { - "systems": "systems_19" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_5": { - "inputs": { - "systems": "systems_24" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_6": { - "inputs": { - "systems": "systems_29" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_7": { - "inputs": { - "systems": "systems_34" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "utils_8": { - "inputs": { - "systems": "systems_39" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "wild": { - "inputs": { - "crane": "crane_7", - "nixpkgs": [ - "tau", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_2": { - "inputs": { - "crane": "crane_10", - "nixpkgs": [ - "tau", - "tau-ext-pim", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_3": { - "inputs": { - "crane": "crane_13", - "nixpkgs": [ - "tau", - "tau-ext-rostra", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_4": { - "inputs": { - "crane": "crane_16", - "nixpkgs": [ - "tau", - "tau-ext-slack", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_5": { - "inputs": { - "crane": "crane_19", - "nixpkgs": [ - "tau", - "tau-ext-swarm", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_6": { - "inputs": { - "crane": "crane_22", - "nixpkgs": [ - "tau", - "tau-ext-telegram", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_7": { - "inputs": { - "crane": "crane_25", - "nixpkgs": [ - "tau", - "tau-ext-xmpp", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", - "type": "github" - } - }, - "wild_8": { - "inputs": { - "crane": "crane_28", - "nixpkgs": [ - "tau", - "tau-ext-zulip", - "selfci", - "flakebox", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762215977, - "narHash": "sha256-x0IZuWjj0LRMj4pu2FVaD8SENm/UVtE1e4rl0EOZZZM=", - "owner": "davidlattimore", - "repo": "wild", - "rev": "7daaf0c89f7b4b9c9ded36e7b3c72aa6512537a1", - "type": "github" - }, - "original": { - "owner": "davidlattimore", - "ref": "0.7.0", - "repo": "wild", + "repo": "site", "type": "github" } } diff --git a/flake.nix b/flake.nix index 4c00aa4..db2e937 100644 --- a/flake.nix +++ b/flake.nix @@ -38,7 +38,7 @@ # nix/packages/arti-onion-service.nix (Surmount-owned 2.7.0 source build). nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; - # rustc/cargo for crane packages and the grok-oss build. Not host + # rustc/cargo for crane packages and the Arti source build. Not host # nixos-26.05 (pkgs.rustc 1.95.0). Not nixos-unstable. Not beta 1.100. # Master HEAD 27347cc2965498fa03393739031c9ef8ecd72c0a (2026-10-19) # still has pkgs/development/compilers/rust/1_99.nix at rustcVersion @@ -82,27 +82,6 @@ flake = false; }; - # Grok OSS TUI (bin/grok-oss). - # Commit pin, not a floating main ref. The repo stays - # github:SurmountSystems/grok-oss. This rev is - # 6703706cc7419ba71af549c1562017f302173c19. Prior lock was - # 525538172dc21391bc690a4cc4a8706a5c554e47. Earlier lock was - # 182293cb30fa5414dd47686b92708103519b7462. Earlier lock was - # decb052744d25b63dbca8a95ca65b4ad794b9ad5. Earlier lock was - # 7dc3685277bb65c4a0634e91a6e2596e48ad882f (main). - # Do not retarget this input to a side branch. - # Default-off NixOS module; package is fail-closed when enable=true. - grok-oss.url = "github:SurmountSystems/grok-oss/6703706cc7419ba71af549c1562017f302173c19"; - - # Tau (bin/tau). SurmountSystems/tau master at - # d2e1955b0491f538a486007d0d3ab779d95a828f (committer - # 2026-10-05T16:18:47Z). This is the operator-synced tree, - # not github:dpc/tau. Commit pin, not a floating master ref. - # Do not follow their nixpkgs. Their nested nixpkgs-unstable - # stays their lock. Default-off NixOS module; package is - # fail-closed when enable=true. - tau.url = "github:SurmountSystems/tau/d2e1955b0491f538a486007d0d3ab779d95a828f"; - # Splora (Esplora-compatible indexer). Operator bumps with nix flake update splora. # NixOS module is nixosModules.splora. REST is one indexer instance plus # JSON-RPC and a cookie; a local bitcoind datadir is not required. @@ -118,8 +97,6 @@ sops-nix, surmount-site, advisory-db, - grok-oss, - tau, splora, ... }: @@ -192,11 +169,6 @@ surmount-secrets-install = self.packages.${system}.surmount-secrets-install; surmount-secrets-prompt = self.packages.${system}.surmount-secrets-prompt; surmount-rekey = self.packages.${system}.surmount-rekey; - grok-oss = self.packages.${system}.grok-oss; - # Flake-input bin/tau (packages.tau). Not referenced unless - # surmount.tau.enable is true. Default-off eval must not - # realize this derivation. - tau = self.packages.${system}.tau; # Flake-input packages. Upstream crane src omits .cargo/config.toml # (laptop cargo still uses Menhera). Do not wrap src again here. splora = self.packages.${system}.splora; @@ -451,19 +423,6 @@ surmount-rekey = pkgs.callPackage ./nix/packages/surmount-rekey.nix { inherit craneLib pkgsRust; }; - # Grok OSS from locked github:SurmountSystems/grok-oss (operator-bump). - # Not in checks.ci (heavy crane). Module stays default-off. - # rustToolchain is nixpkgs-rust 1.99.1, not host pkgs.rustc 1.95.0. - grok-oss = pkgs.callPackage ./nix/packages/grok-oss.nix { - grokOssFlake = grok-oss; - inherit system rustToolchain; - }; - # Tau from locked github:SurmountSystems/tau (operator-synced - # tree, not github:dpc/tau). Consume packages.tau only - # (upstream nix/pkgs/tau.nix). Not in checks.ci. - # Module stays default-off. Fail-closed when enable=true. - tau = - tau.packages.${system}.tau or (throw "tau flake input has no packages.${system}.tau (fail-closed)"); # Flake-input splora packages. Upstream crane omits # .cargo/config.toml from src and builds --offline --locked. # Not in checks.ci: the real package is a rocksdb crane build. @@ -478,7 +437,7 @@ # nixos-26.05: nixfmt-rfc-style is an alias of pkgs.nixfmt (prefer the latter). nixfmt = pkgs.nixfmt; # Flake-pinned just for GHA (`nix shell .#just -c just ci`) and local - # bootstrap without a host just install. Same pattern as grok-oss. + # bootstrap without a host just install. just = pkgs.just; # Ookla Speedtest CLI with --accept-license --accept-gdpr. # Unfree predicate is only ookla-speedtest. nix run .#speedtest @@ -487,7 +446,11 @@ pkgsOokla = import nixpkgs { inherit system; config.allowUnfreePredicate = - pkg: builtins.elem (lib.getName pkg) [ "ookla-speedtest" "speedtest" ]; + pkg: + builtins.elem (lib.getName pkg) [ + "ookla-speedtest" + "speedtest" + ]; }; in pkgsOokla.callPackage ./nix/packages/speedtest.nix { }; @@ -546,10 +509,6 @@ type = "app"; program = "${self.packages.${system}.surmount-host-probe}/bin/surmount-et"; }; - surmount-grok-oss = { - type = "app"; - program = "${self.packages.${system}.surmount-host-probe}/bin/surmount-grok-oss"; - }; surmount-deploy-static-sites = { type = "app"; program = "${self.packages.${system}.surmount-static-sites}/bin/surmount-deploy-static-sites"; diff --git a/hosts/mail-vps/configuration.nix b/hosts/mail-vps/configuration.nix index 1a3e9b2..f499f03 100644 --- a/hosts/mail-vps/configuration.nix +++ b/hosts/mail-vps/configuration.nix @@ -280,7 +280,7 @@ fsType = "ext4"; }; - time.timeZone = "UTC"; + time.timeZone = "America/Denver"; i18n.defaultLocale = "en_US.UTF-8"; # ---- Networking (provider-specific) -------------------------------------- @@ -323,10 +323,6 @@ trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" ]; - # Host cargo cache. An administrator creates the directory. - extra-sandbox-paths = [ - "/var/cache/grok-oss-cargo-target" - ]; }; nixpkgs.config.allowUnfree = false; @@ -340,6 +336,8 @@ tcpdump git scc + fio + sysstat ]; # Wrapped Ookla CLI on PATH. Periodic JSON timer stays off. diff --git a/justfile b/justfile index f6a50ae..9ae5744 100644 --- a/justfile +++ b/justfile @@ -422,6 +422,9 @@ test-eternal-terminal-eval: test-inxi-eval: nix eval --impure --json --file tests/inxi-contract.nix +test-perf-tools-eval: + nix eval --impure --json --file tests/perf-tools-contract.nix + test-remote-builder-eval: nix eval --impure --json --file tests/remote-builder-contract.nix diff --git a/modules/default.nix b/modules/default.nix index c85b386..5e1b6de 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -44,5 +44,7 @@ ./tau.nix # Wrapped Ookla Speedtest CLI. Mail host enables the package; timer stays off. ./speedtest.nix + # Flexible I/O tester (fio) for operator HDD sequential vs random probes. + ./perf-tools.nix ]; } diff --git a/modules/grok-oss.nix b/modules/grok-oss.nix index 59456d6..09ce739 100644 --- a/modules/grok-oss.nix +++ b/modules/grok-oss.nix @@ -1,74 +1,23 @@ -# Optional Grok OSS on the mail host. Default off. -# Enable from host-local. User grok owns ~/.grok. Binary on PATH. -# Do not auto-start the TUI on boot (operator starts it in tmux). -# Hard MemoryMax on the grok user slice. Not Nice=19 (sshd class). -# Install pkgs.tmux here so attach does not depend only on Eternal Terminal. +# grok-oss is not in the mail-host server closure. +# Options remain in options.nix so eval stays defined. Enable must stay +# false. Host-local must not turn this on. This module never installs +# pkgs.grok-oss, tmux-for-grok, or a grok user. { config, lib, - pkgs, ... }: let - cfg = config.surmount; - go = cfg.grokOss; - inherit (lib) mkIf mkMerge; - userSliceName = "user-${toString go.uid}"; - grokHome = go.home; + go = config.surmount.grokOss; in { - config = mkMerge [ - (mkIf (cfg.enable && go.enable) { - assertions = [ - { - assertion = go.package != null; - message = "surmount.grokOss.package must be set when surmount.grokOss.enable is true (bin/grok-oss). Default-off leaves package null so eval does not pull grok-oss. Fail-closed: do not enable without the flake package."; - } - { - assertion = go.memoryMax != null && go.memoryMax != ""; - message = "surmount.grokOss.memoryMax must be a non-empty systemd MemoryMax string when enable is true (empty/null refuse; scaffold default 4G is a grok-oss budget, not a published guest size)."; - } - { - assertion = builtins.substring 0 1 go.home == "/"; - message = "surmount.grokOss.home must be an absolute path."; - } - { - assertion = go.user != "root"; - message = "surmount.grokOss.user must not be root (do not cap the whole machine)."; - } - ]; - - users.groups.${go.user} = { - gid = lib.mkDefault go.uid; - }; - - users.users.${go.user} = { - isNormalUser = true; - description = "Grok OSS operator TUI (no boot start)"; - group = go.user; - uid = go.uid; - home = grokHome; - createHome = true; - hashedPassword = "!"; - linger = false; - }; - - environment.systemPackages = [ pkgs.tmux ] ++ lib.optional (go.package != null) go.package; - - systemd.tmpfiles.rules = [ - "d ${grokHome} 0750 ${go.user} ${go.user} -" - "d ${grokHome}/.grok 0700 ${go.user} ${go.user} -" - ]; - - # Cap the login slice (tmux / grok-oss under user grok). No TUI unit. - systemd.slices.${userSliceName} = { - description = "grok user slice (hard memory cap)"; - sliceConfig = { - MemoryAccounting = true; - MemoryMax = go.memoryMax; - }; - }; - }) - ]; + config = { + assertions = [ + { + assertion = !go.enable; + message = "surmount.grokOss.enable must stay false: grok-oss is not in the mail-host server closure. Do not re-enable from host-local."; + } + ]; + }; } diff --git a/modules/management-ui.nix b/modules/management-ui.nix index f7b04ed..cd123f8 100644 --- a/modules/management-ui.nix +++ b/modules/management-ui.nix @@ -60,6 +60,13 @@ let else "${pkgs.writeText "surmount-static-vhosts.json" (builtins.toJSON staticVhostsJsonMap)}"; extraMailHostnamesCsv = lib.concatStringsSep "," (lib.filter (h: h != "") ui.extraMailHostnames); + # Union status.{primaryDomain} onto a non-empty ACME identifier list so the + # systems dashboard Host is on the certificate hostname set for this edge. + acmeDomains = + let + statusHost = lib.optional (cfg.primaryDomain != "") "status.${cfg.primaryDomain}"; + in + if ui.acme.domains == [ ] then [ ] else lib.unique (ui.acme.domains ++ statusHost); hs = cfg.artiHiddenService; ac = cfg.accessControl; vw = cfg.vaultwarden; @@ -172,6 +179,8 @@ let "SURMOUNT_PRIMARY_DOMAIN=${cfg.primaryDomain}" "SURMOUNT_MAIL_HOSTNAME=${cfg.mailHostname}" "SURMOUNT_SERVICES_HOSTNAME=${cfg.servicesHostname}" + # status.{primaryDomain} is the public systems dashboard Host (Rust-derived). + # ACME identifiers also union that name (acmeDomains) when a list is set. # Stalwart 0.16 default HTTP management listener. "SURMOUNT_STALWART_URL=http://127.0.0.1:8080" "SURMOUNT_REDIRECT_HTTP_TO_HTTPS=${if ui.redirectHttpToHttps then "true" else "false"}" @@ -201,9 +210,7 @@ let ] ++ optional (ui.acme.directory != "") "SURMOUNT_ACME_DIRECTORY=${ui.acme.directory}" ++ optional (ui.acme.email != "") "SURMOUNT_ACME_EMAIL=${ui.acme.email}" - ++ optional ( - ui.acme.domains != [ ] - ) "SURMOUNT_ACME_DOMAINS=${lib.concatStringsSep "," ui.acme.domains}" + ++ optional (acmeDomains != [ ]) "SURMOUNT_ACME_DOMAINS=${lib.concatStringsSep "," acmeDomains}" ++ optional ( ui.acme.accountCredentialsPath != "" ) "SURMOUNT_ACME_ACCOUNT_CREDENTIALS_PATH=${ui.acme.accountCredentialsPath}" diff --git a/modules/perf-tools.nix b/modules/perf-tools.nix new file mode 100644 index 0000000..22be9c3 --- /dev/null +++ b/modules/perf-tools.nix @@ -0,0 +1,22 @@ +# Host convenience: pkgs.fio from nixpkgs so sequential vs random +# direct-I/O jobs can run from this flake's module set. +# Not a critical service. Do not write the system disk; jobs belong on +# a dedicated HDD mount. A deploy-host switch is required before the +# binary exists on the guest PATH. Until then, nix eval / nix run of +# pkgs.fio from this flake is the fallback. + +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.surmount; + inherit (lib) mkIf; +in +{ + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.fio ]; + }; +} diff --git a/modules/speedtest.nix b/modules/speedtest.nix index 14d3231..c2dda21 100644 --- a/modules/speedtest.nix +++ b/modules/speedtest.nix @@ -81,7 +81,11 @@ in (mkIf cfg.enable { # Wrapper getName is speedtest; upstream attr is ookla-speedtest. nixpkgs.config.allowUnfreePredicate = - pkg: builtins.elem (lib.getName pkg) [ "ookla-speedtest" "speedtest" ]; + pkg: + builtins.elem (lib.getName pkg) [ + "ookla-speedtest" + "speedtest" + ]; environment.systemPackages = [ cfg.package ]; }) (mkIf (cfg.enable && cfg.periodic.enable) { diff --git a/modules/tau.nix b/modules/tau.nix index baa9cb7..d4aa181 100644 --- a/modules/tau.nix +++ b/modules/tau.nix @@ -1,35 +1,31 @@ -# Optional Tau coding agent on the mail host. Default off. -# Enable from host-local only. No systemd unit. No extension packages. -# When enable is true, install inputs.tau.packages.${system}.tau only -# (pkgs.tau from that flake attr). Default-off does not reference it. +# tau is not in the mail-host server closure. +# The enable option stays so leftover host-local can be refused closed. +# This module never installs pkgs.tau and never adds a daemon. { config, lib, - pkgs, ... }: let - cfg = config.surmount.tau; - inherit (lib) mkIf mkOption types; - system = pkgs.stdenv.hostPlatform.system; + inherit (lib) mkOption types; in { options.surmount.tau.enable = mkOption { type = types.bool; default = false; description = '' - Install bin/tau from the locked SurmountSystems/tau flake input - (packages.tau, upstream nix/pkgs/tau.nix). Default false. The - mail closure does not gain tau while this stays false. Does not - install extension packages. Does not add a systemd unit. Enable - only from private host-local. + Must stay false. Tau is not in the mail-host server closure. + The flake does not take a tau input. Host-local must not enable this. ''; }; - config = mkIf cfg.enable { - environment.systemPackages = [ - (pkgs.tau or (throw "tau flake input has no packages.${system}.tau (fail-closed)")) + config = { + assertions = [ + { + assertion = !config.surmount.tau.enable; + message = "surmount.tau.enable must stay false: tau is not in the mail-host server closure. Do not re-enable from host-local."; + } ]; }; } diff --git a/nix/packages/grok-oss.nix b/nix/packages/grok-oss.nix deleted file mode 100644 index ebd2a36..0000000 --- a/nix/packages/grok-oss.nix +++ /dev/null @@ -1,37 +0,0 @@ -# Consume github:SurmountSystems/grok-oss flake package (bin/grok-oss). -# Call as: pkgs.callPackage ./grok-oss.nix { inherit grokOssFlake system rustToolchain; } -# Fail-closed: missing packages.grok-oss throws. Do not enable the NixOS -# module without this package. -# pkgs stays the host channel for C libraries. rustToolchain is -# nixpkgs-rust (rustc 1.99.1). Do not use host pkgs.rustc / pkgs.cargo. - -{ - grokOssFlake, - system, - pkgs, - lib, - runCommand, - rustToolchain, -}: -let - # Rewrite leftover stdenv.isLinux / stdenv.isDarwin to hostPlatform - # before import. A tip that already uses hostPlatform is unchanged. - # Do not evaluate grokOssFlake.packages (the old form warns). - patchedSrc = runCommand "grok-oss-flake-hostplatform" { } '' - cp -a ${grokOssFlake.outPath} "$out" - chmod -R u+w "$out" - find "$out" -type f -name '*.nix' -print0 | xargs -0 sed -i \ - -e 's/pkgs\.stdenv\.isLinux/pkgs.stdenv.hostPlatform.isLinux/g' \ - -e 's/pkgs\.stdenv\.isDarwin/pkgs.stdenv.hostPlatform.isDarwin/g' \ - -e 's/stdenv\.isLinux/stdenv.hostPlatform.isLinux/g' \ - -e 's/stdenv\.isDarwin/stdenv.hostPlatform.isDarwin/g' \ - -e 's/CARGO_BUILD_JOBS = "2";/CARGO_BUILD_JOBS = "32";/' - ''; - craneLib = (grokOssFlake.inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; - built = import "${patchedSrc}/flake/grok-oss.nix" { - inherit pkgs lib craneLib; - self = grokOssFlake; - }; -in -built.grok-oss or built.default - or (throw "grok-oss flake input has no packages.${system}.grok-oss (fail-closed; do not set surmount.grokOss.enable without the package)") diff --git a/script/testdata/host-profile/README.md b/script/testdata/host-profile/README.md index b9b8be6..0893f87 100644 --- a/script/testdata/host-profile/README.md +++ b/script/testdata/host-profile/README.md @@ -2,7 +2,7 @@ | Path | Role | |------|------| -| `sample-host-profile.toml` | Public sample profile shape (`example.invalid` only). Multi-SAN (H5): services + mail + apex + www. Staging LE directory (H6: production only with explicit flag or profile field). | +| `sample-host-profile.toml` | Public sample profile shape (`example.invalid` only). Certificate hostnames (H5): services + mail + apex + www + status. Staging LE directory (H6: production only with explicit flag or profile field). | | `expected-host-local-acme.nix` | Expected ACME fragment shape for hermetic render test (space-separated Nix `domains` list; durable H-PEM TLS paths; staging directory). | Real operator profiles live **outside** public git (e.g. diff --git a/script/testdata/host-profile/expected-host-local-acme.nix b/script/testdata/host-profile/expected-host-local-acme.nix index ee05864..e90a243 100644 --- a/script/testdata/host-profile/expected-host-local-acme.nix +++ b/script/testdata/host-profile/expected-host-local-acme.nix @@ -19,7 +19,7 @@ enable = true; directory = "https://acme-staging-v02.api.letsencrypt.org/directory"; email = "ops@example.invalid"; - domains = [ "services.example.invalid" "mail.example.invalid" "example.invalid" "www.example.invalid" ]; + domains = [ "services.example.invalid" "mail.example.invalid" "example.invalid" "www.example.invalid" "status.example.invalid" ]; accountCredentialsPath = "/var/lib/surmount/secrets/acme/account.json"; challenge = "dns-01"; dnsProvider = "external-hook"; diff --git a/script/testdata/host-profile/sample-host-profile.toml b/script/testdata/host-profile/sample-host-profile.toml index dc2d7f0..448264b 100644 --- a/script/testdata/host-profile/sample-host-profile.toml +++ b/script/testdata/host-profile/sample-host-profile.toml @@ -16,16 +16,18 @@ acme_directory = "https://acme-staging-v02.api.letsencrypt.org/directory" # Edge certificate hostnames for laptop DNS-01 (documentation domains only). # Sample list: services (UI), mail (IMAP/SMTP TLS; first-class), apex + www -# (public UNDER CONSTRUCTION). Optional: mta-sts. on the *private* -# laptop profile when serving MTA-STS HTTPS (not this sample). Enable -# mtaStsMode=testing only after that name is on the production leaf. -# Private live profile should include mail. so Evolution sees a -# browser-trusted name. See docs/DNS.md and docs/EDGE_AND_TLS.md. +# (public UNDER CONSTRUCTION), status (systems dashboard). Optional: +# mta-sts. on the *private* laptop profile when serving MTA-STS +# HTTPS (not this sample). Enable mtaStsMode=testing only after that name +# is on the production leaf. Private live profile should include mail. +# so Evolution sees a browser-trusted name. See docs/DNS.md and +# docs/EDGE_AND_TLS.md. acme_domains = [ "services.example.invalid", "mail.example.invalid", "example.invalid", "www.example.invalid", + "status.example.invalid", ] # Optional overrides. Defaults are durable Domain B (H-PEM): diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 9e5b158..3be9964 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -43,6 +43,9 @@ let # systemPackages walk) forces that attr; keep it a cheap fake. vandelay = prev.writeShellScriptBin "vandelay" "exit 0"; surmount-mail-import = prev.writeShellScriptBin "surmount-mail-import-maildir" "exit 0"; + # mail-vps sample enables wrapped speedtest. Keep it a cheap fake so + # t47 does not pull ookla. + speedtest = prev.writeShellScriptBin "speedtest" "exit 0"; surmount-deploy-host = prev.writeShellScriptBin "surmount-deploy-host" "exit 0"; surmount-management-ui = prev.writeShellScriptBin "surmount-management-ui" "exit 0"; # Packaged DNS-01 hook **code** (fake; real package is nix/packages/...). @@ -175,12 +178,12 @@ let assert e.config.surmount.remoteBuilder.diskGuardPercent == 95; assert e.config.surmount.lake.enable == false; assert e.config.surmount.lake.jobs == 4; - # Grok OSS is opt-in (host-local). Scaffold off; no grok user. + # grok-oss is not in the mail-host closure. Enable stays false. assert e.config.surmount.grokOss.enable == false; assert e.config.surmount.grokOss.package == null; assert e.config.surmount.grokOss.user == "grok"; assert e.config.surmount.grokOss.memoryMax == "4G"; - # Tau coding agent is opt-in (host-local). Scaffold off; no daemon. + # tau is not in the mail-host closure. Enable stays false. assert e.config.surmount.tau.enable == false; assert e.config.surmount.logging.rateLimitBurst == "50000"; # Domain C Vaultwarden stays off on sample defaults (no production footgun). @@ -2802,49 +2805,26 @@ let assert !(builtins.any isTmux pkgsList); "t45-grok-oss-default-off-ok"; - t45b-grok-oss-enable-user-and-memory-cap = + t45b-grok-oss-enable-refused = let - e = evalSurmount { + failed = failedAssertions (evalSurmount { grokOss.enable = true; grokOss.package = pkgs.writeShellScriptBin "grok-oss" "exit 0"; - }; - userSlice = e.config.systemd.slices."user-1988".sliceConfig or { }; - grok = e.config.users.users.grok or { }; - pkgsList = e.config.environment.systemPackages; - isGrokOss = - p: - (p.pname or "") == "grok-oss" - || (p.name or "") == "grok-oss" - || lib.hasPrefix "grok-oss-" (p.name or "") - || lib.hasSuffix "-grok-oss" (p.name or ""); - isTmux = - p: (p.pname or "") == "tmux" || (p.name or "") == "tmux" || lib.hasPrefix "tmux-" (p.name or ""); - tmp = e.config.systemd.tmpfiles.rules or [ ]; + }); in - assert failedAssertions e == [ ]; - assert e.config.users.users ? grok; - assert grok.isNormalUser == true; - assert grok.uid == 1988; - assert grok.home == "/home/grok"; - assert grok.linger == false; - assert userSlice.MemoryMax or null == "4G"; - assert !(userSlice ? Nice); - assert !(e.config.systemd.services ? surmount-grok-oss); - assert builtins.any isGrokOss pkgsList; - assert builtins.any isTmux pkgsList; - assert builtins.any (r: lib.hasInfix "/home/grok/.grok" r) tmp; - assert serviceNice e "stalwart-mail" == null; - assert serviceNice e "sshd" == null; - "t45b-grok-oss-enable-user-and-memory-cap-ok"; + assert failed != [ ]; + assert builtins.any (a: lib.hasInfix "grokOss.enable" a.message) failed; + "t45b-grok-oss-enable-refused-ok"; - t45c-grok-oss-enable-requires-package = + t45c-grok-oss-enable-without-package-refused = let failed = failedAssertions (evalSurmount { grokOss.enable = true; }); in - assert builtins.any (a: lib.hasInfix "grokOss.package" a.message) failed; - "t45c-grok-oss-enable-requires-package-ok"; + assert failed != [ ]; + assert builtins.any (a: lib.hasInfix "grokOss.enable" a.message) failed; + "t45c-grok-oss-enable-without-package-refused-ok"; t46-tau-default-off = let @@ -2871,6 +2851,54 @@ let assert !(builtins.any isTau pkgsList); "t46-tau-default-off-ok"; + t46b-tau-enable-refused = + let + failed = failedAssertions (evalSurmount { + tau.enable = true; + }); + in + assert failed != [ ]; + assert builtins.any (a: lib.hasInfix "tau.enable" a.message) failed; + "t46b-tau-enable-refused-ok"; + + # Public sample host (hosts/mail-vps/configuration.nix, no host-local). + # Fail if grokOss/tau are enabled or those packages appear on PATH. + t47-mail-vps-no-grok-tau = + let + e = evalSurmount { + __extraModules = [ ../hosts/mail-vps/configuration.nix ]; + }; + pkgsList = e.config.environment.systemPackages; + isGrokOss = + p: + (p.pname or "") == "grok-oss" + || (p.name or "") == "grok-oss" + || lib.hasPrefix "grok-oss-" (p.name or "") + || lib.hasSuffix "-grok-oss" (p.name or ""); + isTau = + p: + let + pname = p.pname or ""; + name = p.name or ""; + in + pname == "tau" + || pname == "dpc-tau" + || name == "tau" + || lib.hasPrefix "tau-" pname + || lib.hasPrefix "tau-" name + || lib.hasPrefix "dpc-tau" pname + || lib.hasPrefix "dpc-tau" name; + isFio = + p: (p.pname or "") == "fio" || (p.name or "") == "fio" || lib.hasPrefix "fio-" (p.name or ""); + in + assert failedAssertions e == [ ]; + assert e.config.surmount.grokOss.enable == false; + assert e.config.surmount.tau.enable == false; + assert !(builtins.any isGrokOss pkgsList); + assert !(builtins.any isTau pkgsList); + assert builtins.any isFio pkgsList; + "t47-mail-vps-no-grok-tau-ok"; + t50-splora-proxy-default-off = let e = evalSurmount { managementUi.enable = true; }; @@ -3274,9 +3302,11 @@ let t44-lake-default-off t44b-lake-enable-memory-cap t45-grok-oss-default-off - t45b-grok-oss-enable-user-and-memory-cap - t45c-grok-oss-enable-requires-package + t45b-grok-oss-enable-refused + t45c-grok-oss-enable-without-package-refused t46-tau-default-off + t46b-tau-enable-refused + t47-mail-vps-no-grok-tau t50-splora-proxy-default-off t51-splora-proxy-enable-env-and-udp-https t52-splora-electrum-socket-asserts diff --git a/tests/perf-tools-contract.nix b/tests/perf-tools-contract.nix new file mode 100644 index 0000000..99071d7 --- /dev/null +++ b/tests/perf-tools-contract.nix @@ -0,0 +1,62 @@ +# Targeted fio host-package contract (not the 14-minute module-eval suite). +# Impure flake pin so `just test-perf-tools-eval` can nix-eval this file. +# Never embeds host SKUs, addresses, or RAM/swap sizes. +let + flake = builtins.getFlake (toString ./..); + system = builtins.currentSystem; + inherit (flake.inputs.nixpkgs) lib; + evalPerf = + extra: + lib.nixosSystem { + inherit system; + modules = [ + ../modules/options.nix + ../modules/perf-tools.nix + { + system.stateVersion = "26.05"; + networking.hostName = "perf-tools-eval"; + fileSystems."/" = { + device = "nodev"; + fsType = "ext4"; + }; + boot.loader.grub.enable = false; + surmount = extra // { + enable = extra.enable or true; + }; + } + ]; + }; + isFio = + p: + (p.pname or "") == "fio" + || lib.hasPrefix "fio-" (p.name or "") + || lib.hasSuffix "-fio" (p.name or ""); + + t-fio-system-package = + let + e = evalPerf { }; + pkgsList = e.config.environment.systemPackages; + in + assert builtins.any isFio pkgsList; + "t-fio-system-package-ok"; + + t-fio-off-when-surmount-disabled = + let + e = evalPerf { enable = false; }; + pkgsList = e.config.environment.systemPackages; + in + assert !(builtins.any isFio pkgsList); + "t-fio-off-when-surmount-disabled-ok"; + + t-fio-nixpkgs-pname = + let + pkgs = flake.inputs.nixpkgs.legacyPackages.${system}; + in + assert pkgs.fio.pname == "fio"; + "t-fio-nixpkgs-pname-ok"; +in +{ + t-fio-system-package = t-fio-system-package; + t-fio-off-when-surmount-disabled = t-fio-off-when-surmount-disabled; + t-fio-nixpkgs-pname = t-fio-nixpkgs-pname; +} From 912c828dfeea39f26b0ca18e5c38a1b7eb9b7325 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Sat, 24 Oct 2026 01:31:44 -0600 Subject: [PATCH 06/22] fixes --- crates/management-ui/src/status.rs | 741 ++++++++++++++++++++++++++--- docs/DNS.md | 41 +- docs/EDGE_AND_TLS.md | 31 +- docs/SEARCH_AND_UI.md | 4 +- 4 files changed, 718 insertions(+), 99 deletions(-) diff --git a/crates/management-ui/src/status.rs b/crates/management-ui/src/status.rs index ea058d6..946ae06 100644 --- a/crates/management-ui/src/status.rs +++ b/crates/management-ui/src/status.rs @@ -1,23 +1,30 @@ //! Public systems dashboard for Host `status.{primary}` (Axum, not nginx). //! -//! Not the operator console. Payload fields come from process config -//! (listen mode, mail hostname present, MTA-STS mode). No RAM, disk, IP, -//! onion, or other live host facts. +//! Not the operator console. Rows come from a product inventory plus live +//! systemd ActiveState / SubState (and optional HTTP samples). Host uptime +//! comes from `/proc/uptime`. No RAM, disk, IP, onion, or cookie bytes. + +use std::collections::BTreeMap; +use std::net::IpAddr; +use std::path::Path; +use std::process::Command; use axum::http::{StatusCode, header}; use axum::response::{IntoResponse, Response}; use serde::Serialize; use crate::config::AppConfig; -use crate::mta_sts::MtaStsMode; -use crate::redirect::status_hostname; +use crate::mta_sts::{MtaStsMode, mta_sts_policy_hostname}; +use crate::redirect::{host_for_url_authority, status_hostname}; -/// One named systems row the dashboard emits (name/role/state, not live facts). +/// One named systems row the dashboard emits. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SystemsComponent { - pub name: &'static str, - pub role: &'static str, + pub name: String, + pub role: String, pub state: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub href: Option, } /// Representative systems status body. Tests assert these field names exist. @@ -28,73 +35,443 @@ pub struct SystemsStatusPayload { pub version: &'static str, pub host: String, pub primary_domain: String, + pub uptime: String, pub systems: Vec, } -/// Build the payload the status Host handler actually emits. -pub fn systems_status_from_config(config: &AppConfig) -> SystemsStatusPayload { - let host = status_hostname(&config.primary_domain); - let edge_state = if config.listen_mode.is_https() { - "https" - } else { - "http" - }; - let tls_state = if config.listen_mode.is_https() { - "leaf" - } else { - "plain_http" - }; - let mail_state = if config.mail_hostname.trim().is_empty() { - "unset" +/// Live or injected sample for one unit / HTTP check. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ComponentProbe { + pub active_state: String, + pub sub_state: String, + pub load_state: String, + pub http_status: Option, +} + +/// One product row the dashboard knows how to name. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SystemsInventoryItem { + pub id: &'static str, + pub role: &'static str, + pub unit: Option<&'static str>, + pub href: Option, +} + +/// Product units this dashboard may probe. Not Vaultwarden, Lake, or Splora mainnet. +pub const DASHBOARD_UNITS: &[&str] = &[ + "stalwart-mail.service", + "surmount-management-ui.service", + "surmount-arti-hidden-service.service", + "sshd.service", + "surmount-scram.service", + "fail2ban.service", + "splora-testnet3.service", + "splora-testnet4.service", + "splora-mutinynet.service", + "splora-liquid.service", +]; + +/// Ordinary English status from systemd ActiveState / SubState / HTTP samples. +/// +/// Never returns a constant "ok". HTTP client errors win over a running unit. +pub fn classify_component_status(probe: &ComponentProbe) -> String { + if let Some(code) = probe.http_status + && code >= 400 + { + return format!("HTTP {code}"); + } + let load = probe.load_state.trim(); + let active = probe.active_state.trim(); + let sub = probe.sub_state.trim(); + if load.eq_ignore_ascii_case("not-found") { + return "not installed".to_string(); + } + if active.eq_ignore_ascii_case("failed") || sub.eq_ignore_ascii_case("failed") { + return "failed".to_string(); + } + if sub.eq_ignore_ascii_case("listening") { + return "listening".to_string(); + } + if active.eq_ignore_ascii_case("inactive") { + return "inactive".to_string(); + } + if active.eq_ignore_ascii_case("activating") { + return "starting".to_string(); + } + if active.eq_ignore_ascii_case("deactivating") { + return "stopping".to_string(); + } + if active.eq_ignore_ascii_case("reloading") { + return "reloading".to_string(); + } + if active.eq_ignore_ascii_case("maintenance") { + return "maintenance".to_string(); + } + if active.eq_ignore_ascii_case("active") && sub.eq_ignore_ascii_case("running") { + return "running".to_string(); + } + if active.eq_ignore_ascii_case("active") && sub.eq_ignore_ascii_case("exited") { + return "exited".to_string(); + } + if active.eq_ignore_ascii_case("active") { + return "running".to_string(); + } + if !sub.is_empty() { + return sub.to_ascii_lowercase(); + } + if !active.is_empty() { + return active.to_ascii_lowercase(); + } + "unknown".to_string() +} + +/// Standing number law: seconds; minutes at most 60 minutes; N.N hours through +/// 24 hours; N.N days after that. +pub fn format_uptime_seconds(seconds: f64) -> String { + if !seconds.is_finite() || seconds < 0.0 { + return "unknown".to_string(); + } + let secs = seconds.floor() as u64; + if secs < 60 { + if secs == 1 { + return "1 second".to_string(); + } + return format!("{secs} seconds"); + } + if secs <= 3600 { + let minutes = secs / 60; + if minutes == 1 { + return "1 minute".to_string(); + } + return format!("{minutes} minutes"); + } + if secs <= 86_400 { + return format!("{:.1} hours", seconds / 3600.0); + } + format!("{:.1} days", seconds / 86_400.0) +} + +/// First field of `/proc/uptime` (seconds.centiseconds). +pub fn parse_proc_uptime(contents: &str) -> Option { + let first = contents.split_whitespace().next()?; + let n: f64 = first.parse().ok()?; + if !n.is_finite() || n < 0.0 { + return None; + } + Some(n) +} + +/// Read host uptime seconds from a `/proc/uptime`-shaped file. +pub fn read_proc_uptime_file(path: &Path) -> Option { + std::fs::read_to_string(path) + .ok() + .as_deref() + .and_then(parse_proc_uptime) +} + +/// `https://{hostname}{path}` for a DNS hostname. Refuses IP literals. +pub fn https_public_href(host: &str, path: &str) -> Option { + let authority = host_for_url_authority(host); + if authority.is_empty() { + return None; + } + if authority.parse::().is_ok() { + return None; + } + if authority.starts_with('[') { + return None; + } + if authority.chars().all(|c| c.is_ascii_digit() || c == '.') && authority.contains('.') { + // Dotted numeric that did not parse as IPv4 still must not become an href. + return None; + } + let host = authority.to_ascii_lowercase(); + if host == "localhost" { + return None; + } + let path = if path.is_empty() { + "/" + } else if path.starts_with('/') { + path } else { - "configured" + return None; }; - let mta_state = match config.mta_sts_mode { - MtaStsMode::Off => "off", - MtaStsMode::Testing => "testing", - MtaStsMode::Enforce => "enforce", + Some(format!("https://{host}{path}")) +} + +/// Parse `systemctl show -p Id -p ActiveState -p SubState -p LoadState` text. +pub fn parse_systemctl_show(text: &str) -> BTreeMap { + let mut map = BTreeMap::new(); + let mut id = String::new(); + let mut probe = ComponentProbe::default(); + let flush = |id: &mut String, + probe: &mut ComponentProbe, + map: &mut BTreeMap| { + if !id.is_empty() { + map.insert(std::mem::take(id), std::mem::take(probe)); + } + *probe = ComponentProbe::default(); }; - let console_state = if config.services_hostname.trim().is_empty() { - "unset" + for line in text.lines() { + let line = line.trim(); + if line.is_empty() { + flush(&mut id, &mut probe, &mut map); + continue; + } + if let Some(v) = line.strip_prefix("Id=") { + if !id.is_empty() { + flush(&mut id, &mut probe, &mut map); + } + id = v.trim().to_string(); + } else if let Some(v) = line.strip_prefix("ActiveState=") { + probe.active_state = v.trim().to_string(); + } else if let Some(v) = line.strip_prefix("SubState=") { + probe.sub_state = v.trim().to_string(); + } else if let Some(v) = line.strip_prefix("LoadState=") { + probe.load_state = v.trim().to_string(); + } + } + flush(&mut id, &mut probe, &mut map); + map +} + +fn splora_portal_host(config: &AppConfig) -> String { + if let Some(h) = config + .splora_proxy + .portal_host + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + { + return host_for_url_authority(h).to_ascii_lowercase(); + } + let primary = host_for_url_authority(&config.primary_domain); + if primary.is_empty() { + return String::new(); + } + format!("splora.{}", primary.to_ascii_lowercase()) +} + +fn classify_tls_state(https: bool) -> String { + if https { + "listening".to_string() } else { - "services_hostname" - }; + "plain HTTP".to_string() + } +} + +fn classify_mta_sts_state(mode: MtaStsMode) -> String { + match mode { + MtaStsMode::Off => "off".to_string(), + MtaStsMode::Testing => "testing".to_string(), + MtaStsMode::Enforce => "enforcing".to_string(), + } +} + +/// Named services this Host actually runs. Splora mainnet, Vaultwarden, and Lake +/// stay off this list unless a later inventory finds them installed. +pub fn systems_status_inventory(config: &AppConfig) -> Vec { + let mail = https_public_href(&config.mail_hostname, "/"); + let services = https_public_href(&config.services_hostname, "/"); + let status = https_public_href(&status_hostname(&config.primary_domain), "/"); + let site = https_public_href(&config.primary_domain, "/"); + let mta = https_public_href( + &mta_sts_policy_hostname(&config.primary_domain), + "/.well-known/mta-sts.txt", + ); + let portal = splora_portal_host(config); + let splora_testnet = https_public_href(&portal, "/testnet/"); + let splora_testnet4 = https_public_href(&portal, "/testnet4/"); + let splora_mutinynet = https_public_href(&portal, "/mutinynet/"); + let splora_liquid = https_public_href(&portal, "/liquid/"); + vec![ + SystemsInventoryItem { + id: "mail", + role: "mail", + unit: Some("stalwart-mail.service"), + href: mail, + }, + SystemsInventoryItem { + id: "edge", + role: "HTTPS edge", + unit: Some("surmount-management-ui.service"), + href: site.clone(), + }, + SystemsInventoryItem { + id: "tls", + role: "certificate", + unit: None, + href: site.clone(), + }, + SystemsInventoryItem { + id: "mta_sts", + role: "MTA-STS policy", + unit: None, + href: mta, + }, + SystemsInventoryItem { + id: "console", + role: "operator console", + unit: Some("surmount-management-ui.service"), + href: services, + }, + SystemsInventoryItem { + id: "status", + role: "systems dashboard", + unit: Some("surmount-management-ui.service"), + href: status, + }, + SystemsInventoryItem { + id: "site", + role: "public site", + unit: Some("surmount-management-ui.service"), + href: site, + }, + SystemsInventoryItem { + id: "arti", + role: "hidden service", + unit: Some("surmount-arti-hidden-service.service"), + href: None, + }, + SystemsInventoryItem { + id: "sshd", + role: "SSH", + unit: Some("sshd.service"), + href: None, + }, + SystemsInventoryItem { + id: "splora-testnet3", + role: "Splora testnet3", + unit: Some("splora-testnet3.service"), + href: splora_testnet, + }, + SystemsInventoryItem { + id: "splora-testnet4", + role: "Splora testnet4", + unit: Some("splora-testnet4.service"), + href: splora_testnet4, + }, + SystemsInventoryItem { + id: "splora-mutinynet", + role: "Splora mutinynet", + unit: Some("splora-mutinynet.service"), + href: splora_mutinynet, + }, + SystemsInventoryItem { + id: "splora-liquid", + role: "Splora liquid", + unit: Some("splora-liquid.service"), + href: splora_liquid, + }, + SystemsInventoryItem { + id: "scram", + role: "builder memory guard", + unit: Some("surmount-scram.service"), + href: None, + }, + SystemsInventoryItem { + id: "fail2ban", + role: "intrusion prevention", + unit: Some("fail2ban.service"), + href: None, + }, + ] +} + +/// Build the payload from config, formatted uptime, and unit/HTTP samples. +pub fn systems_status_from_probes( + config: &AppConfig, + uptime: &str, + probes: &BTreeMap, +) -> SystemsStatusPayload { + let host = status_hostname(&config.primary_domain); + let https = config.listen_mode.is_https(); + let systems = systems_status_inventory(config) + .into_iter() + .map(|item| { + let state = match item.id { + "tls" => classify_tls_state(https), + "mta_sts" => classify_mta_sts_state(config.mta_sts_mode), + _ => { + let probe = item + .unit + .and_then(|u| probes.get(u)) + .cloned() + .unwrap_or_default(); + classify_component_status(&probe) + } + }; + SystemsComponent { + name: item.id.to_string(), + role: item.role.to_string(), + state, + href: item.href, + } + }) + .collect(); SystemsStatusPayload { surface: "systems_status", service: "surmount", version: env!("CARGO_PKG_VERSION"), host, primary_domain: config.primary_domain.clone(), - systems: vec![ - SystemsComponent { - name: "mail", - role: "smtp_imap", - state: mail_state.to_string(), - }, - SystemsComponent { - name: "edge", - role: "https_axum", - state: edge_state.to_string(), - }, - SystemsComponent { - name: "tls", - role: "certificate", - state: tls_state.to_string(), - }, - SystemsComponent { - name: "mta_sts", - role: "policy", - state: mta_state.to_string(), - }, - SystemsComponent { - name: "console", - role: "operator", - state: console_state.to_string(), - }, - ], + uptime: uptime.to_string(), + systems, } } +fn read_host_uptime() -> String { + #[cfg(test)] + { + if let Ok(raw) = std::env::var("SURMOUNT_STATUS_UPTIME_SECONDS") + && let Ok(n) = raw.parse::() + { + return format_uptime_seconds(n); + } + return format_uptime_seconds(720.0); + } + #[cfg(not(test))] + { + read_proc_uptime_file(Path::new("/proc/uptime")) + .map(format_uptime_seconds) + .unwrap_or_else(|| "unknown".to_string()) + } +} + +fn live_unit_probes() -> BTreeMap { + #[cfg(test)] + { + if std::env::var_os("SURMOUNT_STATUS_SYSTEMCTL").is_none() { + return BTreeMap::new(); + } + } + let systemctl = + std::env::var("SURMOUNT_STATUS_SYSTEMCTL").unwrap_or_else(|_| "systemctl".to_string()); + let mut cmd = Command::new(systemctl); + cmd.args([ + "show", + "-p", + "Id", + "-p", + "ActiveState", + "-p", + "SubState", + "-p", + "LoadState", + "--no-pager", + ]); + cmd.args(DASHBOARD_UNITS); + match cmd.output() { + Ok(out) => parse_systemctl_show(&String::from_utf8_lossy(&out.stdout)), + Err(_) => BTreeMap::new(), + } +} + +/// Build the payload the status Host handler actually emits. +pub fn systems_status_from_config(config: &AppConfig) -> SystemsStatusPayload { + systems_status_from_probes(config, &read_host_uptime(), &live_unit_probes()) +} + /// Serve HTML dashboard, JSON (`/status.json` or Accept), or closed 404. pub fn serve_systems_status(config: &AppConfig, path: &str, accept: Option<&str>) -> Response { let path_only = path.split('?').next().unwrap_or(path); @@ -131,18 +508,35 @@ fn html_escape(s: &str) -> String { .replace('"', """) } +fn linked_text(text: &str, href: Option<&str>) -> String { + let t = html_escape(text); + match href { + Some(h) if !h.is_empty() => format!("{t}", html_escape(h)), + _ => t, + } +} + /// Public HTML dashboard. Named systems fields appear in the body. pub fn render_systems_status_html(payload: &SystemsStatusPayload) -> String { let host = html_escape(&payload.host); - let primary = html_escape(&payload.primary_domain); + let host_link = linked_text( + &payload.host, + https_public_href(&payload.host, "/").as_deref(), + ); + let primary_link = linked_text( + &payload.primary_domain, + https_public_href(&payload.primary_domain, "/").as_deref(), + ); let version = html_escape(payload.version); + let uptime = html_escape(&payload.uptime); let mut rows = String::new(); for c in &payload.systems { - let name = html_escape(c.name); - let role = html_escape(c.role); + let name = html_escape(&c.name); + let role = html_escape(&c.role); let state = html_escape(&c.state); + let label = linked_text(&c.name, c.href.as_deref()); rows.push_str(&format!( - "{name}{role}\ + "{label}{role}\ {state}\n" )); } @@ -176,7 +570,7 @@ body {{ flex-direction: column; }} main {{ - max-width: 36rem; + max-width: 40rem; margin: 2rem auto; padding: 0 1rem; flex: 1; @@ -184,9 +578,12 @@ main {{ .eyebrow {{ color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; font-size: 0.75rem; }} h1 {{ font-size: 1.5rem; margin: 0.5rem 0 1rem; }} .host {{ color: var(--muted); }} +a {{ color: var(--accent); }} +a:hover {{ color: var(--ok); }} table {{ width: 100%; border-collapse: collapse; }} th, td {{ text-align: left; padding: 0.5rem 0; border-bottom: 1px solid var(--border); }} th {{ color: var(--ok); }} +th a {{ color: var(--ok); }} footer {{ padding: 1rem; color: var(--muted); font-size: 0.75rem; }} @@ -194,10 +591,11 @@ footer {{ padding: 1rem; color: var(--muted); font-size: 0.75rem; }}

Surmount

Systems status

-

{host}

-

Primary domain {primary}. Public systems dashboard on the Axum HTTPS edge. Not the operator console.

+

{host_link}

+

Primary domain {primary_link}. Public systems dashboard on the Axum HTTPS edge. Not the operator console.

+

Host uptime {uptime}.

- + {rows}
systemrolestate
systemrolestatus
@@ -214,6 +612,15 @@ mod tests { use super::*; use crate::config::AppConfig; + fn probe(active: &str, sub: &str, load: &str, http: Option) -> ComponentProbe { + ComponentProbe { + active_state: active.into(), + sub_state: sub.into(), + load_state: load.into(), + http_status: http, + } + } + /// Named contract: payload is non-empty and names the systems the handler emits. #[test] fn systems_status_payload_emits_named_fields() { @@ -228,7 +635,7 @@ mod tests { !payload.systems.is_empty(), "systems list must be non-empty" ); - let names: Vec<&str> = payload.systems.iter().map(|c| c.name).collect(); + let names: Vec<&str> = payload.systems.iter().map(|c| c.name.as_str()).collect(); for must in ["mail", "edge", "tls", "mta_sts", "console"] { assert!( names.contains(&must), @@ -243,7 +650,10 @@ mod tests { assert!(html.contains("data-system=\"edge\"")); assert!(html.contains("data-system=\"tls\"")); assert!(!html.to_ascii_lowercase().contains("operator console nav")); - assert!(!html.contains("/mail")); + assert!( + !html.contains("href=\"/mail\"") && !html.contains("href=\"/mail/"), + "must not link to the operator console mail path: {html}" + ); assert!(!html.contains("Dashboard Overview")); let json = serde_json::to_value(&payload).unwrap(); assert_eq!(json["surface"], "systems_status"); @@ -292,4 +702,197 @@ mod tests { let resp = serve_systems_status(&cfg, "/api/v1/system", None); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } + + /// Names, hosts, and public URLs are real https hrefs for hosts this product serves. + #[test] + fn systems_status_html_includes_https_href_for_named_host() { + let cfg = AppConfig::example_test(); + let payload = systems_status_from_config(&cfg); + let html = render_systems_status_html(&payload); + assert!( + html.contains("href=\"https://status.example.test/\""), + "status host must be a clickable https link: {html}" + ); + assert!( + html.contains("href=\"https://mail.example.test/\""), + "mail host must be a clickable https link: {html}" + ); + assert!( + html.contains("href=\"https://services.example.test/\""), + "services host must be a clickable https link: {html}" + ); + assert!( + html.contains("href=\"https://example.test/\""), + "site host must be a clickable https link: {html}" + ); + assert!( + html.contains("href=\"https://splora.example.test/testnet/\""), + "Splora testnet path must be a clickable https link: {html}" + ); + assert!( + !html.contains("href=\"#\""), + "placeholder hash hrefs are not enough: {html}" + ); + assert!( + !html.contains("href=\"https://127."), + "must not put loopback IPs in hrefs: {html}" + ); + let hrefs: Vec<&str> = payload + .systems + .iter() + .filter_map(|c| c.href.as_deref()) + .collect(); + assert!( + hrefs.iter().any(|h| h.starts_with("https://")), + "payload must carry at least one https href: {hrefs:?}" + ); + assert!(https_public_href("127.0.0.1", "/").is_none()); + assert!(https_public_href("::1", "/").is_none()); + assert_eq!( + https_public_href("mail.example.test", "/"), + Some("https://mail.example.test/".into()) + ); + } + + /// Status strings come from the classifier given ActiveState/HTTP samples. + #[test] + fn classify_component_status_uses_unit_and_http_samples_not_constant_ok() { + assert_eq!( + classify_component_status(&probe("active", "running", "loaded", None)), + "running" + ); + assert_eq!( + classify_component_status(&probe("failed", "failed", "loaded", None)), + "failed" + ); + assert_eq!( + classify_component_status(&probe("inactive", "dead", "loaded", None)), + "inactive" + ); + assert_eq!( + classify_component_status(&probe("active", "listening", "loaded", None)), + "listening" + ); + assert_eq!( + classify_component_status(&probe("active", "running", "loaded", Some(502))), + "HTTP 502" + ); + assert_eq!( + classify_component_status(&probe("inactive", "dead", "not-found", None)), + "not installed" + ); + for sample in [ + probe("active", "running", "loaded", None), + probe("failed", "failed", "loaded", None), + probe("inactive", "dead", "loaded", None), + probe("active", "running", "loaded", Some(502)), + ] { + let got = classify_component_status(&sample); + assert_ne!(got, "ok", "must not hard-code ok; got {got} for {sample:?}"); + } + let cfg = AppConfig::example_test(); + let mut probes = BTreeMap::new(); + probes.insert( + "stalwart-mail.service".into(), + probe("failed", "failed", "loaded", None), + ); + probes.insert( + "surmount-management-ui.service".into(), + probe("active", "running", "loaded", Some(502)), + ); + let payload = systems_status_from_probes(&cfg, "12 minutes", &probes); + let mail = payload + .systems + .iter() + .find(|c| c.name == "mail") + .expect("mail row"); + assert_eq!(mail.state, "failed"); + let edge = payload + .systems + .iter() + .find(|c| c.name == "edge") + .expect("edge row"); + assert_eq!(edge.state, "HTTP 502"); + let html = render_systems_status_html(&payload); + assert!(html.contains("data-state=\"failed\"")); + assert!(html.contains("data-state=\"HTTP 502\"")); + assert!(!html.contains("data-state=\"ok\"")); + } + + /// Uptime formatter uses standing number law. Quote the exact strings. + #[test] + fn format_uptime_seconds_matches_standing_number_law() { + assert_eq!(format_uptime_seconds(45.0), "45 seconds"); + assert_eq!(format_uptime_seconds(720.0), "12 minutes"); + assert_eq!(format_uptime_seconds(5400.0), "1.5 hours"); + assert_eq!(format_uptime_seconds(90_000.0), "1.0 days"); + assert_eq!(parse_proc_uptime("12345.67 88888.00\n"), Some(12345.67)); + let dir = std::env::temp_dir(); + let path = dir.join(format!( + "surmount-status-uptime-{}-{}.txt", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + )); + std::fs::write(&path, "5400.12 100.00\n").unwrap(); + assert_eq!(read_proc_uptime_file(&path), Some(5400.12)); + let _ = std::fs::remove_file(&path); + let cfg = AppConfig::example_test(); + let payload = systems_status_from_probes(&cfg, "1.5 hours", &BTreeMap::new()); + let html = render_systems_status_html(&payload); + assert!( + html.contains("Host uptime 1.5 hours."), + "HTML must show the formatted uptime: {html}" + ); + assert_eq!(payload.uptime, "1.5 hours"); + } + + /// Inventory names mail, UI, status, Arti, and Splora testnet (not mainnet). + #[test] + fn systems_status_inventory_lists_mail_ui_status_arti_splora_testnet() { + let cfg = AppConfig::example_test(); + let items = systems_status_inventory(&cfg); + let ids: Vec<&str> = items.iter().map(|i| i.id).collect(); + for must in [ + "mail", + "console", + "status", + "arti", + "splora-testnet3", + "splora-testnet4", + "splora-mutinynet", + "splora-liquid", + "sshd", + ] { + assert!( + ids.contains(&must), + "inventory must name {must}; got {ids:?}" + ); + } + assert!( + !ids.iter().any(|id| id.contains("mainnet")), + "must not revive Splora mainnet: {ids:?}" + ); + assert!( + !ids.iter().any(|id| *id == "vaultwarden" || *id == "lake"), + "must not invent Vaultwarden or Lake: {ids:?}" + ); + let payload = systems_status_from_config(&cfg); + let names: Vec<&str> = payload.systems.iter().map(|c| c.name.as_str()).collect(); + assert!(names.len() > 5, "more than a stub handful: {names:?}"); + for must in ["mail", "console", "status", "arti", "splora-testnet3"] { + assert!( + names.contains(&must), + "payload must name {must}; got {names:?}" + ); + } + let show = parse_systemctl_show( + "Id=stalwart-mail.service\nLoadState=loaded\nActiveState=active\nSubState=running\n\n\ +Id=sshd.service\nLoadState=loaded\nActiveState=failed\nSubState=failed\n", + ); + assert_eq!(show["stalwart-mail.service"].active_state, "active"); + assert_eq!(show["sshd.service"].active_state, "failed"); + } } diff --git a/docs/DNS.md b/docs/DNS.md index d55dec5..9d9d4ba 100644 --- a/docs/DNS.md +++ b/docs/DNS.md @@ -7,7 +7,14 @@ real user mail. Validate send path with [mail-tester.com](https://www.mail-tester.com/) and your provider's blocklist checks. -**Last updated:** 2026-10-10. Operator direction: Google may send as +**Last updated:** 2026-10-24. Host `status.surmount.systems` is a +mail-host vhost A on the same records as apex / `mail` / `services` +(no CNAME, no AAAA while the other names have none). It is the public +systems dashboard on the Axum HTTPS edge, not the operator console. +Laptop DNS-01 intended leaf includes that name. See +[EDGE_AND_TLS.md](EDGE_AND_TLS.md). + +**Prior update:** 2026-10-10. Operator direction: Google may send as `surmount.systems`. Live SPF is `v=spf1 a:mail.surmount.systems include:_spf.google.com -all`. Public resolvers (including `1.1.1.1`) return that TXT. Our host @@ -415,6 +422,7 @@ send. Full ordered boxes: [Earn-trust checklist](#earn-trust-checklist-order-of- - [ ] **A** (and **AAAA** if you have v6) for `services.` -> VPS - [ ] **A/AAAA** for apex and/or `www` if you serve them from this VPS - [ ] **A/AAAA** for `mail.` -> same VPS (when mail or mail cert needs it) +- [ ] **A** for `status.` -> same records as apex / mail / services when the systems dashboard Host is served from this VPS - [ ] **MX** on apex -> `mail.` priority 10 (when accepting mail) - [ ] No orange-cloud / required CDN proxy on MX or submission names - [ ] Records match what host-local / sample config expects (direct to VPS) @@ -575,6 +583,7 @@ notes as you go. | **A / AAAA** | `mail.surmount.systems` | Mail host address(es) | Where MX points | | **A / AAAA** | `surmount.systems` | Apex (web / redirect) | Web + sometimes SPF `a` | | **A / AAAA** | `services.surmount.systems` | Management UI | Admin HTTPS | +| **A / AAAA** | `status.surmount.systems` | Public systems dashboard (Axum Host, not the operator console) | Same A as apex / mail / services; no AAAA while those names have none | | **A / AAAA** | `www.surmount.systems` | Optional www | Web | | **MX** | `surmount.systems` | Prefer `mail.surmount.systems` priority 10 | Inbound routing | | **PTR / rDNS** | Provider reverse zone for the VPS IP | Must match `mail.surmount.systems` | IP reputation | @@ -664,6 +673,8 @@ mail IN A 203.0.113.10 mail IN AAAA 2001:db8::10 services IN A 203.0.113.10 services IN AAAA 2001:db8::10 +status IN A 203.0.113.10 +; status is the public systems dashboard Host. Same A as apex/mail/services. ; --- SPF (adjust includes if you use a relay) --- @ IN TXT "v=spf1 mx a:mail.surmount.systems ip4:203.0.113.10 ip6:2001:db8::10 -all" @@ -721,25 +732,15 @@ Do **not** `enforce` until the operator asks. Dual-pin: [EDGE_AND_TLS.md](EDGE_A **Certificate hostnames (not a storage area network):** intended production leaf is **one** Let's Encrypt PEM pair (`with_single_cert`) -covering **20** names. Live leaf as of 2026-09-07 (LE production YE2, -laptop DNS-01, host ACME off, public CT) is still **18** names: -`baxterartworks.com`, `www.baxterartworks.com`, `btcfur.com`, -`www.btcfur.com`, `exophiles.org`, `www.exophiles.org`, -`iantuckerstudios.com`, `www.iantuckerstudios.com`, `nostrfurs.com`, -`www.nostrfurs.com`, `yiffa.app`, `www.yiffa.app`, `surmount.systems`, -`www.surmount.systems`, `mail.surmount.systems`, -`services.surmount.systems`, `mta-sts.surmount.systems`, -`mail.cryptoquick.com`. -The two names still missing from the live leaf are `cryptoquick.com` and -`www.cryptoquick.com`. That hostname mismatch is why cryptoquick HTTPS -fails verify. Validating A for those names succeeds (AD true). Leftover -parent DS key tag 2368 is gone. `mail.surmount.systems` and -`mail.cryptoquick.com` **are** on the live leaf (IMAP/SMTPS). Six extra -static zones are **live** HTTPS 200. After the tree lists the two missing -names, laptop `--issue` is operator-gated (Namecheap custody). `--live` -does **not** detect missing certificate hostnames. Do **not** -re-issue just to add a name already on that leaf. Esplora Hosts stay off -this leaf. Renew path: private host-profile `acme_domains` + +covering the private host-profile list plus the laptop floor +(`status.surmount.systems` is on that floor). Live leaf as of +2026-10-24 after laptop `--issue` (LE production YE1, DNS-01, host ACME +off) includes `status.surmount.systems`, cryptoquick apex/www, +`splora.surmount.systems`, the six extra static zones, and the +mail-host names. `--live` does **not** detect missing certificate +hostnames. Do **not** re-issue just to add a name already on that leaf. +Esplora Hosts stay off this leaf except the one Splora Host already on +it. Renew path: private host-profile `acme_domains` + `just laptop-renew-cert -- --issue --directory production` (Namecheap ClientIp = laptop egress). Extra-zone names need `--hook` from `nix run .#acme-dns-hook-namecheap-dispatch`. Mail-plane apply: diff --git a/docs/EDGE_AND_TLS.md b/docs/EDGE_AND_TLS.md index fcec432..1b5bfb7 100644 --- a/docs/EDGE_AND_TLS.md +++ b/docs/EDGE_AND_TLS.md @@ -4,9 +4,14 @@ How HTTPS reaches the management UI and (optionally) Stalwart HTTP. Mail protocol ports are **not** edge-proxied; they terminate on Stalwart. See [STACK.md](STACK.md) for the full path map. -**Last updated:** 2026-10-23 (Host `status.surmount.systems` is the public +**Last updated:** 2026-10-24 (Host `status.surmount.systems` is the public systems dashboard on the product Axum HTTPS edge, not nginx and not the -operator console. Intended certificate hostname list includes that name.) +operator console. Names and public hosts are https links. Status text +comes from systemd ActiveState / HTTP samples in ordinary English. +Host uptime comes from `/proc/uptime`. The inventory names mail, +services, this dashboard, Arti, SSH, and Splora testnet units that +exist; it does not invent Vaultwarden, Lake, or Splora mainnet. +Intended certificate hostname list includes `status.surmount.systems`.) Prior 2026-09-22 (one public Host `splora.surmount.systems`. Network paths on that Host, not per-network esplora Hosts. The network prefix is stripped before the Unix socket. The query string is kept. @@ -291,16 +296,24 @@ nginx features. Module file remains until operators no longer need dual-run. Location from untrusted input. Default allowlist: services, apex, www, mail, mta-sts., status., plus any extra static Hosts from `SURMOUNT_STATIC_VHOSTS` / `SURMOUNT_STATIC_VHOSTS_FILE`. -- **Systems dashboard Host (2026-10-23):** `status.{primaryDomain}` +- **Systems dashboard Host (2026-10-24):** `status.{primaryDomain}` (production: `status.surmount.systems`) is a public systems status surface on this same Axum process. GET `/` is HTML; GET `/status.json` is JSON. Named fields: `surface`, `service`, `version`, `host`, - `primary_domain`, and `systems` rows `mail`, `edge`, `tls`, `mta_sts`, - `console` (config-derived states, not RAM/disk/IP). Not the operator - console. `/health` and `/api/health` stay edge probes. Other `/api/*` - is 404. Laptop DNS-01 intended leaf includes `status.surmount.systems`. - Live HTTPS after switch is operator-owned. Do not put nginx back as - product edge. + `primary_domain`, `uptime`, and `systems` rows including `mail`, + `edge`, `tls`, `mta_sts`, `console`, `status`, `site`, `arti`, `sshd`, + and Splora testnet names (`splora-testnet3`, `splora-testnet4`, + `splora-mutinynet`, `splora-liquid`). Names and public hosts are + clickable `https://` hostname links (mail, services, apex, status, + `splora.{primary}` testnet paths, MTA-STS policy). Status text is + ordinary English from systemd ActiveState / SubState and optional HTTP + samples (running, failed, inactive, listening, HTTP 502), not a + constant ok and not RAM/disk/IP. Host uptime is `/proc/uptime` formatted + as seconds, minutes (at most 60 minutes), N.N hours (through 24 hours), + or N.N days. Not the operator console. `/health` and `/api/health` + stay edge probes. Other `/api/*` is 404. Laptop DNS-01 intended leaf + includes `status.surmount.systems`. Live HTTPS after switch is + operator-owned. Do not put nginx back as product edge. - **Extra static Hosts (DS3018xs trees, 2026-08-19):** proven public HTML sites are served from the same Axum process via a Host -> document root map (`SURMOUNT_STATIC_VHOSTS` JSON, or Nix diff --git a/docs/SEARCH_AND_UI.md b/docs/SEARCH_AND_UI.md index 54fd82b..3edccf9 100644 --- a/docs/SEARCH_AND_UI.md +++ b/docs/SEARCH_AND_UI.md @@ -121,7 +121,9 @@ static sites; `just deploy` is the NixOS generation (the host switch). `just deploy-host` is that switch with no default target. Operator console stays on `services.surmount.systems`. Public systems dashboard is Host `status.surmount.systems` on the same Axum edge. -COMING SOON leftover is closed. +That page links public hosts over https, shows ordinary unit/HTTP +status text, and prints host uptime from `/proc/uptime`. COMING SOON +leftover is closed. ## Phases From 0b6ae79782cdbcf476375528fa6563c7f5308eef Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Sat, 24 Oct 2026 14:49:57 -0600 Subject: [PATCH 07/22] hdd --- docs/OPS.md | 8 +++++ justfile | 3 ++ modules/btop.conf | 2 ++ tests/btop-contract.nix | 65 +++++++++++++++++++++++++++++++++++++++++ tests/module-eval.nix | 29 ++++++++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 tests/btop-contract.nix diff --git a/docs/OPS.md b/docs/OPS.md index 34db7df..6f70932 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -817,6 +817,14 @@ background, block graphs, square corners). Canonical file is `/etc/xdg/btop/btop.conf`. Activation also links `/root/.config/btop/btop.conf` (and the same path under any defined normal user's home) so `just btop` loads it without extra flags. +Disk usage follows fstab (`use_fstab = true`). Persist extra disks with +NixOS `fileSystems` (that is how NixOS writes `/etc/fstab`), not by +listing every mount. The extra disk is LABEL=hdd at `/mnt/hdd` via +private host-local `fileSystems` (`/dev/disk/by-label/hdd`, ext4, +`nofail`). Public sample eval does not import that overlay. +`disks_filter` stays empty. Targeted eval: `just test-btop-eval`. +A live `just btop` does not reload a later store conf. Quit btop and +run `just btop` again after the host switch. ### Host hardware probe (`just host-inxi`) diff --git a/justfile b/justfile index 9ae5744..37039e4 100644 --- a/justfile +++ b/justfile @@ -422,6 +422,9 @@ test-eternal-terminal-eval: test-inxi-eval: nix eval --impure --json --file tests/inxi-contract.nix +test-btop-eval: + nix eval --impure --json --file tests/btop-contract.nix + test-perf-tools-eval: nix eval --impure --json --file tests/perf-tools-contract.nix diff --git a/modules/btop.conf b/modules/btop.conf index e4b1253..10ff2cc 100644 --- a/modules/btop.conf +++ b/modules/btop.conf @@ -198,6 +198,8 @@ show_disks = true only_physical = true #* Read disks list from /etc/fstab. This also disables only_physical. +#* True: disk usage follows fstab. Persist extra disks with fileSystems, +#* not by listing every mount. use_fstab = true #* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) diff --git a/tests/btop-contract.nix b/tests/btop-contract.nix new file mode 100644 index 0000000..c0c53b8 --- /dev/null +++ b/tests/btop-contract.nix @@ -0,0 +1,65 @@ +# Targeted btop.conf contract (not the 14-minute module-eval suite). +# Impure flake pin so `just test-btop-eval` can nix-eval this file. +# Named contract: shipped conf has use_fstab = true so disk usage +# follows /etc/fstab (NixOS fileSystems). Extra disks persist with +# fileSystems, not by listing every mount. This public eval does not +# import private host-local, so /mnt/hdd is absent here. Never embeds +# host SKUs, addresses, or disk capacity. +let + flake = builtins.getFlake (toString ./..); + system = builtins.currentSystem; + inherit (flake.inputs.nixpkgs) lib; + evalBtop = + extra: + lib.nixosSystem { + inherit system; + modules = [ + ../modules/options.nix + ../modules/btop.nix + { + system.stateVersion = "26.05"; + networking.hostName = "btop-eval"; + fileSystems."/" = { + device = "nodev"; + fsType = "ext4"; + }; + boot.loader.grub.enable = false; + surmount = extra // { + enable = extra.enable or true; + }; + } + ]; + }; + confText = + e: + let + entry = e.config.environment.etc."xdg/btop/btop.conf" or null; + in + if entry == null then + "" + else if entry ? source then + builtins.readFile entry.source + else if entry ? text then + entry.text + else + ""; + + t37b-btop-use-fstab-true = + let + e = evalBtop { }; + text = confText e; + in + assert text != ""; + assert lib.hasInfix "use_fstab = true" text; + assert lib.hasInfix ''disks_filter = ""'' text; + assert lib.hasInfix "show_disks = true" text; + assert lib.hasInfix "only_physical = true" text; + assert !(lib.hasInfix "use_fstab = false" text); + # Public modules-only eval. Host-local hardware-configuration.nix + # owns fileSystems."/mnt/hdd"; this file does not import it. + assert !(e.config.fileSystems ? "/mnt/hdd"); + "t37b-btop-use-fstab-true-ok"; +in +{ + t37b-btop-use-fstab-true = t37b-btop-use-fstab-true; +} diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 3be9964..3003c5a 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -2252,6 +2252,34 @@ let assert builtins.any (r: lib.hasInfix "/root/.config/btop/btop.conf" r) rules; "t37-btop-local-config-ok"; + # Host btop disk usage follows fstab (NixOS fileSystems). Extra disks + # persist in private host-local, not in this public eval. + t37b-btop-use-fstab-true = + let + e = evalSurmount { }; + etc = e.config.environment.etc; + entry = etc."xdg/btop/btop.conf" or null; + text = + if entry == null then + "" + else if entry ? source then + builtins.readFile entry.source + else if entry ? text then + entry.text + else + ""; + in + assert entry != null; + assert lib.hasInfix "use_fstab = true" text; + assert lib.hasInfix ''disks_filter = ""'' text; + assert lib.hasInfix "show_disks = true" text; + assert lib.hasInfix "only_physical = true" text; + assert !(lib.hasInfix "use_fstab = false" text); + # Public sample host only. Host-local hardware-configuration.nix + # owns fileSystems."/mnt/hdd"; this eval does not import it. + assert !(e.config.fileSystems ? "/mnt/hdd"); + "t37b-btop-use-fstab-true-ok"; + t42-guest-root-justfile = let e = evalSurmount { }; @@ -3282,6 +3310,7 @@ let t36-btop-system-package t41-inxi-system-package t37-btop-local-config + t37b-btop-use-fstab-true t42-guest-root-justfile t43-scram-watch-highest-priority t43b-swapfile-enable-without-path-asserts From 418b782e259f6561edd2a01b6d08b91057b52ce7 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Mon, 26 Oct 2026 14:37:29 -0600 Subject: [PATCH 08/22] optimizations --- docs/OPS.md | 33 +++++ hosts/mail-vps/configuration.nix | 3 + modules/default.nix | 2 + modules/mail-host-cpu.nix | 206 +++++++++++++++++++++++++++++++ tests/module-eval.nix | 52 ++++++++ 5 files changed, 296 insertions(+) create mode 100644 modules/mail-host-cpu.nix diff --git a/docs/OPS.md b/docs/OPS.md index 6f70932..d74bb42 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -848,6 +848,39 @@ Target comes from `--target`, `SURMOUNT_DEPLOY_TARGET`, or plan names in the public tree. Prefer not to paste RAM/swap sizes into product docs. +### Mail-host compiler CPU (x86-64-v3) + +The mail host compiles its own Rust and C/C++ source builds for +`x86-64-v3`. That is the measured guest CPU: avx, avx2, bmi1, bmi2, +f16c, fma, lzcnt (kernel flag `abm`), movbe, popcnt, sse4.1, sse4.2, +and ssse3. AVX-512 is absent, so this is not `x86-64-v4`. QEMU iothread +was not sampled. Do not claim `iothread=1`. + +`hosts/mail-vps/configuration.nix` sets `surmount.mailHostCpu.enable`. +`modules/mail-host-cpu.nix` then sets `gccArch` and `rustTargetCpu` to +`x86-64-v3`, `NIX_CFLAGS_COMPILE` and `NIX_CXXFLAGS_COMPILE` to +`-march=x86-64-v3`, and `CARGO_BUILD_RUSTFLAGS` to +`-C target-cpu=x86-64-v3`. The nixpkgs cc-wrapper applies +`NIX_CFLAGS_COMPILE` to both C and C++. This nixpkgs does not map +`gcc.arch` onto rustc, so the Rust flag is set on the derivation. + +`nixpkgs.hostPlatform.gcc.arch` stays unset. The cc-wrapper bakes +`gcc.arch` into the stdenv, and that would rebuild the NixOS closure +and force the same march onto every package. Generic CI eval +(`tests/module-eval.nix` without the mail-vps host file) leaves +`surmount.mailHostCpu.enable` false, so those flags are empty. Flake +`packages.*` (GitHub Actions `checks.*.ci`, laptop `nix build`) are +built from `mkPkgsBare` and are not wrapped. + +Do not use `-march=native` in this public flake. Stalwart stays a +release binary (`dontBuild`); this march does not recompile that +tarball or the RocksDB inside it. Splora and bitcoind or elementsd are +on the tune list only when a derivation references those attributes. +The sample host does not enable `surmount.sploraIndexer` and does not +start bitcoind. + +Contract: `t62-mail-host-cpu-v3` in `tests/module-eval.nix`. + ### Remote Nix builder (operator 2026-08-17; Lake default-off 2026-08-25) **surmount-1** is the allowed remote Nix builder (ssh-ng). Mail and builder diff --git a/hosts/mail-vps/configuration.nix b/hosts/mail-vps/configuration.nix index f499f03..e1ee3e5 100644 --- a/hosts/mail-vps/configuration.nix +++ b/hosts/mail-vps/configuration.nix @@ -100,6 +100,9 @@ # ---- Surmount stack ------------------------------------------------------ surmount = { enable = true; + # Measured guest CPU is x86-64-v3 (no avx512). Source builds only. + # Flake packages.* and the generic CI host stay untuned. + mailHostCpu.enable = true; primaryDomain = "surmount.systems"; mailHostname = "mail.surmount.systems"; servicesHostname = "services.surmount.systems"; diff --git a/modules/default.nix b/modules/default.nix index 5e1b6de..89d5c72 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -46,5 +46,7 @@ ./speedtest.nix # Flexible I/O tester (fio) for operator HDD sequential vs random probes. ./perf-tools.nix + # Mail-host source builds: x86-64-v3. Default off (CI / generic). + ./mail-host-cpu.nix ]; } diff --git a/modules/mail-host-cpu.nix b/modules/mail-host-cpu.nix new file mode 100644 index 0000000..ab0d1b4 --- /dev/null +++ b/modules/mail-host-cpu.nix @@ -0,0 +1,206 @@ +# Mail-host source builds request x86-64-v3. CI and generic laptop +# package builds do not. nixpkgs.hostPlatform.gcc.arch stays unset: +# the cc-wrapper bakes that into every NixOS derivation and would +# rebuild the closure. Per-derivation NIX_CFLAGS_COMPILE is the same +# -march the wrapper would add from gcc.arch, and this nixpkgs does +# not map gcc.arch to rustc -C target-cpu. +# +# Stalwart is a release binary (dontBuild). Do not retune it. +# Do not use -march=native. Do not request x86-64-v4 or avx512. + +{ + config, + lib, + ... +}: +let + cfg = config.surmount.mailHostCpu; + inherit (lib) mkIf mkOption types; + march = "x86-64-v3"; + nixCflagsCompile = "-march=${march}"; + cargoBuildRustflags = "-C target-cpu=${march}"; + # Source builds that can land in the mail-host closure. + # bitcoind and elementsd are tuned only if something references them. + # This module does not enable them and does not start them. + tunedPackages = [ + "surmount-management-ui" + "artiOnionService" + "acme-dns-hook-namecheap" + "surmount-private-data" + "surmount-host-logs" + "surmount-shc" + "surmount-niced-builder" + "surmount-scram" + "surmount-leftover-homes" + "surmount-host-probe" + "surmount-static-sites" + "surmount-diskstation" + "surmount-deploy-host" + "surmount-dns-zone" + "surmount-domain-audit" + "surmount-host-cutover" + "surmount-acme-namecheap" + "surmount-stalwart-ops" + "surmount-mail-import" + "surmount-secrets-install" + "surmount-secrets-prompt" + "surmount-rekey" + "splora" + "splora-liquid" + "bitcoind" + "elementsd" + ]; + + addFlag = + flag: old: name: + let + top = if old ? ${name} && old.${name} != null then toString old.${name} else ""; + fromEnv = + if (old.env or { }) ? ${name} && old.env.${name} != null then toString old.env.${name} else ""; + combined = lib.concatStringsSep " " ( + lib.filter (s: s != "") [ + top + fromEnv + ] + ); + in + if flag == "" then + combined + else if lib.hasInfix flag combined then + combined + else + lib.concatStringsSep " " ( + lib.filter (s: s != "") [ + flag + combined + ] + ); + + tuneDrv = + pkg: + if !(lib.isDerivation pkg) then + pkg + else + pkg.overrideAttrs (old: { + env = (old.env or { }) // { + NIX_CFLAGS_COMPILE = addFlag nixCflagsCompile old "NIX_CFLAGS_COMPILE"; + NIX_CXXFLAGS_COMPILE = addFlag nixCflagsCompile old "NIX_CXXFLAGS_COMPILE"; + CARGO_BUILD_RUSTFLAGS = addFlag cargoBuildRustflags old "CARGO_BUILD_RUSTFLAGS"; + }; + }); + + artifactsOf = + pkg: + if pkg ? cargoArtifacts && pkg.cargoArtifacts != null then + pkg.cargoArtifacts + else if pkg ? env && pkg.env ? cargoArtifacts then + pkg.env.cargoArtifacts + else + null; + + tune = + pkg: + let + flagged = tuneDrv pkg; + artifacts = artifactsOf pkg; + in + if lib.isDerivation artifacts then + flagged.overrideAttrs (_old: { + cargoArtifacts = tuneDrv artifacts; + }) + else + flagged; + + # mkAfter so this runs after the flake surmountOverlay, which + # replaces these attrs with the generic package set. + mailHostCpuOverlay = + _final: prev: + lib.foldl' lib.recursiveUpdate { } ( + map ( + name: + lib.optionalAttrs (prev ? ${name}) { + ${name} = tune prev.${name}; + } + ) tunedPackages + ); +in +{ + options.surmount.mailHostCpu = { + enable = lib.mkEnableOption '' + compile mail-host Rust and C/C++ source builds for x86-64-v3. + Off for the generic CI host. Does not set nixpkgs.hostPlatform. + ''; + + gccArch = mkOption { + type = types.str; + default = ""; + description = '' + gcc.arch token for mail-host source builds. Empty when enable + is false. Not nixpkgs.hostPlatform.gcc.arch. + ''; + }; + + rustTargetCpu = mkOption { + type = types.str; + default = ""; + description = '' + rustc -C target-cpu value for mail-host source builds. + Empty when enable is false. + ''; + }; + + nixCflagsCompile = mkOption { + type = types.str; + default = ""; + description = '' + NIX_CFLAGS_COMPILE / NIX_CXXFLAGS_COMPILE prefix for mail-host + source builds. The nixpkgs cc-wrapper applies NIX_CFLAGS_COMPILE + to C and C++. Empty when enable is false. + ''; + }; + + cargoBuildRustflags = mkOption { + type = types.str; + default = ""; + description = '' + CARGO_BUILD_RUSTFLAGS prefix for mail-host source builds. + Empty when enable is false. + ''; + }; + + tunedPackages = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Package attribute names that receive the mail-host flags when + enable is true and the attribute exists. Stalwart's release + binary is not in this list. + ''; + }; + }; + + config = mkIf cfg.enable { + surmount.mailHostCpu.gccArch = march; + surmount.mailHostCpu.rustTargetCpu = march; + surmount.mailHostCpu.nixCflagsCompile = nixCflagsCompile; + surmount.mailHostCpu.cargoBuildRustflags = cargoBuildRustflags; + surmount.mailHostCpu.tunedPackages = tunedPackages; + + nixpkgs.overlays = lib.mkAfter [ mailHostCpuOverlay ]; + + assertions = [ + { + assertion = + cfg.gccArch == march + && cfg.rustTargetCpu == march + && cfg.nixCflagsCompile == nixCflagsCompile + && cfg.cargoBuildRustflags == cargoBuildRustflags + && !(lib.hasInfix "native" cfg.nixCflagsCompile) + && !(lib.hasInfix "native" cfg.cargoBuildRustflags) + && !(lib.hasInfix "x86-64-v4" cfg.gccArch) + && !(lib.hasInfix "avx512" cfg.cargoBuildRustflags); + message = "surmount.mailHostCpu must request x86-64-v3, not native, v4, or avx512."; + } + ]; + }; +} diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 3003c5a..169b5d7 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -3227,6 +3227,57 @@ let assert !(lib.hasInfix "esplora-surmount-systems" toml); "t61-splora-portal-host-and-onion-extras-ok"; + # Mail sample host requests x86-64-v3 for Rust and for C/C++. + # The generic CI host (no hosts/mail-vps/configuration.nix) must not. + # nixpkgs.hostPlatform.gcc.arch stays unset: that flag is baked into the + # cc-wrapper and would rebuild the whole NixOS closure. + t62-mail-host-cpu-v3 = + let + mail = evalSurmount { + __extraModules = [ ../hosts/mail-vps/configuration.nix ]; + }; + generic = evalSurmount { }; + mailCpu = mail.config.surmount.mailHostCpu; + genericCpu = generic.config.surmount.mailHostCpu; + # nixosSystem passes nixpkgs.system. hostPlatform stays unset unless + # a module sets it. localSystem is the elaborated build platform. + hostArch = + e: + if e.options.nixpkgs.hostPlatform.isDefined then + e.config.nixpkgs.hostPlatform.gcc.arch or null + else + null; + localArch = e: e.config.nixpkgs.localSystem.gcc.arch or null; + in + assert mailCpu.enable == true; + assert mailCpu.gccArch == "x86-64-v3"; + assert mailCpu.rustTargetCpu == "x86-64-v3"; + assert mailCpu.nixCflagsCompile == "-march=x86-64-v3"; + assert mailCpu.cargoBuildRustflags == "-C target-cpu=x86-64-v3"; + assert !(lib.hasInfix "native" mailCpu.nixCflagsCompile); + assert !(lib.hasInfix "native" mailCpu.cargoBuildRustflags); + assert !(lib.hasInfix "x86-64-v4" mailCpu.gccArch); + assert !(lib.hasInfix "avx512" mailCpu.cargoBuildRustflags); + assert builtins.elem "surmount-management-ui" mailCpu.tunedPackages; + assert builtins.elem "splora" mailCpu.tunedPackages; + assert builtins.elem "splora-liquid" mailCpu.tunedPackages; + assert builtins.elem "bitcoind" mailCpu.tunedPackages; + assert builtins.elem "elementsd" mailCpu.tunedPackages; + assert !(builtins.elem "stalwart-mail" mailCpu.tunedPackages); + assert mail.config.surmount.sploraIndexer.enable == false; + assert mail.config.surmount.lake.enable == false; + assert hostArch mail != "x86-64-v3"; + assert localArch mail != "x86-64-v3"; + assert genericCpu.enable == false; + assert genericCpu.gccArch == ""; + assert genericCpu.rustTargetCpu == ""; + assert genericCpu.nixCflagsCompile == ""; + assert genericCpu.cargoBuildRustflags == ""; + assert genericCpu.tunedPackages == [ ]; + assert hostArch generic != "x86-64-v3"; + assert localArch generic != "x86-64-v3"; + "t62-mail-host-cpu-v3-ok"; + results = [ t1-defaults t1b-p1-free-443-plan-and-firewall @@ -3348,6 +3399,7 @@ let t59-splora-remote-ui-extra-groups-when-proxy-on t60-splora-remote-enable-needs-cookie-path t61-splora-portal-host-and-onion-extras + t62-mail-host-cpu-v3 ]; in { From f0f873ef3e92a5257e0b2ce280753fea2cf3e386 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Mon, 26 Oct 2026 17:58:56 -0600 Subject: [PATCH 09/22] fixes --- Cargo.lock | 54 ++++++++-------- docs/OPS.md | 67 +++++++++++-------- modules/default.nix | 1 + modules/mail-host-cpu.nix | 48 ++++++++++---- modules/mail-host-rust-cpu.nix | 115 +++++++++++++++++++++++++++++++++ tests/mail-host-rust-cpu.nix | 99 ++++++++++++++++++++++++++++ tests/module-eval.nix | 25 +++++-- 7 files changed, 339 insertions(+), 70 deletions(-) create mode 100644 modules/mail-host-rust-cpu.nix create mode 100644 tests/mail-host-rust-cpu.nix diff --git a/Cargo.lock b/Cargo.lock index cfaee58..3ca4570 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,9 +166,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" dependencies = [ "rustversion", ] @@ -586,9 +586,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" dependencies = [ "find-msvc-tools", "jobserver", @@ -734,9 +734,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "memchr", @@ -953,18 +953,18 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "defmt" -version = "0.3.100" +version = "0.3.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +checksum = "4deef977660e373c458df605a913d4ab121db0ccd18d1ad6976dff7d37cddb53" dependencies = [ - "defmt 1.1.1", + "defmt 1.2.0", ] [[package]] name = "defmt" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -972,21 +972,21 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "defmt-parser" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" dependencies = [ "thiserror 2.0.21", ] @@ -1162,7 +1162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed" dependencies = [ "autocfg", - "defmt 0.3.100", + "defmt 0.3.101", "heapless", "serde", ] @@ -1200,9 +1200,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "fluent" @@ -2295,9 +2295,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "litemap" @@ -2960,9 +2960,9 @@ dependencies = [ [[package]] name = "reactive_graph" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f11fac67625645add76313dab8dae510fca0654d96ef98fd8f961a7d2e22c1" +checksum = "3bcec36a2d1b9a53a4129b3270b19e3df6f6b88c2e7c97ed9404dc9307f137d7" dependencies = [ "any_spawner", "async-lock", @@ -3303,9 +3303,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" dependencies = [ "windows-sys 0.61.2", ] @@ -4328,9 +4328,9 @@ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-normalization" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" dependencies = [ "tinyvec", ] diff --git a/docs/OPS.md b/docs/OPS.md index d74bb42..f4fdae4 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -850,34 +850,49 @@ product docs. ### Mail-host compiler CPU (x86-64-v3) -The mail host compiles its own Rust and C/C++ source builds for -`x86-64-v3`. That is the measured guest CPU: avx, avx2, bmi1, bmi2, -f16c, fma, lzcnt (kernel flag `abm`), movbe, popcnt, sse4.1, sse4.2, -and ssse3. AVX-512 is absent, so this is not `x86-64-v4`. QEMU iothread -was not sampled. Do not claim `iothread=1`. +The mail host compiles C and C++ for `x86-64-v3`, including libraries +those packages build, not only a named list of top-level derivations. +That is the measured guest CPU: avx, avx2, bmi1, bmi2, f16c, fma, lzcnt +(kernel flag `abm`), movbe, popcnt, sse4.1, sse4.2, and ssse3. AES is +present on that guest. This module does not add a separate `-maes` or +any flag above `x86-64-v3`. AVX-512 is absent, so this is not +`x86-64-v4`. QEMU iothread was not sampled. Do not claim `iothread=1`. `hosts/mail-vps/configuration.nix` sets `surmount.mailHostCpu.enable`. -`modules/mail-host-cpu.nix` then sets `gccArch` and `rustTargetCpu` to -`x86-64-v3`, `NIX_CFLAGS_COMPILE` and `NIX_CXXFLAGS_COMPILE` to -`-march=x86-64-v3`, and `CARGO_BUILD_RUSTFLAGS` to -`-C target-cpu=x86-64-v3`. The nixpkgs cc-wrapper applies -`NIX_CFLAGS_COMPILE` to both C and C++. This nixpkgs does not map -`gcc.arch` onto rustc, so the Rust flag is set on the derivation. - -`nixpkgs.hostPlatform.gcc.arch` stays unset. The cc-wrapper bakes -`gcc.arch` into the stdenv, and that would rebuild the NixOS closure -and force the same march onto every package. Generic CI eval -(`tests/module-eval.nix` without the mail-vps host file) leaves -`surmount.mailHostCpu.enable` false, so those flags are empty. Flake -`packages.*` (GitHub Actions `checks.*.ci`, laptop `nix build`) are -built from `mkPkgsBare` and are not wrapped. - -Do not use `-march=native` in this public flake. Stalwart stays a -release binary (`dontBuild`); this march does not recompile that -tarball or the RocksDB inside it. Splora and bitcoind or elementsd are -on the tune list only when a derivation references those attributes. -The sample host does not enable `surmount.sploraIndexer` and does not -start bitcoind. +`modules/mail-host-cpu.nix` then sets `nixpkgs.hostPlatform` to this +host's system with `gcc.arch = "x86-64-v3"`. The nixpkgs cc-wrapper +reads `targetPlatform.gcc.arch` and writes `-march=x86-64-v3` into +`cc-cflags-before`. That flag applies to every C and C++ compile that +uses this host's stdenv, including dependencies such as nixpkgs +RocksDB, libsodium, and bitcoind or elementsd libraries when those +derivations are built. The next switch rebuilds that C and C++ closure. +The module is opt-in. Generic CI eval does not enable it. + +The same module still sets `gccArch` and `rustTargetCpu` to +`x86-64-v3`, and it still prefixes `NIX_CFLAGS_COMPILE`, +`NIX_CXXFLAGS_COMPILE`, and `CARGO_BUILD_RUSTFLAGS` on a named +top-level list. The nixpkgs cc-wrapper applies `NIX_CFLAGS_COMPILE` to +both C and C++. This nixpkgs does not map `gcc.arch` onto rustc, so +the Rust `-C target-cpu=x86-64-v3` flag stays on those named +derivations only. The host platform march is what covers C and C++ +dependency builds. + +Generic CI eval (`tests/module-eval.nix` without the mail-vps host +file) leaves `surmount.mailHostCpu.enable` false, so +`nixpkgs.hostPlatform.gcc.arch` stays unset and the named flags are +empty. Flake `packages.*` (GitHub Actions `checks.*.ci`, laptop +`nix build`) are built from `mkPkgsBare` and are not wrapped. Do not +set this march on those package sets. + +Do not use `-march=native` in this public flake. Do not request +`x86-64-v4` or `-mavx512`. Stalwart stays a release binary +(`dontBuild`). This march does not recompile that tarball or the +RocksDB inside it. A nixpkgs RocksDB built as a dependency of something +this host compiles is in scope. The vendored RocksDB inside the +Stalwart release tarball is not. Splora and bitcoind or elementsd are +on the named tune list only when a derivation references those +attributes. The sample host does not enable `surmount.sploraIndexer` +and does not start bitcoind. Contract: `t62-mail-host-cpu-v3` in `tests/module-eval.nix`. diff --git a/modules/default.nix b/modules/default.nix index 89d5c72..11c8961 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -48,5 +48,6 @@ ./perf-tools.nix # Mail-host source builds: x86-64-v3. Default off (CI / generic). ./mail-host-cpu.nix + ./mail-host-rust-cpu.nix ]; } diff --git a/modules/mail-host-cpu.nix b/modules/mail-host-cpu.nix index ab0d1b4..3b73bc8 100644 --- a/modules/mail-host-cpu.nix +++ b/modules/mail-host-cpu.nix @@ -1,12 +1,17 @@ -# Mail-host source builds request x86-64-v3. CI and generic laptop -# package builds do not. nixpkgs.hostPlatform.gcc.arch stays unset: -# the cc-wrapper bakes that into every NixOS derivation and would -# rebuild the closure. Per-derivation NIX_CFLAGS_COMPILE is the same -# -march the wrapper would add from gcc.arch, and this nixpkgs does -# not map gcc.arch to rustc -C target-cpu. +# Mail-host C and C++ builds, including their libraries, request +# x86-64-v3. When this module is enabled it sets +# nixpkgs.hostPlatform.gcc.arch. The cc-wrapper reads that into +# cc-cflags-before, so dependency builds (nixpkgs RocksDB, libsodium, +# bitcoind and elementsd libraries, and the rest of this host's +# stdenv) get -march=x86-64-v3. The next switch rebuilds that closure. +# CI, the generic eval host, and flake packages.* (mkPkgsBare) leave +# the module off, so they stay untuned. # -# Stalwart is a release binary (dontBuild). Do not retune it. -# Do not use -march=native. Do not request x86-64-v4 or avx512. +# This nixpkgs does not map gcc.arch to rustc -C target-cpu. Named +# top-level derivations still get CARGO_BUILD_RUSTFLAGS. Stalwart is +# a release binary (dontBuild). Do not retune that tarball or the +# RocksDB inside it. Do not use -march=native. Do not request +# x86-64-v4 or avx512. { config, @@ -127,8 +132,9 @@ in { options.surmount.mailHostCpu = { enable = lib.mkEnableOption '' - compile mail-host Rust and C/C++ source builds for x86-64-v3. - Off for the generic CI host. Does not set nixpkgs.hostPlatform. + compile mail-host C and C++ for x86-64-v3, including libraries + built for this host. Sets nixpkgs.hostPlatform.gcc.arch when + enabled. Off for the generic CI host and for flake packages.*. ''; gccArch = mkOption { @@ -136,7 +142,9 @@ in default = ""; description = '' gcc.arch token for mail-host source builds. Empty when enable - is false. Not nixpkgs.hostPlatform.gcc.arch. + is false. When enable is true this is also + nixpkgs.hostPlatform.gcc.arch, which the cc-wrapper applies + to every C and C++ compile in this host's package set. ''; }; @@ -186,6 +194,15 @@ in surmount.mailHostCpu.cargoBuildRustflags = cargoBuildRustflags; surmount.mailHostCpu.tunedPackages = tunedPackages; + # cc-wrapper machineFlags reads targetPlatform.gcc.arch and writes + # -march into nix-support/cc-cflags-before. That covers dependency + # builds. It does not retune a dontBuild release binary. The named + # overlay below still sets Rust target-cpu; gcc.arch does not. + nixpkgs.hostPlatform = { + system = config.nixpkgs.system; + gcc.arch = march; + }; + nixpkgs.overlays = lib.mkAfter [ mailHostCpuOverlay ]; assertions = [ @@ -201,6 +218,15 @@ in && !(lib.hasInfix "avx512" cfg.cargoBuildRustflags); message = "surmount.mailHostCpu must request x86-64-v3, not native, v4, or avx512."; } + { + assertion = + (config.nixpkgs.hostPlatform.gcc.arch or null) == march + && (config.nixpkgs.hostPlatform.system or "") == "x86_64-linux" + && !(lib.hasInfix "native" (config.nixpkgs.hostPlatform.gcc.arch or "")) + && (config.nixpkgs.hostPlatform.gcc.arch or "") != "x86-64-v4" + && !(lib.hasInfix "avx512" (config.nixpkgs.hostPlatform.gcc.arch or "")); + message = "surmount.mailHostCpu must set nixpkgs.hostPlatform.gcc.arch to x86-64-v3 on x86_64-linux, not native, v4, or avx512."; + } ]; }; } diff --git a/modules/mail-host-rust-cpu.nix b/modules/mail-host-rust-cpu.nix new file mode 100644 index 0000000..1176644 --- /dev/null +++ b/modules/mail-host-rust-cpu.nix @@ -0,0 +1,115 @@ +# Mail-host Cargo builds: -C target-cpu=x86-64-v3 on every rustc Cargo +# runs, including dependency crates and proc-macros. Gated on +# surmount.mailHostCpu.enable (no second option). enable false installs +# nothing, so CI and mkPkgsBare stay generic. +# +# Crane buildDepsOnly is a separate derivation (cargoArtifacts). A flag +# on the top-level package does not reach that derivation. Stamp both. +# +# Duplicate -C target-cpu is a rustc hard error. The wrapper skips when +# argv or RUSTFLAGS already contains target-cpu= (the other module may +# already set CARGO_BUILD_RUSTFLAGS on the same package). +# +# Does not rebuild rustc. RUSTC_WRAPPER calls the rustc already on the +# derivation (nixpkgs-rust, or Splora's rust-bin). + +{ + config, + lib, + ... +}: +let + cfg = config.surmount.mailHostCpu; + # C and C++. MARCH for those stays in mail-host-cpu.nix. + cppNames = [ + "bitcoind" + "elementsd" + ]; + # splora-http is craneLib.buildPackage in the Splora flake. This + # repo's package overlay does not always expose the attr. + rustNames = lib.unique (lib.subtractLists cppNames cfg.tunedPackages ++ [ "splora-http" ]); + + hookText = '' + real=$1 + if [ -z "$real" ]; then + echo "mail-host rustc wrapper: missing rustc path" >&2 + exit 1 + fi + shift + need=1 + for arg in "$@"; do + case "$arg" in + *target-cpu=*) need=0 ;; + esac + done + case "''${RUSTFLAGS-}" in + *target-cpu=*) need=0 ;; + esac + case "''${CARGO_ENCODED_RUSTFLAGS-}" in + *target-cpu=*) need=0 ;; + esac + if [ "$need" -eq 1 ]; then + set -- -C "target-cpu=x86-64-v3" "$@" + fi + if [ -n "''${SURMOUNT_RUSTC_WRAPPER_NEXT-}" ]; then + exec "''${SURMOUNT_RUSTC_WRAPPER_NEXT}" "$real" "$@" + fi + exec "$real" "$@" + ''; + + tuneWith = + wrapper: pkg: + if !(lib.isDerivation pkg) then + pkg + else + pkg.overrideAttrs ( + old: + let + prevEnv = old.env or { }; + prevWrap = if prevEnv ? RUSTC_WRAPPER then prevEnv.RUSTC_WRAPPER else null; + nextDeps = + if old ? cargoArtifacts && lib.isDerivation old.cargoArtifacts then + tuneWith wrapper old.cargoArtifacts + else + null; + in + { + env = + prevEnv + // { + RUSTC_WRAPPER = wrapper; + } + // lib.optionalAttrs (prevWrap != null) { + SURMOUNT_RUSTC_WRAPPER_NEXT = prevWrap; + }; + passthru = (old.passthru or { }) // { + mailHostRustCpuHook = hookText; + mailHostRustCpuUses = "RUSTC_WRAPPER"; + } // lib.optionalAttrs (nextDeps != null) { + mailHostRustCpuCargoArtifacts = nextDeps; + }; + } + // lib.optionalAttrs (nextDeps != null) { + cargoArtifacts = nextDeps; + } + ); + + rustCpuOverlay = + _final: prev: + let + wrapper = prev.writeShellScript "mail-host-rustc-wrapper" hookText; + tune = tuneWith wrapper; + in + lib.foldl' ( + acc: name: + acc + // lib.optionalAttrs (prev ? ${name} && lib.isDerivation prev.${name}) { + ${name} = tune prev.${name}; + } + ) { } rustNames; +in +{ + config = lib.mkIf cfg.enable { + nixpkgs.overlays = lib.mkAfter [ rustCpuOverlay ]; + }; +} diff --git a/tests/mail-host-rust-cpu.nix b/tests/mail-host-rust-cpu.nix new file mode 100644 index 0000000..1db038d --- /dev/null +++ b/tests/mail-host-rust-cpu.nix @@ -0,0 +1,99 @@ +# Eval-only contract for the mail-host Rust CPU wrapper. +# Does not build rustc, Splora, bitcoind, or any crate. +# nix eval --impure --json --file tests/mail-host-rust-cpu.nix +let + flake = builtins.getFlake (toString ./..); + system = builtins.currentSystem; + inherit (flake.inputs.nixpkgs) lib; + + mkProbe = + prev: pname: cargoArtifacts: + prev.stdenv.mkDerivation ( + { + inherit pname; + version = "0"; + dontUnpack = true; + installPhase = "mkdir -p $out"; + } + // lib.optionalAttrs (cargoArtifacts != null) { + inherit cargoArtifacts; + } + ); + + evalRust = + enable: + lib.nixosSystem { + inherit system; + modules = [ + ../modules/mail-host-cpu.nix + ../modules/mail-host-rust-cpu.nix + ( + { ... }: + { + system.stateVersion = "26.05"; + networking.hostName = "mail-host-rust-cpu-eval"; + fileSystems."/" = { + device = "nodev"; + fsType = "ext4"; + }; + boot.loader.grub.enable = false; + surmount.mailHostCpu.enable = enable; + nixpkgs.overlays = [ + ( + _final: prev: + let + deps = mkProbe prev "surmount-management-ui-deps" null; + sploraDeps = mkProbe prev "splora-deps" null; + in + { + surmount-management-ui = mkProbe prev "surmount-management-ui" deps; + splora = mkProbe prev "splora" sploraDeps; + splora-http = mkProbe prev "splora-http" null; + bitcoind = mkProbe prev "bitcoind" null; + elementsd = mkProbe prev "elementsd" null; + } + ) + ]; + } + ) + ]; + }; + + hookOf = pkg: pkg.mailHostRustCpuHook or ""; + usesOf = pkg: pkg.mailHostRustCpuUses or ""; + depsHookOf = pkg: if pkg ? mailHostRustCpuCargoArtifacts then hookOf pkg.mailHostRustCpuCargoArtifacts else ""; + + goodHook = text: lib.hasInfix "target-cpu=x86-64-v3" text && lib.hasInfix "RUSTFLAGS" text && lib.hasInfix "CARGO_ENCODED_RUSTFLAGS" text; + + on = evalRust true; + off = evalRust false; + onUi = on.pkgs.surmount-management-ui; + onSplora = on.pkgs.splora; + onHttp = on.pkgs.splora-http; + offUi = off.pkgs.surmount-management-ui; + + t-enable-true-top-and-deps = + assert goodHook (hookOf onUi); + assert usesOf onUi == "RUSTC_WRAPPER"; + assert goodHook (depsHookOf onUi); + assert depsHookOf onUi == hookOf onUi; + assert goodHook (hookOf onSplora); + assert goodHook (depsHookOf onSplora); + assert goodHook (hookOf onHttp); + assert !(on.pkgs.bitcoind ? mailHostRustCpuHook); + assert !(on.pkgs.elementsd ? mailHostRustCpuHook); + "t-enable-true-rust-hook-x86-64-v3"; + + t-enable-false-hook-absent = + assert hookOf offUi == ""; + assert !(offUi ? mailHostRustCpuHook); + assert !(offUi ? mailHostRustCpuCargoArtifacts); + assert !(off.pkgs.splora ? mailHostRustCpuHook); + assert !(off.pkgs.splora-http ? mailHostRustCpuHook); + assert !(off.pkgs.bitcoind ? mailHostRustCpuHook); + "t-enable-false-rust-hook-absent"; +in +{ + t-enable-true-top-and-deps = t-enable-true-top-and-deps; + t-enable-false-hook-absent = t-enable-false-hook-absent; +} diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 169b5d7..9c5cecd 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -3227,10 +3227,12 @@ let assert !(lib.hasInfix "esplora-surmount-systems" toml); "t61-splora-portal-host-and-onion-extras-ok"; - # Mail sample host requests x86-64-v3 for Rust and for C/C++. - # The generic CI host (no hosts/mail-vps/configuration.nix) must not. - # nixpkgs.hostPlatform.gcc.arch stays unset: that flag is baked into the - # cc-wrapper and would rebuild the whole NixOS closure. + # Mail sample host requests x86-64-v3 for Rust named attrs and for + # every C/C++ compile in that host package set. The cc-wrapper reads + # hostPlatform.gcc.arch into cc-cflags-before, which covers dependency + # builds, not only tunedPackages. The generic CI host must not. + # localSystem.gcc.arch stays unset: hostPlatform is the knob, and the + # legacy localSystem option is ignored once hostPlatform is set. t62-mail-host-cpu-v3 = let mail = evalSurmount { @@ -3240,7 +3242,7 @@ let mailCpu = mail.config.surmount.mailHostCpu; genericCpu = generic.config.surmount.mailHostCpu; # nixosSystem passes nixpkgs.system. hostPlatform stays unset unless - # a module sets it. localSystem is the elaborated build platform. + # a module sets it. localSystem is the legacy elaborated platform. hostArch = e: if e.options.nixpkgs.hostPlatform.isDefined then @@ -3248,6 +3250,9 @@ let else null; localArch = e: e.config.nixpkgs.localSystem.gcc.arch or null; + # Package-set platform the cc-wrapper consults for -march. + # This is not the named tunedPackages overlay. + stdenvArch = e: e.pkgs.stdenv.hostPlatform.gcc.arch or null; in assert mailCpu.enable == true; assert mailCpu.gccArch == "x86-64-v3"; @@ -3266,7 +3271,11 @@ let assert !(builtins.elem "stalwart-mail" mailCpu.tunedPackages); assert mail.config.surmount.sploraIndexer.enable == false; assert mail.config.surmount.lake.enable == false; - assert hostArch mail != "x86-64-v3"; + assert stdenvArch mail == "x86-64-v3"; + assert hostArch mail == "x86-64-v3"; + assert hostArch mail != "x86-64-v4"; + assert !(lib.hasInfix "native" (if hostArch mail == null then "" else hostArch mail)); + assert !(lib.hasInfix "avx512" (if hostArch mail == null then "" else hostArch mail)); assert localArch mail != "x86-64-v3"; assert genericCpu.enable == false; assert genericCpu.gccArch == ""; @@ -3276,6 +3285,10 @@ let assert genericCpu.tunedPackages == [ ]; assert hostArch generic != "x86-64-v3"; assert localArch generic != "x86-64-v3"; + assert stdenvArch generic != "x86-64-v3"; + assert stdenvArch generic != "x86-64-v4"; + assert !(lib.hasInfix "native" (if stdenvArch generic == null then "" else stdenvArch generic)); + assert !(lib.hasInfix "avx512" (if stdenvArch generic == null then "" else stdenvArch generic)); "t62-mail-host-cpu-v3-ok"; results = [ From a47fc261a49d5ee40ff7796a7a2af8818f44b724 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Mon, 26 Oct 2026 22:15:58 -0600 Subject: [PATCH 10/22] fixes --- AGENTS.md | 23 ++-- docs/OPS.md | 62 ++++++++--- modules/arti-hidden-service.nix | 2 + modules/mail-host-rust-cpu.nix | 2 +- modules/management-ui.nix | 15 ++- modules/scram.nix | 6 +- modules/splora-indexer.nix | 67 ++++++++---- modules/stalwart-service.nix | 2 + tests/module-eval.nix | 180 ++++++++++++++++++++++++++++++-- 9 files changed, 301 insertions(+), 58 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 50e95e8..e6e44b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -441,9 +441,10 @@ builder work share that host. That is why niceness and a hard memory cap matter. **Lake is optional and default off** (`surmount.lake` in `modules/lake.nix`; operator 2026-08-25 supersedes the 2026-08-18 "never a lake unit" line). Do **not** start Lake on the live guest -from an agent. When host-local enables it, the unit must be niced, -`MemoryMax`-capped on `surmount-lake.service`, and journaled. Do not -Nice= mail. +from an agent. When that eval or host-local enables Lake, Lake is +nice 19 with idle I/O, `MemoryMax`-capped on `surmount-lake.service`, +and journaled. Mail is nice -20. Do not set builder `Nice=19` on mail +to deprioritize it. Dev builds (remote Nix store builds, flake check, hygiene compiles) are always maximally nice: `nice -n 19` and idle ionice when practical. @@ -453,11 +454,17 @@ of **one** CPU (that starves the ssh-ng protocol and parks cores). `cpuQuota = "auto"` means 95 percent times online CPUs on the niced wrapper, and no one-CPU quota on the slices. -Mail and other critical services (`stalwart`, `management-ui`, `sshd`, -`arti`, networking) are **not** nice. They stay at normal priority. Do -**not** set `Nice=` on those units. Do **not** globally starve mail by -nicing the whole machine. Prefer a niced builder user and a niced -remote-build path. +Mail and scram are nice -20 on ordinary CFS. They are not fifo. They +are not realtime I/O. If a separate mail database unit exists, it is +also nice -20. scram stays root and its start command still contains +`--watch`. The HTTP management UI is nice -19. Arti is nice -18. An +enabled Splora indexer, and the bitcoind or elementsd unit that indexer starts, is nice 5. The Nix daemon is nice 19 with idle +I/O. Lake, when that eval or host-local enables Lake, is nice 19 with +idle I/O. sshd stays un-niced (`Nice` unset). Do **not** set `Nice=` +on sshd. Builds stay nice 19 with idle I/O. No realtime policy on +product units. Do **not** globally starve mail by nicing the whole +machine. Prefer a niced builder user and a niced remote-build path. +Mail is the highest nice priority (-20), not `Nice` unset. **Hard memory cap (SHC ticket 261, closed 2026-08-18; console evidence 2026-08-18).** **surmount-1** is mail plus the allowed remote Nix diff --git a/docs/OPS.md b/docs/OPS.md index f4fdae4..054e0e6 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -944,8 +944,17 @@ reboot.** **In-guest follow-up (2026-08-25):** Builder `MemoryMax` is a **builder budget**, not 95 percent of the whole guest (that lets rustc starve mail). `nix-daemon` gets a positive `OOMScoreAdjust` so it is the -preferred in-guest OOM victim; mail, management-ui, and sshd get a -negative score and stay un-niced. `surmount.hardening.qemuGuestAgent` +preferred in-guest OOM victim. Mail and scram are nice -20 on ordinary +CFS. They are not fifo. They are not realtime I/O. If a separate mail +database unit exists, it is also nice -20. scram stays root and its +start command still contains `--watch`. The HTTP management UI is +nice -19. Arti is nice -18. An enabled Splora indexer, and the bitcoind or elementsd unit that indexer starts, is nice 5. The +Nix daemon is nice 19 with idle I/O. Lake, when that eval or host-local +enables Lake, is nice 19 with idle I/O. sshd stays un-niced (`Nice` +unset). Do **not** set `Nice=` on sshd. Builds stay nice 19 with idle +I/O. No realtime policy on product units. Mail, the HTTP management UI, +and sshd still get a negative OOM score. Do not set builder `Nice=19` +on mail to deprioritize it. `surmount.hardening.qemuGuestAgent` starts `services.qemuGuest` when enabled from host-local so staff can inject a command next time. Disk refuse stays at 95 percent. `cpuQuota = auto` stays 95 percent times online CPUs on the niced wrapper. Live @@ -1023,9 +1032,19 @@ real `memoryMax` as the **builder budget**; never publish that number as a guest size in git. Do not set it to about 95 percent of the whole guest while mail shares the box. Mail and other critical units are **not** on this slice and do -**not** get this `MemoryMax` or `Nice=`. `nix-daemon` **does** get -`Nice=19` and idle ionice when the module is enabled (dev builds -only; not mail). +**not** get this builder `MemoryMax` or this builder `Nice=19`. +Mail and scram are nice -20 on ordinary CFS. They are not fifo. They +are not realtime I/O. If a separate mail database unit exists, it is +also nice -20. scram stays root and its start command still contains +`--watch`. The HTTP management UI is nice -19. Arti is nice -18. An +enabled Splora indexer, and the bitcoind or elementsd unit that indexer starts, is nice 5. The Nix daemon is nice 19 with idle +I/O. Lake, when that eval or host-local enables Lake, is nice 19 with +idle I/O. sshd stays un-niced (`Nice` unset). Do **not** set `Nice=` +on sshd. Builds stay nice 19 with idle I/O. No realtime policy on +product units. Mail is the highest nice priority (-20), not `Nice` +unset. Do not set builder `Nice=19` on mail to deprioritize it. +`nix-daemon` **does** get `Nice=19` and idle I/O when the module is +enabled (dev builds only; not mail). Hermetic tests: crate `surmount-niced-builder` in `checks.*.ci`, `just test-remote-builder-eval`, `just test-logging-eval`, `nix eval --impure --json --file tests/lake-contract.nix`, @@ -1061,13 +1080,20 @@ make maximally nice use of cores without also OOMing the guest. That is the contract. Do not retarget the question to static-site slowness. Dev builds (remote Nix store builds, flake check, hygiene compiles) are -always maximally nice: `nice -n 19` and idle ionice when practical. -Prefer a niced builder user and a niced remote-build path. Prefer -build-on-laptop then switch so the host does not compile the UI. Do -**not** set `Nice=` on mail or other critical units (`stalwart-mail`, -`surmount-management-ui`, `sshd`, `surmount-arti-hidden-service` / Arti, -networking). Do **not** nice the whole machine. Starving mail to make a -compile cheaper is a miss. +always nice 19 with idle I/O. Prefer a niced builder user and a niced +remote-build path. Prefer build-on-laptop then switch so the host does +not compile the UI. Mail and scram are nice -20 on ordinary CFS. They +are not fifo. They are not realtime I/O. If a separate mail database +unit exists, it is also nice -20. scram stays root and its start +command still contains `--watch`. The HTTP management UI is nice -19. +Arti is nice -18. An enabled Splora indexer, and the bitcoind or elementsd unit that indexer starts, is nice 5. The Nix daemon +is nice 19 with idle I/O. Lake, when that eval or host-local enables +Lake, is nice 19 with idle I/O. sshd stays un-niced (`Nice` unset). +Do **not** set `Nice=` on sshd. Builds stay nice 19 with idle I/O. No +realtime policy on product units. Do **not** nice the whole machine. +Starving mail to make a compile cheaper is a miss. Mail is the highest +nice priority (-20). Do not set builder `Nice=19` on mail to +deprioritize it. **Niceness is not a memory cap.** We already started niced Lake without `MemoryMax`; that is why the box went dark (guest OOM / hypervisor @@ -1641,8 +1667,16 @@ Paper trail is **journald** (operator-only). `surmount.logging.enable` defaults true with `surmount.enable`. Persistent journal under `/var/log/journal` with a **size cap** so logs cannot fill the disk (SHC ticket 261 class). Scaffold `SystemMaxUse=1G` and `RuntimeMaxUse=256M` are not published guest disk or -RAM. `ForwardToSyslog=no`. Mail, management-ui, sshd, Arti, and networking -stay at normal priority (not niced). No public log dump on the apex site. +RAM. `ForwardToSyslog=no`. Mail and scram are nice -20 on ordinary CFS. +They are not fifo. They are not realtime I/O. If a separate mail +database unit exists, it is also nice -20. scram stays root and its +start command still contains `--watch`. The HTTP management UI is +nice -19. Arti is nice -18. An enabled Splora indexer, and the bitcoind or elementsd unit that indexer starts, is nice 5. The +Nix daemon is nice 19 with idle I/O. Lake, when that eval or host-local +enables Lake, is nice 19 with idle I/O. sshd stays un-niced (`Nice` +unset). Do **not** set `Nice=` on sshd. Builds stay nice 19 with idle +I/O. No realtime policy on product units. Mail is the highest nice +priority (-20), not `Nice` unset. No public log dump on the apex site. | Source | Where | Notes | |--------|-------|-------| diff --git a/modules/arti-hidden-service.nix b/modules/arti-hidden-service.nix index b2ab044..dedb613 100644 --- a/modules/arti-hidden-service.nix +++ b/modules/arti-hidden-service.nix @@ -395,6 +395,8 @@ in ExecStartPost = "-${hostnamePublishScript}"; Restart = "on-failure"; RestartSec = "10s"; + # Ordinary CFS. Not fifo, not rr, not idle I/O, not realtime I/O. + Nice = -18; # state_dir is onionServiceStateDir (HS identity + instance state). # cache_dir is under stateDir. Both need write for a live publisher. ReadWritePaths = [ diff --git a/modules/mail-host-rust-cpu.nix b/modules/mail-host-rust-cpu.nix index 1176644..9a3debe 100644 --- a/modules/mail-host-rust-cpu.nix +++ b/modules/mail-host-rust-cpu.nix @@ -103,7 +103,7 @@ let lib.foldl' ( acc: name: acc - // lib.optionalAttrs (prev ? ${name} && lib.isDerivation prev.${name}) { + // lib.optionalAttrs (prev ? ${name}) { ${name} = tune prev.${name}; } ) { } rustNames; diff --git a/modules/management-ui.nix b/modules/management-ui.nix index cd123f8..9771c2e 100644 --- a/modules/management-ui.nix +++ b/modules/management-ui.nix @@ -439,6 +439,8 @@ let ExecStart = "${pkg}/bin/surmount-management-ui"; Restart = "on-failure"; RestartSec = "5s"; + # Ordinary CFS. Not fifo, not rr, not idle I/O, not realtime I/O. + Nice = -19; SyslogIdentifier = "surmount-management-ui"; StandardOutput = "journal"; StandardError = "journal"; @@ -859,17 +861,26 @@ in serviceConfig = serviceConfig; }; } + # Static unit names. Reading config.services.splora.instances while + # defining systemd.services recurses. MemoryMax only; Nice stays on + # the indexer wrap. The five names are the default indexer units, + # including splora-mainnet. splora-queue stays on its own gate. // lib.optionalAttrs (sploraServiceOn && splora.unitsMemoryMax != null) ( let cap = { serviceConfig.MemoryMax = splora.unitsMemoryMax; }; - inst = lib.filterAttrs (_: i: i.enable or false) config.services.splora.instances; in lib.optionalAttrs (config.services.splora.queueEnable or false) { splora-queue = cap; } - // lib.mapAttrs' (name: _: lib.nameValuePair "splora-${name}" cap) inst + // lib.genAttrs [ + "splora-mainnet" + "splora-testnet3" + "splora-testnet4" + "splora-mutinynet" + "splora-liquid" + ] (_: cap) // lib.optionalAttrs (config.services.splora.popularScripts.enable or false) { splora-popular-scripts = cap; } diff --git a/modules/scram.nix b/modules/scram.nix index cc5ff5e..4249cca 100644 --- a/modules/scram.nix +++ b/modules/scram.nix @@ -25,11 +25,9 @@ in ExecStart = "${scram} --watch"; Restart = "always"; RestartSec = "1s"; + # CFS, not SCHED_FIFO. Nice -20 is the last line. Do not set + # CPUSchedulingPolicy to fifo or rr, and do not set realtime I/O. Nice = -20; - CPUSchedulingPolicy = "fifo"; - CPUSchedulingPriority = 99; - IOSchedulingClass = "realtime"; - IOSchedulingPriority = 0; OOMScoreAdjust = -1000; # Watchdog must be able to kill other uids. User = "root"; diff --git a/modules/splora-indexer.nix b/modules/splora-indexer.nix index e97b2ec..78b48f4 100644 --- a/modules/splora-indexer.nix +++ b/modules/splora-indexer.nix @@ -73,26 +73,57 @@ in } ]; }) - ] - ++ optionals hasSplora [ - (mkIf (cfg.enable && idx.enable) { - services.splora.enable = true; - services.splora.instances = mkMerge ( - map (name: { - ${name} = mkIf (idx.instanceName == name) { - enable = true; - network = idx.network; - daemonRpcAddr = idx.daemonRpcAddr; - cookieFile = idx.cookieFile; - daemonDir = idx.daemonDir; - jsonrpcImport = idx.jsonrpcImport; - publicHealth = idx.publicHealth; - dbBlockCacheMb = idx.dbBlockCacheMb; - extraArgs = idx.extraArgs; - }; - }) allowedNames + # Nice on units this module's imported splora stack actually starts. + # Static instance names. Do not map config.services.splora.instances + # into systemd attr keys (that recurses). Instance enable defaults + # true even when services.splora.enable is false, so the service + # enable is required or those names become Nice-only stubs. + (mkIf (hasSplora && config.services.splora.enable) { + systemd.services = mkMerge ( + map ( + name: + let + inst = config.services.splora.instances.${name} or { }; + on = inst.enable or false; + daemonName = if name == "liquid" then "elementsd-liquid" else "bitcoind-${name}"; + in + mkMerge [ + (mkIf on { + "splora-${name}" = { + serviceConfig.Nice = 5; + }; + }) + (mkIf (on && (inst.startLocalDaemon or false)) { + ${daemonName} = { + serviceConfig.Nice = 5; + }; + }) + ] + ) allowedNames ); }) ] + ++ optionals hasSplora ( + [ + (mkIf (cfg.enable && idx.enable) { + services.splora.enable = true; + services.splora.instances = mkMerge ( + map (name: { + ${name} = mkIf (idx.instanceName == name) { + enable = true; + network = idx.network; + daemonRpcAddr = idx.daemonRpcAddr; + cookieFile = idx.cookieFile; + daemonDir = idx.daemonDir; + jsonrpcImport = idx.jsonrpcImport; + publicHealth = idx.publicHealth; + dbBlockCacheMb = idx.dbBlockCacheMb; + extraArgs = idx.extraArgs; + }; + }) allowedNames + ); + }) + ] + ) ); } diff --git a/modules/stalwart-service.nix b/modules/stalwart-service.nix index a6812f7..a39a2f5 100644 --- a/modules/stalwart-service.nix +++ b/modules/stalwart-service.nix @@ -274,6 +274,8 @@ in KillSignal = "SIGINT"; Restart = "on-failure"; RestartSec = 5; + # Ordinary CFS. Mail is the database process. No fifo, rr, or realtime I/O. + Nice = -20; SyslogIdentifier = "stalwart-mail"; StandardOutput = "journal"; StandardError = "journal"; diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 9c5cecd..330472d 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -201,6 +201,21 @@ let assert e.config.surmount.sploraIndexer.publicHealth == false; assert e.config.surmount.managementUi.http3Enable == true; assert (e.config.services.splora.enable or false) == false; + # Splora off must not define indexer or daemon units. Instance enable + # defaults true even when services.splora.enable is false, so a Nice + # gate that only reads the instance still invents a Nice-only unit. + assert builtins.all (n: !(builtins.hasAttr n e.config.systemd.services)) [ + "splora-mainnet" + "splora-testnet3" + "splora-testnet4" + "splora-mutinynet" + "splora-liquid" + "bitcoind-mainnet" + "bitcoind-testnet3" + "bitcoind-testnet4" + "bitcoind-mutinynet" + "elementsd-liquid" + ]; assert !(builtins.elem 443 (e.config.networking.firewall.allowedUDPPorts or [ ])); assert failedAssertions e == [ ]; "t1-defaults-ok"; @@ -2308,17 +2323,114 @@ let t43-scram-watch-highest-priority = let - e = evalSurmount { }; + e = evalSurmount { + remoteBuilder.enable = true; + managementUi.enable = true; + artiHiddenService.enable = true; + artiHiddenService.startDaemon = true; + artiHiddenService.packageIsOnionServiceCapable = true; + # Smallest existing splora switch in this suite. Not a mainnet chain. + __extraModules = [ + { + services.splora.enable = true; + } + ]; + }; svc = e.config.systemd.services.surmount-scram or { }; sc = svc.serviceConfig or { }; wanted = svc.wantedBy or [ ]; + niceOf = + scx: + let + n = scx.Nice or null; + in + if n == null then null else toString n; + cpuOf = scx: lib.toLower (toString (scx.CPUSchedulingPolicy or "")); + ioOf = scx: lib.toLower (toString (scx.IOSchedulingClass or "")); + mail = e.config.systemd.services.stalwart-mail.serviceConfig or { }; + ui = e.config.systemd.services.surmount-management-ui.serviceConfig or { }; + arti = e.config.systemd.services.surmount-arti-hidden-service.serviceConfig or { }; + nixd = e.config.systemd.services.nix-daemon.serviceConfig or { }; + sshd = e.config.systemd.services.sshd.serviceConfig or { }; + # Indexer units this eval starts: splora-, bitcoind-*, elementsd-*. + # Queue, HTTP front, and popular-scripts are not indexers. + indexerName = + n: + let + ln = lib.toLower n; + in + (lib.hasPrefix "bitcoind-" ln) + || (lib.hasPrefix "elementsd-" ln) + || ( + lib.hasPrefix "splora-" ln + && ln != "splora-queue" + && ln != "splora-http" + && ln != "splora-popular-scripts" + ); + indexerOn = lib.filterAttrs ( + n: unit: + indexerName n + && (unit.enable or true) + && ((unit.wantedBy or [ ]) != [ ] || (unit.requiredBy or [ ]) != [ ]) + ) e.config.systemd.services; + indexerNames = builtins.attrNames indexerOn; + indexerNices = map (n: niceOf (indexerOn.${n}.serviceConfig or { })) indexerNames; + # 0 through 10 inclusive. One shared value. Null is not a nice. + niceInBand = n: n != null && builtins.match "(0|[1-9]|10)" n != null; + # services.splora.enable starts these. A missing name is a failure. + requiredIndexerUnits = [ + "splora-mainnet" + "splora-testnet3" + "splora-testnet4" + "splora-mutinynet" + "splora-liquid" + "bitcoind-mainnet" + "bitcoind-testnet3" + "bitcoind-testnet4" + "bitcoind-mutinynet" + "elementsd-liquid" + ]; + indexerStubs = lib.filterAttrs ( + n: unit: + indexerName n + && (niceOf (unit.serviceConfig or { }) != null) + && (toString (unit.serviceConfig.ExecStart or "") == "") + ) e.config.systemd.services; in assert sc.ExecStart or "" != ""; assert lib.hasInfix "--watch" (toString (sc.ExecStart or "")); - assert sc.Nice or 0 == -20; + assert (niceOf sc) == "-20"; assert sc.OOMScoreAdjust or 0 == -1000; - assert sc.CPUSchedulingPolicy or "" == "fifo"; assert builtins.elem "multi-user.target" wanted; + # Old t43 expected SCHED_FIFO. Realtime class and rr are the same miss. + assert (cpuOf sc) != "fifo"; + assert (cpuOf sc) != "rr"; + assert (ioOf sc) != "realtime"; + assert (sc.User or "root") == "root"; + assert (niceOf mail) == "-20"; + assert (cpuOf mail) != "fifo"; + assert (cpuOf mail) != "rr"; + assert (ioOf mail) != "realtime"; + assert (niceOf ui) == "-19"; + assert (cpuOf ui) != "fifo"; + assert (cpuOf ui) != "rr"; + assert (ioOf ui) != "idle"; + assert (niceOf arti) == "-18"; + assert (cpuOf arti) != "fifo"; + assert (cpuOf arti) != "rr"; + assert (ioOf arti) != "idle"; + # Every enabled indexer shares one nice in 0..10. A missing unit fails. + assert builtins.all (n: builtins.elem n indexerNames) requiredIndexerUnits; + assert indexerNames != [ ]; + assert builtins.all (n: niceInBand n && n == builtins.head indexerNices) indexerNices; + assert indexerStubs == { }; + assert (niceOf nixd) == "19"; + assert (ioOf nixd) == "idle"; + assert (cpuOf nixd) != "fifo"; + assert (cpuOf nixd) != "rr"; + # Unset sshd Nice passes. Do not require the attribute. + assert (niceOf sshd) != "19"; + assert (niceOf sshd) != "-20"; "t43-scram-watch-highest-priority-ok"; t43b-swapfile-enable-without-path-asserts = @@ -2355,6 +2467,25 @@ let in sc.Nice or null; + # Null stays null. Int or string both print as the unit-file token. + shownNice = n: if n == null then null else toString n; + + serviceCpuPolicy = + e: name: + let + svc = e.config.systemd.services.${name} or { }; + sc = svc.serviceConfig or { }; + in + lib.toLower (toString (sc.CPUSchedulingPolicy or "")); + + serviceIoClass = + e: name: + let + svc = e.config.systemd.services.${name} or { }; + sc = svc.serviceConfig or { }; + in + lib.toLower (toString (sc.IOSchedulingClass or "")); + etcText = e: name: let @@ -2401,7 +2532,7 @@ let in (mem == null || mem == "" || mem == "infinity") && (sl == null || sl != "surmount-builder.slice") - && (serviceNice e name == null) + && (shownNice (serviceNice e name) != "19") ) critical; in assert failedAssertions e == [ ]; @@ -2433,9 +2564,24 @@ let assert serviceMemoryMax e "surmount-management-ui" != "4G"; assert serviceSlice e "surmount-management-ui" != "surmount-builder.slice"; assert serviceMemoryMax e "surmount-arti-hidden-service" != "4G"; - assert serviceNice e "stalwart-mail" == null; - assert serviceNice e "surmount-management-ui" == null; - assert serviceNice e "surmount-arti-hidden-service" == null; + assert (shownNice (serviceNice e "stalwart-mail")) == "-20"; + assert (serviceCpuPolicy e "stalwart-mail") != "fifo"; + assert (serviceCpuPolicy e "stalwart-mail") != "rr"; + assert (serviceIoClass e "stalwart-mail") != "realtime"; + assert (shownNice (serviceNice e "surmount-management-ui")) == "-19"; + assert (serviceCpuPolicy e "surmount-management-ui") != "fifo"; + assert (serviceCpuPolicy e "surmount-management-ui") != "rr"; + assert (serviceIoClass e "surmount-management-ui") != "idle"; + assert (shownNice (serviceNice e "surmount-arti-hidden-service")) == "-18"; + assert (serviceCpuPolicy e "surmount-arti-hidden-service") != "fifo"; + assert (serviceCpuPolicy e "surmount-arti-hidden-service") != "rr"; + assert (serviceIoClass e "surmount-arti-hidden-service") != "idle"; + assert (shownNice (serviceNice e "sshd")) != "19"; + assert (shownNice (serviceNice e "sshd")) != "-20"; + assert (shownNice (serviceNice e "nix-daemon")) == "19"; + assert (serviceIoClass e "nix-daemon") == "idle"; + assert (serviceCpuPolicy e "nix-daemon") != "fifo"; + assert (serviceCpuPolicy e "nix-daemon") != "rr"; "t39-remote-builder-enable-memory-cap-ok"; t39b-remote-builder-custom-budget-not-sku = @@ -2526,6 +2672,8 @@ let assert d.MemoryMax or null == e.config.surmount.remoteBuilder.memoryMax; assert d.Nice or null == 19; assert (d.IOSchedulingClass or "") == "idle"; + assert (lib.toLower (toString (d.CPUSchedulingPolicy or ""))) != "fifo"; + assert (lib.toLower (toString (d.CPUSchedulingPolicy or ""))) != "rr"; "t39g-nix-daemon-is-the-real-build-cgroup-ok"; t39h-max-jobs-half-measured-cpus = @@ -2550,7 +2698,12 @@ let assert (mail.OOMScoreAdjust or 0) < 0; assert (sshd.OOMScoreAdjust or 0) < 0; assert (ui.OOMScoreAdjust or 0) < 0; - assert !(mail ? Nice); + assert (shownNice (mail.Nice or null)) == "-20"; + assert (lib.toLower (toString (mail.CPUSchedulingPolicy or ""))) != "fifo"; + assert (lib.toLower (toString (mail.CPUSchedulingPolicy or ""))) != "rr"; + assert (lib.toLower (toString (mail.IOSchedulingClass or ""))) != "realtime"; + assert (shownNice (sshd.Nice or null)) != "19"; + assert (shownNice (sshd.Nice or null)) != "-20"; "t39o-builder-is-preferred-oom-victim-ok"; # H3: bad recovery path charset fails closed. @@ -2804,10 +2957,15 @@ let assert failedAssertions e == [ ]; assert s.MemoryMax or null == "4G"; assert s.Nice or null == 19; + assert (s.IOSchedulingClass or "") == "idle"; assert (s.OOMScoreAdjust or 0) > 0; assert builtins.elem "multi-user.target" (e.config.systemd.services.surmount-lake.wantedBy or [ ]); - assert serviceNice e "stalwart-mail" == null; - assert serviceNice e "sshd" == null; + assert (shownNice (serviceNice e "stalwart-mail")) == "-20"; + assert (serviceCpuPolicy e "stalwart-mail") != "fifo"; + assert (serviceCpuPolicy e "stalwart-mail") != "rr"; + assert (serviceIoClass e "stalwart-mail") != "realtime"; + assert (shownNice (serviceNice e "sshd")) != "19"; + assert (shownNice (serviceNice e "sshd")) != "-20"; "t44b-lake-enable-memory-cap-ok"; t45-grok-oss-default-off = @@ -3376,7 +3534,6 @@ let t37-btop-local-config t37b-btop-use-fstab-true t42-guest-root-justfile - t43-scram-watch-highest-priority t43b-swapfile-enable-without-path-asserts t39-remote-builder-enable-memory-cap t39b-remote-builder-custom-budget-not-sku @@ -3413,6 +3570,7 @@ let t60-splora-remote-enable-needs-cookie-path t61-splora-portal-host-and-onion-extras t62-mail-host-cpu-v3 + t43-scram-watch-highest-priority ]; in { From a5302d070982172bf932762c632b3f2778a2c7da Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Mon, 26 Oct 2026 23:44:33 -0600 Subject: [PATCH 11/22] fixes --- docs/OPS.md | 3 ++- modules/mail-host-cpu.nix | 5 +++++ tests/module-eval.nix | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/OPS.md b/docs/OPS.md index 054e0e6..3ff2e65 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -860,7 +860,8 @@ any flag above `x86-64-v3`. AVX-512 is absent, so this is not `hosts/mail-vps/configuration.nix` sets `surmount.mailHostCpu.enable`. `modules/mail-host-cpu.nix` then sets `nixpkgs.hostPlatform` to this -host's system with `gcc.arch = "x86-64-v3"`. The nixpkgs cc-wrapper +host's system with `gcc.arch = "x86-64-v3"`. The host Nix daemon must +advertise gccarch-x86-64-v3 or the v3 stdenv will not build. The nixpkgs cc-wrapper reads `targetPlatform.gcc.arch` and writes `-march=x86-64-v3` into `cc-cflags-before`. That flag applies to every C and C++ compile that uses this host's stdenv, including dependencies such as nixpkgs diff --git a/modules/mail-host-cpu.nix b/modules/mail-host-cpu.nix index 3b73bc8..686d0d8 100644 --- a/modules/mail-host-cpu.nix +++ b/modules/mail-host-cpu.nix @@ -203,6 +203,11 @@ in gcc.arch = march; }; + # The v3 stdenv requires gccarch-x86-64-v3. extra-system-features is + # a freeform list and concatenates, so this does not drop + # surmount-remote or the daemon's system-features defaults. + nix.settings.extra-system-features = [ "gccarch-${march}" ]; + nixpkgs.overlays = lib.mkAfter [ mailHostCpuOverlay ]; assertions = [ diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 330472d..db6b520 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -3449,6 +3449,49 @@ let assert !(lib.hasInfix "avx512" (if stdenvArch generic == null then "" else stdenvArch generic)); "t62-mail-host-cpu-v3-ok"; + # v3 stdenv sets requiredSystemFeatures to gccarch-x86-64-v3. The mail + # host must advertise that token on extra-system-features when + # mailHostCpu is on. The generic eval leaves the module off and must + # not advertise it. With the remote builder also on, the same list + # must still contain surmount-remote (freeform list concat, not a + # system-features replacement). + t62-gccarch-feature = + let + mail = evalSurmount { + __extraModules = [ ../hosts/mail-vps/configuration.nix ]; + }; + mailBuilder = evalSurmount { + __extraModules = [ ../hosts/mail-vps/configuration.nix ]; + remoteBuilder.enable = true; + }; + generic = evalSurmount { }; + featureList = + v: + if v == null then + [ ] + else if builtins.isList v then + v + else + lib.splitString " " (toString v); + extraOf = e: featureList (e.config.nix.settings.extra-system-features or [ ]); + sysOf = e: featureList (e.config.nix.settings.system-features or [ ]); + mailExtra = extraOf mail; + builderExtra = extraOf mailBuilder; + genericExtra = extraOf generic; + in + assert mail.config.surmount.mailHostCpu.enable == true; + assert builtins.elem "gccarch-x86-64-v3" mailExtra; + assert !(builtins.elem "gccarch-x86-64-v3" genericExtra); + assert generic.config.surmount.mailHostCpu.enable == false; + assert mailBuilder.config.surmount.remoteBuilder.enable == true; + assert builtins.elem "gccarch-x86-64-v3" builderExtra; + assert builtins.elem "surmount-remote" builderExtra; + assert builtins.elem "benchmark" (sysOf mail); + assert builtins.elem "big-parallel" (sysOf mail); + assert builtins.elem "kvm" (sysOf mail); + assert builtins.elem "nixos-test" (sysOf mail); + "t62-gccarch-feature-ok"; + results = [ t1-defaults t1b-p1-free-443-plan-and-firewall @@ -3570,6 +3613,7 @@ let t60-splora-remote-enable-needs-cookie-path t61-splora-portal-host-and-onion-extras t62-mail-host-cpu-v3 + t62-gccarch-feature t43-scram-watch-highest-priority ]; in From 93e093dfdd06965f2ff195b069a1c2a27f2f153c Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Wed, 4 Nov 2026 03:49:49 -0700 Subject: [PATCH 12/22] fixes --- AGENTS.md | 48 +- Cargo.lock | 753 ++---------------- Cargo.toml | 19 +- README.md | 8 +- crates/management-ui/src/acme.rs | 474 ++++++++++- crates/management-ui/src/config.rs | 108 +++ crates/management-ui/src/main.rs | 59 +- crates/management-ui/src/pages.rs | 2 +- crates/management-ui/src/redirect.rs | 4 +- crates/management-ui/src/status.rs | 362 ++++++++- crates/surmount-private-data/src/lib.rs | 36 + crates/surmount-rekey/Cargo.toml | 9 +- crates/surmount-rekey/src/lib.rs | 73 +- crates/surmount-rekey/src/main.rs | 2 +- docs/COMPACTION-PIN.md | 43 +- docs/DATASTORES.md | 15 +- docs/EDGE_AND_TLS.md | 57 +- docs/MIGRATION.md | 6 +- docs/OPS.md | 4 +- docs/SEARCH_AND_UI.md | 7 +- docs/SECURITY.md | 4 +- docs/STACK.md | 4 +- docs/architecture-review.md | 11 +- docs/hygiene.md | 4 +- docs/open-choices.md | 4 +- docs/operator-direction.md | 15 + docs/packages-and-forks.md | 2 +- docs/research/rust-edge-and-uds.md | 6 +- .../scaffold-assumptions-inventory.md | 21 +- docs/research/version-audit.md | 43 +- flake.lock | 24 +- flake.nix | 3 + modules/arti-hidden-service.nix | 30 +- modules/backups.nix | 9 + modules/eternal-terminal.nix | 12 +- modules/hardening.nix | 65 +- modules/lake.nix | 7 +- modules/logging.nix | 16 +- modules/mail-host-rust-cpu.nix | 15 +- modules/mail.nix | 2 +- modules/management-ui.nix | 181 ++++- modules/networking.nix | 10 + modules/options.nix | 52 +- modules/remote-builder.nix | 7 +- modules/scram.nix | 32 +- modules/secrets.nix | 20 + modules/speedtest.nix | 56 +- modules/splora-indexer.nix | 56 +- modules/stalwart-service.nix | 4 + modules/vaultwarden.nix | 7 + modules/web.nix | 57 +- .../bitcoind-mutinynet-signet-spacing.patch | 11 + nix/packages/bitcoind-mutinynet.nix | 24 + nix/packages/stalwart-cli.nix | 9 +- nix/packages/stalwart-mail.nix | 16 +- nix/packages/stalwart-spam-filter.nix | 26 +- nix/packages/stalwart-webui.nix | 6 +- nix/packages/surmount-rekey.nix | 3 +- nix/packages/vandelay.nix | 14 +- .../surmount-laptop-renew-cert.service | 5 + .../surmount-laptop-renew-cert.timer | 1 + tests/fail2ban-permanent-eval.nix | 150 ++++ tests/host-cert-renew-eval.nix | 238 ++++++ tests/mail-host-rust-cpu.nix | 9 +- tests/module-eval.nix | 743 +++++++++++++++++ 65 files changed, 3106 insertions(+), 1017 deletions(-) create mode 100644 nix/packages/bitcoind-mutinynet-signet-spacing.patch create mode 100644 nix/packages/bitcoind-mutinynet.nix create mode 100644 tests/fail2ban-permanent-eval.nix create mode 100644 tests/host-cert-renew-eval.nix diff --git a/AGENTS.md b/AGENTS.md index e6e44b9..7a7441c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,8 +91,10 @@ Product detail lives under `docs/`. This file is standing law only. switch and stays the explicit equivalent with no default target), hypervisor, Namecheap **hosted DNSSEC Status** click, or the operator using a TUI they asked to sit in. - Host-local enable is agent work when this machine already has - host-local. Using `just grok-oss` after the guest has the package is + Turning a switch on in the private overlay on horizon + (`~/.local/share/surmount/host-local/default.nix`) is agent work when + that file is already on this laptop. That directory is not a machine + name. The machines are horizon and surmount-1. Using `just grok-oss` after the guest has the package is the product, not a leftover click. If this laptop already has the Namecheap env files (mode 0600 regular files under `~/.local/share/surmount/`), `just laptop-renew-cert` is **agent work**. @@ -371,6 +373,30 @@ in the same turn (standing rule + compaction reload table). One file is attention dilution. Chat-only is compaction loss. Nix needing an untracked file is still not a reason to stage. +## Number parallel items, and name the machine (operator 2026-11-03) + +Operator 2026-11-03. Two rules. + +1. Number every parallel list in operator-facing chat, reports, and + leftover lists. Use 1, 2, 3. A bare stack of services is a miss. + Each number is a complete sentence when the item is a leftover or a + decision. +2. Name the machine. **horizon** is this laptop. **surmount-1** is the + mail host and the allowed remote Nix builder. Do not say "host-local" + as if it were a hostname. + +The private Nix overlay that `just deploy` copies is a directory, not a +machine. On horizon that directory is +`~/.local/share/surmount/host-local/` and the entry file is +`default.nix`. The recipe passes that directory as the `--host-local` +flag and the target is `root@surmount-1`. After the copy, surmount-1 +has the same files at `/root/surmount-server/host-local/`. When a +sentence needs that directory, name horizon or surmount-1 and the path +in the same sentence. The flag name `--host-local` stays, because that +is the deploy argument. + +Dual-pin [docs/COMPACTION-PIN.md](docs/COMPACTION-PIN.md). + ## Deploy dry-run is agent work (operator 2026-09-02) Full command list: [docs/TEST-AND-DEPLOY.md](docs/TEST-AND-DEPLOY.md). @@ -438,10 +464,14 @@ unless the operator asked for that slice. **surmount-1** is the allowed remote Nix builder (ssh-ng). Mail and builder work share that host. That is why niceness and a hard memory -cap matter. **Lake is optional and default off** (`surmount.lake` in +cap matter. The private overlay is the directory +`~/.local/share/surmount/host-local/` on horizon, copied to +`/root/surmount-server/host-local/` on surmount-1. That directory is +not a hostname. See *Number parallel items, and name the machine*. +**Lake is optional and default off** (`surmount.lake` in `modules/lake.nix`; operator 2026-08-25 supersedes the 2026-08-18 "never a lake unit" line). Do **not** start Lake on the live guest -from an agent. When that eval or host-local enables Lake, Lake is +from an agent. When that eval or the private overlay on horizon enables Lake, Lake is nice 19 with idle I/O, `MemoryMax`-capped on `surmount-lake.service`, and journaled. Mail is nice -20. Do not set builder `Nice=19` on mail to deprioritize it. @@ -459,7 +489,7 @@ are not realtime I/O. If a separate mail database unit exists, it is also nice -20. scram stays root and its start command still contains `--watch`. The HTTP management UI is nice -19. Arti is nice -18. An enabled Splora indexer, and the bitcoind or elementsd unit that indexer starts, is nice 5. The Nix daemon is nice 19 with idle -I/O. Lake, when that eval or host-local enables Lake, is nice 19 with +I/O. Lake, when that eval or the private overlay on horizon enables Lake, is nice 19 with idle I/O. sshd stays un-niced (`Nice` unset). Do **not** set `Nice=` on sshd. Builds stay nice 19 with idle I/O. No realtime policy on product units. Do **not** globally starve mail by nicing the whole @@ -483,8 +513,8 @@ cap (systemd `MemoryMax` / cgroup `memory.max`) plus the about-95-percent CPU/RAM/storage guards SHC asked for. Those 95 percent guards are a **total** ceiling, not "give the builder 95 percent of guest RAM." Mail and other critical services must **not** be starved. In-tree: -`surmount.remoteBuilder` (default off) owns that cap for **ssh-ng / -nixbuilder only**; enable from host-local. Trusted ssh-ng forwards +`surmount.remoteBuilder` (default off in the public modules) owns that cap for **ssh-ng / +nixbuilder only**; the private overlay on horizon turns it on for surmount-1. Trusted ssh-ng forwards rustc to **system `nix-daemon.service`**. `MemoryMax` and `Nice=19` must apply to **that** unit, not only `user-.slice`. A nixbuilder-only cap is a miss. Job slots (`maxJobs`, laptop machines @@ -497,9 +527,9 @@ RAM size. Scaffold `maxJobs = 32` is half the measured mail-host CPUs builder OOM score, and Nice=19 are why this is no longer 8. Do not publish the raw CPU count. The builder is the preferred in-guest OOM victim (`OOMScoreAdjust`). Enable `surmount.hardening.qemuGuestAgent` -from host-local so SHC can inject a command when SSH is stuck. Do **not** +from the private overlay on horizon so SHC can inject a command when SSH is stuck. Do **not** start Lake from an agent. `surmount.lake.enable` stays false until -host-local turns it on after this generation is live. +the private overlay on horizon turns it on after this generation is live. Operator correction 2026-08-18: Nix is **misconfigured** if builds do not make maximally nice use of cores **without also OOMing the box**. diff --git a/Cargo.lock b/Cargo.lock index 3ca4570..028dfe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,82 +12,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "age" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd290633c2482479f70f6d1d96ae0e9f52c6a26cd5859edd47ee1fe33fc89f26" -dependencies = [ - "age-core", - "base64 0.22.1", - "bech32 0.11.1", - "chacha20poly1305", - "cipher", - "cookie-factory", - "hkdf", - "hmac 0.12.1", - "hpke", - "i18n-embed", - "i18n-embed-fl", - "lazy_static", - "ml-kem", - "nom 8.0.0", - "p256", - "pin-project", - "rand 0.8.8", - "rust-embed", - "scrypt", - "sha2 0.10.9", - "sha3", - "subtle", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "age-core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d4375964d1501e5f1b32aef2ead573913893ff238448d4e9fdf1522d828656" -dependencies = [ - "base64 0.22.1", - "bech32 0.11.1", - "chacha20poly1305", - "cookie-factory", - "hkdf", - "hpke", - "io_tee", - "nom 8.0.0", - "rand 0.8.8", - "secrecy", - "sha2 0.10.9", -] - [[package]] name = "aho-corasick" version = "1.1.5" @@ -188,7 +112,7 @@ dependencies = [ "asn1-rs-derive", "asn1-rs-impl", "displaydoc", - "nom 7.1.3", + "nom", "num-traits", "rusticata-macros", "thiserror 2.0.21", @@ -408,12 +332,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base64" version = "0.22.1" @@ -539,7 +457,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "hybrid-array 0.4.15", + "hybrid-array", ] [[package]] @@ -586,9 +504,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", "jobserver", @@ -751,16 +669,10 @@ dependencies = [ "convert_case 0.6.0", "pathdiff", "serde_core", - "toml 1.1.8+spec-1.1.0", + "toml", "winnow", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -775,9 +687,9 @@ checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] name = "const_format" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +checksum = "a3decb4d6965448659923544a634a0571ba44cede4e11ef15a5a317126840454" dependencies = [ "const_format_proc_macros", "konst", @@ -785,9 +697,9 @@ dependencies = [ [[package]] name = "const_format_proc_macros" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +checksum = "941b570269a642506f08cb5b688c04bfa6cfd41a26f8bcdc49db8fd00c0ab659" dependencies = [ "proc-macro2", "quote", @@ -827,15 +739,6 @@ dependencies = [ "convert_case 0.11.0", ] -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -870,18 +773,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -889,7 +780,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] @@ -899,16 +789,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "hybrid-array 0.4.15", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", + "hybrid-array", ] [[package]] @@ -991,16 +872,6 @@ dependencies = [ "thiserror 2.0.21", ] -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "zeroize", -] - [[package]] name = "der-parser" version = "10.0.0" @@ -1009,7 +880,7 @@ checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ "asn1-rs", "displaydoc", - "nom 7.1.3", + "nom", "num-bigint", "num-traits", "rusticata-macros", @@ -1040,7 +911,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common 0.1.7", - "subtle", ] [[package]] @@ -1050,7 +920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.2", + "const-oid", "crypto-common 0.2.2", "ctutils", ] @@ -1094,25 +964,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "equivalent" version = "1.0.3" @@ -1173,82 +1024,18 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "fiat-crypto" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "find-crate" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" -dependencies = [ - "toml 0.5.11", -] - [[package]] name = "find-msvc-tools" version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" -[[package]] -name = "fluent" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" -dependencies = [ - "fluent-bundle", - "unic-langid", -] - -[[package]] -name = "fluent-bundle" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" -dependencies = [ - "fluent-langneg", - "fluent-syntax", - "intl-memoizer", - "intl_pluralrules", - "rustc-hash", - "self_cell", - "smallvec", - "unic-langid", -] - -[[package]] -name = "fluent-langneg" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "fluent-syntax" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" -dependencies = [ - "memchr", - "thiserror 2.0.21", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1376,7 +1163,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -1406,16 +1192,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - [[package]] name = "gloo-net" version = "0.6.0" @@ -1450,17 +1226,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "guardian" version = "1.3.0" @@ -1580,24 +1345,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hmac" version = "0.13.0" @@ -1607,26 +1354,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "hpke" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4917627a14198c3603282c5158b815ad5534795451d3c074b53cf3cee0960b11" -dependencies = [ - "aead", - "aes-gcm", - "chacha20poly1305", - "digest 0.10.7", - "generic-array", - "hkdf", - "hmac 0.12.1", - "p256", - "rand_core 0.6.4", - "sha2 0.10.9", - "subtle", - "zeroize", -] - [[package]] name = "html-escape" version = "0.2.15" @@ -1678,15 +1405,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hybrid-array" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" -dependencies = [ - "typenum", -] - [[package]] name = "hybrid-array" version = "0.4.15" @@ -1774,75 +1492,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "i18n-config" -version = "0.4.8" -source = "git+https://github.com/kellpossible/cargo-i18n?rev=2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4#2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4" -dependencies = [ - "log", - "serde", - "thiserror 2.0.21", - "toml 1.1.8+spec-1.1.0", - "unic-langid", -] - -[[package]] -name = "i18n-embed" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a217bbb075dcaefb292efa78897fc0678245ca67f265d12c351e42268fcb0305" -dependencies = [ - "arc-swap", - "fluent", - "fluent-langneg", - "fluent-syntax", - "i18n-embed-impl", - "intl-memoizer", - "log", - "parking_lot", - "rust-embed", - "thiserror 1.0.69", - "unic-langid", - "walkdir", -] - -[[package]] -name = "i18n-embed-fl" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602e6bd30c3db2749e13e38b363a3d98d9d41de1d8de7a79c31bb69e45b47cda" -dependencies = [ - "find-crate", - "fluent", - "fluent-syntax", - "i18n-config", - "i18n-embed", - "proc-macro-error3", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", - "unic-langid", -] - -[[package]] -name = "i18n-embed-impl" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" -dependencies = [ - "find-crate", - "i18n-config", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "icu_collections" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" dependencies = [ "displaydoc", "potential_utf", @@ -1854,9 +1508,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" dependencies = [ "displaydoc", "litemap", @@ -1867,9 +1521,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1881,15 +1535,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" [[package]] name = "icu_properties" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" dependencies = [ "displaydoc", "icu_collections", @@ -1902,15 +1556,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" [[package]] name = "icu_provider" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" dependencies = [ "displaydoc", "icu_locale_core", @@ -1994,25 +1648,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" -[[package]] -name = "intl-memoizer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" -dependencies = [ - "type-map", - "unic-langid", -] - -[[package]] -name = "intl_pluralrules" -version = "7.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" -dependencies = [ - "unic-langid", -] - [[package]] name = "inventory" version = "0.3.25" @@ -2022,12 +1657,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io_tee" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" - [[package]] name = "ipnet" version = "2.12.2" @@ -2125,25 +1754,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures 0.2.17", -] - -[[package]] -name = "kem" -version = "0.3.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" -dependencies = [ - "rand_core 0.6.4", - "zeroize", -] - [[package]] name = "konst" version = "0.2.20" @@ -2305,15 +1915,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.34" @@ -2376,16 +1977,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2403,18 +1994,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "ml-kem" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" -dependencies = [ - "hybrid-array 0.2.3", - "kem", - "rand_core 0.6.4", - "sha3", -] - [[package]] name = "next_tuple" version = "0.1.0" @@ -2431,15 +2010,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "nostr" version = "0.45.5" @@ -2553,9 +2123,9 @@ checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" [[package]] name = "openssl-probe" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" [[package]] name = "or_poisoned" @@ -2563,45 +2133,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "elliptic-curve", - "primeorder", -] - [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "paste" version = "1.0.15" @@ -2614,16 +2151,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pem" version = "4.0.0" @@ -2683,18 +2210,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "potential_utf" version = "0.1.6" @@ -2729,37 +2244,6 @@ dependencies = [ "syn 3.0.9", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-error-attr3" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error3" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" -dependencies = [ - "proc-macro-error-attr3", - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -3011,15 +2495,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.2", -] - [[package]] name = "regex" version = "1.13.1" @@ -3138,41 +2613,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rust-embed" -version = "8.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19afa5b4b6a611de00bd1bdae6ae6f39084c9399f0679c3f52d8469cf335cc23" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d8afda6374eac59e066abee06d265247ebbaf3006cf878e2879e8356e34053" -dependencies = [ - "mime_guess", - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.119", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d84e8ba78bd384263e5922f084cbe1b081c3b7e69add59c8fb097b879ba968a" -dependencies = [ - "sha2 0.11.1", - "walkdir", -] - [[package]] name = "rustc-hash" version = "2.1.3" @@ -3194,7 +2634,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" dependencies = [ - "nom 7.1.3", + "nom", ] [[package]] @@ -3283,15 +2723,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -3310,36 +2741,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2", - "salsa20", - "sha2 0.10.9", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "subtle", - "zeroize", -] - [[package]] name = "secp256k1" version = "0.30.0" @@ -3392,12 +2793,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self_cell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" - [[package]] name = "semver" version = "1.0.28" @@ -3580,16 +2975,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "sha3" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" -dependencies = [ - "digest 0.10.7", - "keccak", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -3648,9 +3033,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.16.3" +version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" [[package]] name = "socket2" @@ -3752,7 +3137,7 @@ dependencies = [ "h3", "h3-quinn", "h3-util", - "hmac 0.13.0", + "hmac", "http-body-util", "hyper", "hyper-util", @@ -3789,9 +3174,10 @@ version = "0.1.0" name = "surmount-rekey" version = "0.1.0" dependencies = [ - "age", "anyhow", + "bech32 0.11.1", "secrecy", + "x25519-dalek", ] [[package]] @@ -4014,12 +3400,11 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" dependencies = [ "displaydoc", - "serde_core", "zerovec", ] @@ -4031,9 +3416,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.3" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -4080,15 +3465,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "1.1.8+spec-1.1.0" @@ -4260,15 +3636,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash", -] - [[package]] name = "typed-builder" version = "0.23.2" @@ -4295,31 +3662,6 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "serde", - "tinystr", -] - -[[package]] -name = "unicase" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" - [[package]] name = "unicode-ident" version = "1.0.27" @@ -4402,9 +3744,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.28.0" +version = "1.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4702,7 +4044,7 @@ dependencies = [ "data-encoding", "der-parser", "lazy_static", - "nom 7.1.3", + "nom", "oid-registry", "ring", "rusticata-macros", @@ -4728,9 +4070,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4823,11 +4165,10 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" dependencies = [ - "serde", "yoke", "zerofrom", "zerovec-derive", @@ -4835,9 +4176,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 501e1bb..7f95a75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,11 +111,9 @@ sha2 = "0.11" base64 = "0.23" # CSRF tokens + CSP nonces (already in lock via other crates; direct dep for auth). getrandom = "0.4" -# rage CLI is built on this library (age-encryption.org). Used by just rekey. -# 0.12.1+ pulls i18n-embed-fl 0.10.1, which uses proc-macro-error3. -# RUSTSEC-2026-0173 is proc-macro-error2, which is not in this lock. -# Identity::generate / to_public / to_string stay. -age = { version = "0.12", default-features = false } +# surmount-rekey mints X25519 age identity strings itself (x25519-dalek + +# bech32 0.11). Do not depend on the age crate: 0.12.1 always pulls hpke +# and p256, and that edge requires crypto-bigint 0.5 (RUSTSEC-2026-0387). # menhera 10d lists chacha20 0.10.2 and faster-hex 0.10.1 (checked # 2026-10-17). Those git lines are gone. crates-io is still replaced by @@ -143,17 +141,14 @@ age = { version = "0.12", default-features = false } # https://rustsec.org/advisories/RUSTSEC-2026-0383 (accessed: 2026-10-24). # When menhera 10d lists 1.0.10 or later, drop this webpki-root-certs line. # -# i18n-config 0.4.8 on crates.io / menhera still depends on basic-toml -# (RUSTSEC-2026-0374, unmaintained, no patched versions). git master -# 2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4 is still version 0.4.8 and -# uses the toml crate. Do not ignore RUSTSEC-2026-0374. +# i18n-config left with age. age 0.12.1 was the only crate that pulled +# i18n-embed, and that edge required crypto-bigint 0.5. Do not re-add the +# i18n-config git patch unless a crate depends on i18n-config again. +# basic-toml (RUSTSEC-2026-0374) was only reached through that edge. # https://rustsec.org/advisories/RUSTSEC-2026-0374 (accessed: 2026-10-24). -# When a crates.io i18n-config without basic-toml is on menhera 10d, -# drop this i18n-config line. [patch.crates-io] rustls = { git = "https://github.com/rustls/rustls", rev = "f04804fa13341a9b1dc02b5d1dd8a746f2c4b7fb" } ctutils = { git = "https://github.com/RustCrypto/utils", rev = "88d6e90d4cfd2fc3333d00e13c99241507d2138f" } rustls-webpki = { git = "https://github.com/rustls/webpki", rev = "8ecdcc76f56b68fda67a6adf7976f5eed5006a4c" } webpki-root-certs = { git = "https://github.com/rustls/webpki-roots", rev = "7ebfa0b377fd0c7e0a2848f9d9b2867a4c2198cf" } -i18n-config = { git = "https://github.com/kellpossible/cargo-i18n", rev = "2ce7b1dfe9a8cc224288bf50cbc2a67a6de863c4" } diff --git a/README.md b/README.md index 59db682..a1efbd3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ offline Synology DiskStation (MailPlus + static sites) with a reproducible stack: - **Stalwart** - SMTP / submission / IMAP / ManageSieve / JMAP (mail engine - pinned in `nix/packages/stalwart-mail.nix`, currently **0.16.15**; + pinned in `nix/packages/stalwart-mail.nix`, currently **1.0.2**; **RocksDB** all-role co-location **fine for now**; [docs/DATASTORES.md](docs/DATASTORES.md). Internal FTS good enough for now; Surmount builds own search product later) @@ -247,9 +247,9 @@ ssh -L 8080:127.0.0.1:8080 mail-vps # open http://127.0.0.1:8080 ``` -Transitional nginx may also expose `/stalwart-admin/` on -`services.surmount.systems` for bootstrap; remove or lock down once you no -longer need it. Port numbers: confirm live defaults in modules (UI vs Stalwart +Transitional nginx does not publish `/stalwart-admin/`. That public path +was removed. Bootstrap is the SSH tunnel above, to the loopback Stalwart +HTTP listener. Port numbers: confirm live defaults in modules (UI vs Stalwart HTTP split is documented in architecture-review). **NixOS option path:** Surmount-owned `services.stalwart` module for diff --git a/crates/management-ui/src/acme.rs b/crates/management-ui/src/acme.rs index 6c741e4..8ebe510 100644 --- a/crates/management-ui/src/acme.rs +++ b/crates/management-ui/src/acme.rs @@ -1,9 +1,11 @@ //! In-process ACME (Let's Encrypt-capable) for management-ui HTTPS. //! //! **Default off.** CI and hermetic tests never require a live ACME directory -//! or real DNS. Product challenge for this slice is **DNS-01** (fits -//! redirect-only product :80). HTTP-01 on product :80 remains parked (Q-EDGE). -//! TLS-ALPN-01 is residual (ALPN routing on the rustls acceptor). +//! or real DNS. DNS-01 remains the in-process adapter. HTTP-01 is the +//! surmount-1 renew path: the plain HTTP listener serves +//! `/.well-known/acme-challenge/` from `SURMOUNT_ACME_HTTP01_WEBROOT`, and +//! the host timer `surmount-cert-renew` runs the ACME client. TLS-ALPN-01 +//! is residual (ALPN routing on the rustls acceptor). //! //! # What `DnsProvider` / `SURMOUNT_ACME_DNS_PROVIDER` means //! @@ -96,30 +98,28 @@ pub const HOOK_WAIT_UNSUPPORTED_EXIT: i32 = 2; pub const HOOK_MINIMAL_PATH: &str = "/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/usr/bin:/bin"; -/// Challenge type for this product slice. HTTP-01 is not offered. +/// Challenge type. DNS-01 is the in-process adapter. HTTP-01 is the host +/// renew path (webroot on the product :80 listener). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AcmeChallenge { - /// DNS-01 (`_acme-challenge.` TXT). Primary for redirect-only :80. + /// DNS-01 (`_acme-challenge.` TXT). Dns01, + /// HTTP-01 (`/.well-known/acme-challenge/` from the webroot). + Http01, } impl AcmeChallenge { pub fn parse(raw: &str) -> Result { match raw.trim().to_ascii_lowercase().as_str() { "" | "dns-01" | "dns01" | "dns" => Ok(Self::Dns01), - "http-01" | "http01" | "http" => Err( - "ACME HTTP-01 on product :80 is parked (Q-EDGE). Use dns-01 \ - (SURMOUNT_ACME_CHALLENGE=dns-01) or external host PEMs" - .into(), - ), + "http-01" | "http01" | "http" => Ok(Self::Http01), "tls-alpn-01" | "tls-alpn" | "alpn" => Err( "ACME TLS-ALPN-01 is residual for this slice (not wired). Use \ - dns-01 or external host PEMs" + dns-01, http-01, or external host PEMs" .into(), ), other => Err(format!( - "unknown SURMOUNT_ACME_CHALLENGE {other:?}; only dns-01 is \ - supported in this build" + "unknown SURMOUNT_ACME_CHALLENGE {other:?}; use dns-01 or http-01" )), } } @@ -127,6 +127,7 @@ impl AcmeChallenge { pub fn as_str(self) -> &'static str { match self { Self::Dns01 => "dns-01", + Self::Http01 => "http-01", } } } @@ -186,6 +187,8 @@ pub struct AcmeConfig { pub dns_hook_timeout_secs: u64, /// Reissue when remaining leaf lifetime is under this many days. pub renew_days_before_expiry: u64, + /// Document root for HTTP-01 token files. Empty unless challenge is HTTP-01. + pub http01_webroot: PathBuf, } impl Default for AcmeConfig { @@ -201,6 +204,7 @@ impl Default for AcmeConfig { dns_hook_path: PathBuf::new(), dns_hook_timeout_secs: DEFAULT_ACME_DNS_HOOK_TIMEOUT_SECS, renew_days_before_expiry: DEFAULT_ACME_RENEW_DAYS_BEFORE_EXPIRY, + http01_webroot: PathBuf::new(), } } } @@ -254,6 +258,11 @@ impl AcmeConfig { } _ => DEFAULT_ACME_RENEW_DAYS_BEFORE_EXPIRY, }; + let http01_webroot = PathBuf::from( + get("SURMOUNT_ACME_HTTP01_WEBROOT") + .unwrap_or_default() + .trim(), + ); let cfg = Self { enable, @@ -266,6 +275,7 @@ impl AcmeConfig { dns_hook_path, dns_hook_timeout_secs, renew_days_before_expiry, + http01_webroot, }; cfg.validate()?; Ok(cfg) @@ -327,9 +337,16 @@ impl AcmeConfig { self.account_credentials_path )); } - // Challenge already refused HTTP-01 / TLS-ALPN at parse. - if self.challenge != AcmeChallenge::Dns01 { - return Err("only ACME DNS-01 is supported in this build".into()); + // TLS-ALPN is refused at parse. HTTP-01 needs an absolute webroot. + // DNS-01 does not. + if self.challenge == AcmeChallenge::Http01 + && (path_is_blank(&self.http01_webroot) || !self.http01_webroot.is_absolute()) + { + return Err( + "SURMOUNT_ACME_CHALLENGE=http-01 requires SURMOUNT_ACME_HTTP01_WEBROOT \ + as an absolute host path (fail-closed)" + .into(), + ); } // Lab mock must never target production Let's Encrypt (fail closed). if self.dns_provider == DnsProviderKind::Mock @@ -988,29 +1005,22 @@ async fn dns01_publish_wait( .map_err(|e| format!("ACME DNS-01 propagation wait failed: {e}")) } -async fn issue_with_instant_acme( - req: &IssueRequest, - dns: &dyn DnsProvider, -) -> Result { - use instant_acme::{Account, ChallengeType, Identifier, NewAccount, NewOrder, RetryPolicy}; +/// Load a saved ACME account, or register one and write the credentials file. +/// +/// Shared by DNS-01 and HTTP-01. Does not publish a challenge. +async fn open_acme_account(req: &IssueRequest) -> Result { + use instant_acme::{Account, NewAccount}; if req.domains.is_empty() { - return Err("instant-acme DNS-01 issuer requires at least one domain".into()); + return Err("ACME issuer requires at least one domain".into()); } - // Ensure process-level rustls provider before hyper-rustls ClientConfig // (Account::builder). Without this, rustls panics when no automatic // default is selected. Idempotent if main already installed aws-lc-rs. let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - - // Account::builder uses default hyper-rustls client (aws-lc-rs crypto). let builder = Account::builder().map_err(|e| format!("ACME account builder failed: {e}"))?; - let contact = format!("mailto:{}", req.email); - let (account, credentials) = if !path_is_blank(&req.account_credentials_path) - && req.account_credentials_path.exists() - { - // Fail closed: same owner-only regular-file contract as TLS private key. + if !path_is_blank(&req.account_credentials_path) && req.account_credentials_path.exists() { require_owner_only_secret_file(&req.account_credentials_path, "ACME account credentials")?; let raw = std::fs::read_to_string(&req.account_credentials_path).map_err(|e| { format!( @@ -1024,12 +1034,10 @@ async fn issue_with_instant_acme( req.account_credentials_path.display() ) })?; - let account = builder + builder .from_credentials(creds) .await - .map_err(|e| format!("restore ACME account from credentials: {e}"))?; - // Keep existing credentials file as-is. - (account, None) + .map_err(|e| format!("restore ACME account from credentials: {e}")) } else { let (account, creds) = builder .create( @@ -1043,12 +1051,18 @@ async fn issue_with_instant_acme( ) .await .map_err(|e| format!("ACME account create failed: {e}"))?; - (account, Some(creds)) - }; - - if let Some(creds) = credentials { write_account_credentials(&req.account_credentials_path, &creds)?; + Ok(account) } +} + +async fn issue_with_instant_acme( + req: &IssueRequest, + dns: &dyn DnsProvider, +) -> Result { + use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy}; + + let account = open_acme_account(req).await?; // DNS-01: track each FQDN immediately after set_txt; always clear on exit // (success or any failure after publish: wait, set_ready, poll, finalize, cert). @@ -1130,6 +1144,204 @@ async fn issue_with_instant_acme( order_result } +fn http01_challenge_dir(webroot: &Path) -> PathBuf { + webroot.join(".well-known").join("acme-challenge") +} + +/// Write one HTTP-01 key authorization. The file name is the token. +/// Body is the exact key authorization, no extra newline. +fn write_http01_token_file(webroot: &Path, token: &str, body: &str) -> Result { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + if http01_token_from_path(&format!("/.well-known/acme-challenge/{token}")).is_none() { + return Err("ACME HTTP-01 token is not a safe single path segment".into()); + } + if body.is_empty() + || body.len() > 512 + || !body.is_ascii() + || body.as_bytes().contains(&b'\n') + || body.as_bytes().contains(&0u8) + { + return Err( + "ACME HTTP-01 key authorization must be one non-empty ASCII line \ + (fail-closed)" + .into(), + ); + } + let dir = http01_challenge_dir(webroot); + std::fs::create_dir_all(&dir) + .map_err(|e| format!("create HTTP-01 challenge directory {}: {e}", dir.display()))?; + let path = dir.join(token); + if path.exists() { + let meta = std::fs::symlink_metadata(&path) + .map_err(|e| format!("stat HTTP-01 token {}: {e}", path.display()))?; + if meta.file_type().is_symlink() || !meta.is_file() { + return Err(format!( + "HTTP-01 token path is not a regular file: {}", + path.display() + )); + } + } + let tmp = dir.join(format!(".{token}.tmp")); + { + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o644) + .open(&tmp) + .map_err(|e| format!("open HTTP-01 token temp {}: {e}", tmp.display()))?; + file.write_all(body.as_bytes()) + .map_err(|e| format!("write HTTP-01 token temp {}: {e}", tmp.display()))?; + file.sync_all() + .map_err(|e| format!("sync HTTP-01 token temp {}: {e}", tmp.display()))?; + } + std::fs::rename(&tmp, &path).map_err(|e| { + let _ = std::fs::remove_file(&tmp); + format!("rename HTTP-01 token into {}: {e}", path.display()) + })?; + Ok(path) +} + +fn remove_http01_token_files(paths: &[PathBuf]) { + for path in paths { + let _ = std::fs::remove_file(path); + } +} + +/// Let's Encrypt production (or any configured directory) via HTTP-01. +/// +/// Token files land under `webroot`. The already-running management UI +/// serves them on port 80. This function does not bind a port and does +/// not call Namecheap. +async fn issue_http01(acme: &AcmeConfig) -> Result { + use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy}; + + if acme.challenge != AcmeChallenge::Http01 { + return Err("HTTP-01 issuer requires SURMOUNT_ACME_CHALLENGE=http-01".into()); + } + if path_is_blank(&acme.http01_webroot) || !acme.http01_webroot.is_absolute() { + return Err("HTTP-01 issuer requires an absolute SURMOUNT_ACME_HTTP01_WEBROOT".into()); + } + let req = IssueRequest { + domains: acme.domains.clone(), + email: acme.email.clone(), + directory_url: acme.directory_url.clone(), + account_credentials_path: acme.account_credentials_path.clone(), + }; + let account = open_acme_account(&req).await?; + let mut published: Vec = Vec::new(); + let order_result = async { + let identifiers: Vec = req + .domains + .iter() + .map(|d| Identifier::Dns(d.clone())) + .collect(); + let mut order = account + .new_order(&NewOrder::new(identifiers.as_slice())) + .await + .map_err(|e| format!("ACME new_order failed: {e}"))?; + let mut authorizations = order.authorizations(); + while let Some(result) = authorizations.next().await { + let mut authz = result.map_err(|e| format!("ACME authorization: {e}"))?; + match authz.status { + instant_acme::AuthorizationStatus::Pending => {} + instant_acme::AuthorizationStatus::Valid => continue, + other => { + return Err(format!("ACME authorization unexpected status: {other:?}")); + } + } + let mut challenge = authz + .challenge(ChallengeType::Http01) + .ok_or_else(|| "ACME order has no HTTP-01 challenge".to_string())?; + let aid = challenge.identifier(); + if aid.wildcard { + return Err( + "wildcard ACME identifiers are not supported for HTTP-01 in this build \ + (fail-closed)" + .into(), + ); + } + let token = challenge.token.clone(); + let body = challenge.key_authorization().as_str().to_string(); + let path = write_http01_token_file(&acme.http01_webroot, &token, &body)?; + published.push(path); + challenge + .set_ready() + .await + .map_err(|e| format!("ACME challenge set_ready: {e}"))?; + } + let status = order + .poll_ready(&RetryPolicy::default()) + .await + .map_err(|e| format!("ACME poll_ready: {e}"))?; + if status != instant_acme::OrderStatus::Ready { + return Err(format!( + "ACME order not ready after HTTP-01 (status={status:?}); fail-closed" + )); + } + let key_pem = order + .finalize() + .await + .map_err(|e| format!("ACME finalize (CSR/key): {e}"))?; + let cert_pem = order + .poll_certificate(&RetryPolicy::default()) + .await + .map_err(|e| format!("ACME poll_certificate: {e}"))?; + Ok(IssuedCert { cert_pem, key_pem }) + } + .await; + remove_http01_token_files(&published); + order_result +} + +/// What `acme-renew` did. NotDue means the leaf is still outside the window. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Http01RenewOutcome { + /// Existing PEMs are still usable. Nothing was written. + NotDue, + /// New cert and key were written to the configured paths. + WrotePems, +} + +/// Host timer entry. HTTP-01 only. Refuses DNS-01 and Namecheap hooks. +/// +/// Skips the directory when the leaf is outside the early-renew window and +/// already covers the configured names. Otherwise issues and writes PEMs. +pub async fn renew_http01_if_due( + paths: &TlsPaths, + acme: &AcmeConfig, +) -> Result { + if !acme.enable { + return Err("acme-renew requires SURMOUNT_ACME_ENABLE (fail-closed)".into()); + } + if acme.challenge != AcmeChallenge::Http01 { + return Err( + "acme-renew is the HTTP-01 path on this host. dns-01 stays the \ + in-process adapter and is not this timer (fail-closed)" + .into(), + ); + } + if acme.dns_provider != DnsProviderKind::None { + return Err("HTTP-01 renew requires SURMOUNT_ACME_DNS_PROVIDER=none \ + (Namecheap credentials stay off this host; fail-closed)" + .into()); + } + let now = SystemTime::now(); + let domains = if acme.domains.is_empty() { + None + } else { + Some(acme.domains.as_slice()) + }; + if pem_material_usable(paths, now, domains, acme.renew_days_before_expiry)? { + return Ok(Http01RenewOutcome::NotDue); + } + let issued = issue_http01(acme).await?; + write_issued_pems(paths, &issued)?; + Ok(Http01RenewOutcome::WrotePems) +} + fn write_account_credentials( path: &Path, creds: &instant_acme::AccountCredentials, @@ -1554,6 +1766,61 @@ pub enum TlsMaterialDecision { IssuedAndWrotePems { mock: bool }, } +/// Token file name from an HTTP-01 request path. Rejects traversal. +pub fn http01_token_from_path(path_and_query: &str) -> Option<&str> { + let path = path_and_query.split('?').next().unwrap_or(path_and_query); + let rest = path.strip_prefix("/.well-known/acme-challenge/")?; + if rest.is_empty() + || rest.contains('/') + || rest.contains('\\') + || rest.contains('.') + || rest.contains("..") + { + return None; + } + if !rest + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') + { + return None; + } + Some(rest) +} + +/// Read one HTTP-01 key authorization from the webroot. None when this +/// process is not in HTTP-01 mode, or the token file is missing. +/// +/// The file must be a regular file under `webroot/.well-known/acme-challenge/`, +/// at most 512 bytes, ASCII, and not a symlink. +pub fn read_http01_challenge_file(acme: &AcmeConfig, path_and_query: &str) -> Option> { + if !acme.enable + || acme.challenge != AcmeChallenge::Http01 + || path_is_blank(&acme.http01_webroot) + { + return None; + } + let token = http01_token_from_path(path_and_query)?; + let path = acme + .http01_webroot + .join(".well-known") + .join("acme-challenge") + .join(token); + let meta = std::fs::symlink_metadata(&path).ok()?; + if !meta.is_file() || meta.len() == 0 || meta.len() > 512 { + return None; + } + let root = acme.http01_webroot.canonicalize().ok()?; + let file = path.canonicalize().ok()?; + if !file.starts_with(&root) { + return None; + } + let bytes = std::fs::read(&path).ok()?; + if bytes.is_empty() || bytes.len() > 512 || !bytes.iter().all(|b| b.is_ascii() && *b != 0) { + return None; + } + Some(bytes) +} + /// Prepare TLS PEMs for HTTPS listen. /// /// - ACME **off**: require existing PEMs (fail closed). Same as historical path. @@ -1578,6 +1845,19 @@ pub async fn ensure_tls_material( } else { Some(acme.domains.as_slice()) }; + // HTTP-01 issuance is the surmount-cert-renew timer, not this process. + // Keep serving a leaf that has not reached notAfter. Early renew is the + // timer's job, so a missed run does not take HTTPS down inside the window. + if acme.challenge == AcmeChallenge::Http01 { + if pem_material_usable(paths, now, domains, 0)? { + return Ok(TlsMaterialDecision::ReusedExistingPems); + } + return Err( + "HTTP-01 PEMs are missing or expired. The surmount-cert-renew unit \ + must write the TLS cert and key (fail-closed; no silent cleartext)" + .into(), + ); + } if pem_material_usable(paths, now, domains, acme.renew_days_before_expiry)? { return Ok(TlsMaterialDecision::ReusedExistingPems); } @@ -1737,9 +2017,121 @@ mod tests { } #[test] - fn acme_http01_challenge_refused() { - let err = AcmeChallenge::parse("http-01").unwrap_err(); - assert!(err.contains("parked") || err.contains("HTTP-01"), "{err}"); + fn acme_http01_challenge_parses() { + assert_eq!( + AcmeChallenge::parse("http-01").unwrap(), + AcmeChallenge::Http01 + ); + assert_eq!(AcmeChallenge::Http01.as_str(), "http-01"); + assert_eq!( + http01_token_from_path("/.well-known/acme-challenge/abc_DEF-123"), + Some("abc_DEF-123") + ); + assert_eq!( + http01_token_from_path("/.well-known/acme-challenge/abc/def"), + None + ); + assert_eq!( + http01_token_from_path("/.well-known/acme-challenge/.."), + None + ); + assert_eq!(http01_token_from_path("/health"), None); + } + + #[test] + fn acme_http01_requires_absolute_webroot() { + let err = AcmeConfig::from_env_map(|k| match k { + "SURMOUNT_ACME_ENABLE" => Some("1".into()), + "SURMOUNT_ACME_DOMAINS" => Some("services.example.test".into()), + "SURMOUNT_ACME_EMAIL" => Some("ops@example.test".into()), + "SURMOUNT_ACME_DIRECTORY" => Some(directories::LETS_ENCRYPT_PRODUCTION.into()), + "SURMOUNT_ACME_ACCOUNT_CREDENTIALS_PATH" => { + Some("/var/lib/surmount/secrets/acme/account.json".into()) + } + "SURMOUNT_ACME_CHALLENGE" => Some("http-01".into()), + "SURMOUNT_ACME_DNS_PROVIDER" => Some("none".into()), + _ => None, + }) + .unwrap_err(); + assert!( + err.contains("HTTP01_WEBROOT") || err.contains("webroot"), + "{err}" + ); + + let cfg = AcmeConfig::from_env_map(|k| match k { + "SURMOUNT_ACME_ENABLE" => Some("1".into()), + "SURMOUNT_ACME_DOMAINS" => Some("services.example.test".into()), + "SURMOUNT_ACME_EMAIL" => Some("ops@example.test".into()), + "SURMOUNT_ACME_DIRECTORY" => Some(directories::LETS_ENCRYPT_PRODUCTION.into()), + "SURMOUNT_ACME_ACCOUNT_CREDENTIALS_PATH" => { + Some("/var/lib/surmount/secrets/acme/account.json".into()) + } + "SURMOUNT_ACME_CHALLENGE" => Some("http-01".into()), + "SURMOUNT_ACME_DNS_PROVIDER" => Some("none".into()), + "SURMOUNT_ACME_HTTP01_WEBROOT" => Some("/var/lib/surmount/acme-http01".into()), + _ => None, + }) + .unwrap(); + assert_eq!(cfg.challenge, AcmeChallenge::Http01); + assert_eq!(cfg.dns_provider, DnsProviderKind::None); + assert_eq!( + cfg.http01_webroot, + PathBuf::from("/var/lib/surmount/acme-http01") + ); + } + + #[test] + fn http01_token_file_roundtrip_and_rejects_dotdot() { + let dir = temp_dir("acme-http01-token"); + let err = write_http01_token_file(&dir, "..", "x").unwrap_err(); + assert!( + err.contains("safe"), + "dotdot token must be refused, got {err}" + ); + write_http01_token_file(&dir, "abc_DEF-123", "abc_DEF-123.thumb").unwrap(); + let acme = AcmeConfig { + enable: true, + challenge: AcmeChallenge::Http01, + http01_webroot: dir.clone(), + ..AcmeConfig::default() + }; + let body = read_http01_challenge_file(&acme, "/.well-known/acme-challenge/abc_DEF-123") + .expect("token file"); + assert_eq!(body, b"abc_DEF-123.thumb"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn renew_http01_refuses_dns01() { + let dir = temp_dir("acme-http01-refuse"); + let cert = dir.join("cert.pem"); + let key = dir.join("key.pem"); + let acme = AcmeConfig { + enable: true, + challenge: AcmeChallenge::Dns01, + dns_provider: DnsProviderKind::None, + directory_url: "https://acme-v02.api.letsencrypt.org/directory".into(), + email: "ops@example.test".into(), + domains: vec!["services.example.test".into()], + account_credentials_path: dir.join("account.json"), + http01_webroot: dir.join("webroot"), + ..AcmeConfig::default() + }; + let paths = TlsPaths { + cert_path: cert, + key_path: key, + }; + let err = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(renew_http01_if_due(&paths, &acme)) + .unwrap_err(); + assert!( + err.contains("HTTP-01"), + "dns-01 must not take the host renew path, got {err}" + ); + let _ = std::fs::remove_dir_all(&dir); } #[test] diff --git a/crates/management-ui/src/config.rs b/crates/management-ui/src/config.rs index c9e9b67..38bbee3 100644 --- a/crates/management-ui/src/config.rs +++ b/crates/management-ui/src/config.rs @@ -744,6 +744,42 @@ impl AppConfig { } } +/// Settings `surmount-management-ui acme-renew` needs. +/// +/// The command is not an HTTP route. It must not go through +/// [`AppConfig::from_env`], because that parse refuses nostr mode without +/// `SURMOUNT_SESSION_SECRET` before any issuance. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AcmeRenewSettings { + pub acme: AcmeConfig, + pub tls: TlsPaths, +} + +impl AcmeRenewSettings { + /// Load ACME and TLS settings for the host renew command. + /// + /// Does not call [`AppConfig::from_env`]. That parse validates nostr + /// auth and refuses a missing session secret. This command does not + /// serve HTTP and does not need that secret. + pub fn from_env() -> Result { + let acme = AcmeConfig::from_env()?; + let listen = ListenMode::from_parts( + env::var("SURMOUNT_LISTEN_MODE").ok().as_deref(), + env::var("SURMOUNT_TLS_CERT").ok().as_deref(), + env::var("SURMOUNT_TLS_KEY").ok().as_deref(), + )?; + let tls = match listen { + ListenMode::Https(paths) => paths, + ListenMode::PlainHttp => { + return Err("acme-renew requires SURMOUNT_LISTEN_MODE=https with \ + SURMOUNT_TLS_CERT and SURMOUNT_TLS_KEY (fail-closed)" + .into()); + } + }; + Ok(Self { acme, tls }) + } +} + /// SoT for truthy env flags (`1` / `true` / `yes` / `on`, case-insensitive). /// /// Used by [`env_bool`] and directory lab-escape @@ -1045,6 +1081,7 @@ mod tests { "SURMOUNT_ACME_DOMAINS", "SURMOUNT_ACME_ACCOUNT_CREDENTIALS_PATH", "SURMOUNT_ACME_CHALLENGE", + "SURMOUNT_ACME_HTTP01_WEBROOT", "SURMOUNT_ACME_DNS_PROVIDER", "SURMOUNT_ACME_DNS_HOOK", "SURMOUNT_ACME_DNS_HOOK_TIMEOUT_SECS", @@ -1369,6 +1406,77 @@ mod tests { ); } + /// Named contract: `acme-renew` is not an HTTP route. Nostr mode without + /// `SURMOUNT_SESSION_SECRET` must not refuse before issue. The HTTP + /// server path still refuses that same environment. + #[test] + fn acme_renew_nostr_without_session_secret_does_not_exit_before_issue() { + let _g = EnvGuard::acquire(); + set_env("SURMOUNT_AUTH_MODE", "nostr"); + set_env("SURMOUNT_LISTEN", "0.0.0.0:443"); + set_env("SURMOUNT_LISTEN_MODE", "https"); + set_env( + "SURMOUNT_TLS_CERT", + "/var/lib/surmount/secrets/tls/cert.pem", + ); + set_env("SURMOUNT_TLS_KEY", "/var/lib/surmount/secrets/tls/key.pem"); + set_env("SURMOUNT_ACME_ENABLE", "1"); + set_env("SURMOUNT_ACME_CHALLENGE", "http-01"); + set_env("SURMOUNT_ACME_DNS_PROVIDER", "none"); + set_env( + "SURMOUNT_ACME_DIRECTORY", + "https://acme-v02.api.letsencrypt.org/directory", + ); + set_env("SURMOUNT_ACME_EMAIL", "ops@example.test"); + set_env( + "SURMOUNT_ACME_DOMAINS", + "services.example.test,mail.example.test", + ); + set_env( + "SURMOUNT_ACME_ACCOUNT_CREDENTIALS_PATH", + "/var/lib/surmount/secrets/acme/account.json", + ); + set_env( + "SURMOUNT_ACME_HTTP01_WEBROOT", + "/var/lib/surmount/acme-http01", + ); + + let settings = AcmeRenewSettings::from_env(); + assert!( + settings.is_ok(), + "acme-renew exited before issue (nostr without session secret): {settings:?}" + ); + let settings = settings.unwrap(); + assert!(settings.acme.enable); + assert_eq!(settings.acme.challenge, crate::acme::AcmeChallenge::Http01); + assert_eq!( + settings.acme.dns_provider, + crate::acme::DnsProviderKind::None + ); + assert_eq!( + settings.acme.domains, + vec![ + "services.example.test".to_string(), + "mail.example.test".to_string() + ] + ); + assert_eq!( + settings.tls.cert_path.as_os_str(), + std::ffi::OsStr::new("/var/lib/surmount/secrets/tls/cert.pem") + ); + assert_eq!( + settings.tls.key_path.as_os_str(), + std::ffi::OsStr::new("/var/lib/surmount/secrets/tls/key.pem") + ); + + let err = AppConfig::from_env() + .expect_err("the HTTP server must still refuse nostr without the session secret"); + assert!( + err.contains("SESSION_SECRET"), + "full UI config must still refuse nostr without the session secret: {err}" + ); + } + #[test] fn auth_mode_nostr_with_secret_ok() { let _g = EnvGuard::acquire(); diff --git a/crates/management-ui/src/main.rs b/crates/management-ui/src/main.rs index 71dbfc9..71f68bf 100644 --- a/crates/management-ui/src/main.rs +++ b/crates/management-ui/src/main.rs @@ -21,10 +21,11 @@ //! for local reverse-proxies (Arti HS lean path) when primary is https //! - In-memory fixed-window rate limit; trusted client IP when peer is loopback //! - Ban decision layer (whitelist last-used, optional enforce/dry-run; default off) -//! - Optional in-process ACME (DNS-01; default off). HTTP-01 on product :80 parked. +//! - Optional in-process ACME (DNS-01; default off). HTTP-01 challenge files +//! are served from `SURMOUNT_ACME_HTTP01_WEBROOT` when that challenge is on. //! //! Stalwart remains the mail engine. Auth: Nostr foundation (mode off by default). -//! ACME HTTP-01 on product :80 is parked (see RESIDUAL.md). +//! Enduring Let's Encrypt renewal is the surmount-1 `surmount-cert-renew` timer. mod acme; mod apex_static; @@ -64,7 +65,7 @@ use crate::apex_static::{ apex_static_file, document_root_is_ready, public_site_csp, serve_apex_static_file, }; use crate::client_ip::client_ip_for_rate_limit; -use crate::config::AppConfig; +use crate::config::{AcmeRenewSettings, AppConfig}; use crate::directory::Directory; use crate::mta_sts::{mta_sts_policy_body, should_serve_mta_sts_policy}; use crate::redirect::{ @@ -126,6 +127,37 @@ async fn main() -> anyhow::Result<()> { // and other rustls consumers panic without this. Already-installed is OK. let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + // Host timer: `surmount-management-ui acme-renew`. Issues HTTP-01 and + // writes the PEM paths, then exits. Does not bind :80 or :443. The + // already-running process serves the challenge files. + // + // This branch is before AppConfig::from_env. That parse refuses nostr + // mode without the session secret. acme-renew is not an HTTP route and + // loads ACME and TLS settings only. + if std::env::args().nth(1).as_deref() == Some("acme-renew") { + if std::env::args().len() != 2 { + bail!("acme-renew takes no extra arguments"); + } + let settings = AcmeRenewSettings::from_env() + .map_err(anyhow::Error::msg) + .context( + "acme-renew loads ACME and TLS settings only (no session secret; fail-closed)", + )?; + match crate::acme::renew_http01_if_due(&settings.tls, &settings.acme) + .await + .map_err(anyhow::Error::msg) + .context("HTTP-01 renew (fail-closed; no Namecheap)")? + { + crate::acme::Http01RenewOutcome::NotDue => { + info!("HTTP-01 renew not due; existing PEMs still usable"); + } + crate::acme::Http01RenewOutcome::WrotePems => { + info!("HTTP-01 renew wrote the TLS cert and key this host presents"); + } + } + return Ok(()); + } + let config = AppConfig::from_env().map_err(anyhow::Error::msg)?; config .validate_local_cleartext() @@ -1600,6 +1632,17 @@ async fn http_redirect_handler(State(state): State>, request: Requ .path_and_query() .map(|pq| pq.as_str()) .unwrap_or("/"); + // HTTP-01 token, before the HTTPS redirect. Let's Encrypt fetches this + // on port 80. Other paths still redirect. + if let Some(body) = crate::acme::read_http01_challenge_file(&state.config.acme, path_and_query) + { + return ( + StatusCode::OK, + [("content-type", "text/plain; charset=utf-8")], + body, + ) + .into_response(); + } let https_port = https_port_for_redirect(state.config.listen.port()); match http_upgrade_location( true, @@ -1632,6 +1675,16 @@ async fn apex_public_host_middleware( let services = state.config.services_hostname.as_str(); let path_only = path.split('?').next().unwrap_or(path); + // HTTP-01 on the HTTPS listener too, in case a client followed the :80 redirect. + if let Some(body) = crate::acme::read_http01_challenge_file(&state.config.acme, path_only) { + return ( + StatusCode::OK, + [("content-type", "text/plain; charset=utf-8")], + body, + ) + .into_response(); + } + // Router::layer runs after routing. Onion Host rewrite sets the mapped // clearnet Host; serve MTA-STS here when that Host is the policy name. if path_only == "/.well-known/mta-sts.txt" diff --git a/crates/management-ui/src/pages.rs b/crates/management-ui/src/pages.rs index 5763684..435aec4 100644 --- a/crates/management-ui/src/pages.rs +++ b/crates/management-ui/src/pages.rs @@ -1392,7 +1392,7 @@ pub fn render_mail_page_with( "Stalwart HTTP answered. Mail protocols still depend on host DNS, firewall, and account setup. Browser webmail is not shipped yet. Use IMAP and SMTP in Evolution or iPhone Mail, or use JMAP when the authenticated proxy lands." .to_string() } else { - "When down: start Stalwart on the host, confirm the configured probe URL, or use SSH tunnel / bootstrap /stalwart-admin/ while bringing the host up. Probe URL env is documented on System." + "When down: start Stalwart on the host, confirm the configured probe URL, or use an SSH tunnel to the loopback Stalwart HTTP listener while bringing the host up. Probe URL env is documented on System." .to_string() }; diff --git a/crates/management-ui/src/redirect.rs b/crates/management-ui/src/redirect.rs index 21f3fe5..516c85b 100644 --- a/crates/management-ui/src/redirect.rs +++ b/crates/management-ui/src/redirect.rs @@ -4,8 +4,8 @@ //! `redirect_http_to_https` and `http_redirect_listen` are set (see main). //! Dual-run nginx (`web.enable`) must not also own :80; Nix asserts that mutex. //! -//! ACME HTTP-01 on product :80 is parked (Q-EDGE / residual). This path only -//! builds redirect targets and bind decisions. +//! HTTP-01 token files are served by the :80 handler before this redirect. +//! This module only builds redirect targets and bind decisions. use std::collections::BTreeMap; use std::fmt; diff --git a/crates/management-ui/src/status.rs b/crates/management-ui/src/status.rs index 946ae06..8a8086c 100644 --- a/crates/management-ui/src/status.rs +++ b/crates/management-ui/src/status.rs @@ -1,12 +1,15 @@ //! Public systems dashboard for Host `status.{primary}` (Axum, not nginx). //! //! Not the operator console. Rows come from a product inventory plus live -//! systemd ActiveState / SubState (and optional HTTP samples). Host uptime -//! comes from `/proc/uptime`. No RAM, disk, IP, onion, or cookie bytes. +//! probes. A Splora indexer row is up only when that network's proxy socket +//! accepted a connection. systemd ActiveState=active is not enough. Other +//! rows still use ActiveState / SubState and optional HTTP samples. Host +//! uptime comes from `/proc/uptime`. No RAM, disk, IP, onion, or cookie bytes. use std::collections::BTreeMap; use std::net::IpAddr; -use std::path::Path; +use std::os::unix::net::UnixStream; +use std::path::{Path, PathBuf}; use std::process::Command; use axum::http::{StatusCode, header}; @@ -46,6 +49,12 @@ pub struct ComponentProbe { pub sub_state: String, pub load_state: String, pub http_status: Option, + /// None: this row is not a Splora listen check. + /// Some(true): that network's proxy socket accepted a connection. + /// A first HTTP response of 401 on that connection is still up. + /// Some(false): the socket is absent or refused. Not Up. + /// HTTP status 0 is not up. + pub socket_accepts: Option, } /// One product row the dashboard knows how to name. @@ -71,10 +80,26 @@ pub const DASHBOARD_UNITS: &[&str] = &[ "splora-liquid.service", ]; -/// Ordinary English status from systemd ActiveState / SubState / HTTP samples. +/// Ordinary English status from listen result, ActiveState / SubState, or HTTP. /// -/// Never returns a constant "ok". HTTP client errors win over a running unit. +/// Never returns a constant "ok". HTTP client errors win over a running unit +/// when that row did not accept a proxy socket. An accepted socket is +/// "running" even when the first HTTP status is 401 (Splora's auth gate +/// with public health off). Status 0 is not a response and stays "inactive". +/// `socket_accepts` Some(false) is "inactive" (the existing down word). +/// Some(true) is "running" (the existing up word). None keeps systemd. pub fn classify_component_status(probe: &ComponentProbe) -> String { + if probe.http_status == Some(0) { + return "inactive".to_string(); + } + // 401 on an accepted connection is the indexer auth gate, not a down row. + // Other 4xx/5xx still win when the socket did not accept, and they still + // win on an accepted socket except for that 401 gate. + if probe.socket_accepts == Some(true) + && !matches!(probe.http_status, Some(code) if code >= 400 && code != 401) + { + return "running".to_string(); + } if let Some(code) = probe.http_status && code >= 400 { @@ -83,9 +108,12 @@ pub fn classify_component_status(probe: &ComponentProbe) -> String { let load = probe.load_state.trim(); let active = probe.active_state.trim(); let sub = probe.sub_state.trim(); - if load.eq_ignore_ascii_case("not-found") { + if load.eq_ignore_ascii_case("not-found") && probe.socket_accepts != Some(true) { return "not installed".to_string(); } + if probe.socket_accepts == Some(false) { + return "inactive".to_string(); + } if active.eq_ignore_ascii_case("failed") || sub.eq_ignore_ascii_case("failed") { return "failed".to_string(); } @@ -125,6 +153,50 @@ pub fn classify_component_status(probe: &ComponentProbe) -> String { "unknown".to_string() } +/// HTTP Unix socket the public Splora proxy uses for this indexer name. +/// +/// `/testnet` is testnet3, `/testnet4` is testnet4, `/signet` and +/// `/mutinynet` are mutinynet, `/liquid` is liquid. Mainnet is not a +/// dashboard row (None). A configured instance socket wins; otherwise +/// the proxy default is `{DEFAULT_SOCKET_DIR}/{instance}.http.sock`. +pub fn splora_dashboard_socket(config: &AppConfig, instance: &str) -> Option { + if !matches!(instance, "testnet3" | "testnet4" | "mutinynet" | "liquid") { + return None; + } + if let Some(inst) = config.splora_proxy.instance_by_name(instance) { + return Some(inst.http_socket.clone()); + } + Some(PathBuf::from(format!( + "{}/{instance}.http.sock", + crate::proxy_vaultwarden::splora::DEFAULT_SOCKET_DIR + ))) +} + +/// True only when `path` accepts a Unix connection. Missing path is false. +pub fn unix_socket_accepts(path: &Path) -> bool { + UnixStream::connect(path).is_ok() +} + +/// Set `socket_accepts` on the four Splora dashboard units from the proxy socket. +fn annotate_splora_listen(config: &AppConfig, probes: &mut BTreeMap) { + const ROWS: &[(&str, &str)] = &[ + ("splora-testnet3.service", "testnet3"), + ("splora-testnet4.service", "testnet4"), + ("splora-mutinynet.service", "mutinynet"), + ("splora-liquid.service", "liquid"), + ]; + for (unit, instance) in ROWS { + let Some(path) = splora_dashboard_socket(config, instance) else { + continue; + }; + let accepts = unix_socket_accepts(&path); + probes + .entry((*unit).to_string()) + .or_default() + .socket_accepts = Some(accepts); + } +} + /// Standing number law: seconds; minutes at most 60 minutes; N.N hours through /// 24 hours; N.N days after that. pub fn format_uptime_seconds(seconds: f64) -> String { @@ -428,7 +500,7 @@ fn read_host_uptime() -> String { { return format_uptime_seconds(n); } - return format_uptime_seconds(720.0); + format_uptime_seconds(720.0) } #[cfg(not(test))] { @@ -469,7 +541,9 @@ fn live_unit_probes() -> BTreeMap { /// Build the payload the status Host handler actually emits. pub fn systems_status_from_config(config: &AppConfig) -> SystemsStatusPayload { - systems_status_from_probes(config, &read_host_uptime(), &live_unit_probes()) + let mut probes = live_unit_probes(); + annotate_splora_listen(config, &mut probes); + systems_status_from_probes(config, &read_host_uptime(), &probes) } /// Serve HTML dashboard, JSON (`/status.json` or Accept), or closed 404. @@ -618,6 +692,7 @@ mod tests { sub_state: sub.into(), load_state: load.into(), http_status: http, + socket_accepts: None, } } @@ -895,4 +970,275 @@ Id=sshd.service\nLoadState=loaded\nActiveState=failed\nSubState=failed\n", assert_eq!(show["stalwart-mail.service"].active_state, "active"); assert_eq!(show["sshd.service"].active_state, "failed"); } + + /// Named contract: unit active and socket absent is not Up. + /// + /// The page's up word is "running". The existing not-up word for a + /// service that is down is "inactive" (ActiveState=inactive). + #[test] + fn splora_active_unit_with_absent_socket_is_not_up() { + let cfg = AppConfig::example_test(); + assert_eq!( + splora_dashboard_socket(&cfg, "testnet3") + .unwrap() + .to_str() + .unwrap(), + "/run/splora/testnet3.http.sock" + ); + assert_eq!( + splora_dashboard_socket(&cfg, "testnet4") + .unwrap() + .to_str() + .unwrap(), + "/run/splora/testnet4.http.sock" + ); + assert_eq!( + splora_dashboard_socket(&cfg, "mutinynet") + .unwrap() + .to_str() + .unwrap(), + "/run/splora/mutinynet.http.sock" + ); + assert_eq!( + splora_dashboard_socket(&cfg, "liquid") + .unwrap() + .to_str() + .unwrap(), + "/run/splora/liquid.http.sock" + ); + assert!( + splora_dashboard_socket(&cfg, "mainnet").is_none(), + "mainnet stays off and must not be shown as Up" + ); + assert!( + !DASHBOARD_UNITS.iter().any(|unit| unit.contains("mainnet")), + "mainnet must not be a dashboard unit: {DASHBOARD_UNITS:?}" + ); + + let mut custom = cfg.clone(); + custom + .splora_proxy + .instances + .push(crate::proxy_vaultwarden::splora::SploraInstance { + name: "testnet3".into(), + hosts: Vec::new(), + http_socket: PathBuf::from("/run/splora/testnet3.http.sock"), + }); + assert_eq!( + splora_dashboard_socket(&custom, "testnet3") + .unwrap() + .to_str() + .unwrap(), + "/run/splora/testnet3.http.sock" + ); + + let missing = std::env::temp_dir().join(format!( + "surmount-status-absent-socket-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + )); + assert!( + !unix_socket_accepts(&missing), + "a missing socket must not count as accepted" + ); + + let mut probes = BTreeMap::new(); + for unit in [ + "splora-testnet3.service", + "splora-testnet4.service", + "splora-mutinynet.service", + "splora-liquid.service", + ] { + probes.insert( + unit.to_string(), + ComponentProbe { + active_state: "active".into(), + sub_state: "running".into(), + load_state: "loaded".into(), + http_status: None, + socket_accepts: Some(false), + }, + ); + } + let payload = systems_status_from_probes(&cfg, "1 minute", &probes); + for id in [ + "splora-testnet3", + "splora-testnet4", + "splora-mutinynet", + "splora-liquid", + ] { + let row = payload + .systems + .iter() + .find(|c| c.name == id) + .unwrap_or_else(|| panic!("missing row {id}")); + assert_eq!( + row.state, "inactive", + "{id} unit active with absent socket must not be Up (got {})", + row.state + ); + assert_ne!(row.state, "running"); + assert_ne!(row.state, "Up"); + } + let html = render_systems_status_html(&payload); + assert!(html.contains("data-state=\"inactive\"")); + assert!( + !html.contains("data-state=\"running\""), + "absent socket must not paint running/Up: {html}" + ); + assert!(!html.contains("data-state=\"Up\"")); + assert!( + !html.contains("mainnet"), + "mainnet must not appear on the status page" + ); + + let mut accepting = BTreeMap::new(); + accepting.insert( + "splora-testnet3.service".into(), + ComponentProbe { + active_state: "active".into(), + sub_state: "running".into(), + load_state: "loaded".into(), + http_status: None, + socket_accepts: Some(true), + }, + ); + let up = systems_status_from_probes(&cfg, "1 minute", &accepting); + let up_row = up + .systems + .iter() + .find(|c| c.name == "splora-testnet3") + .expect("splora-testnet3"); + assert_eq!( + up_row.state, "running", + "an accepting socket uses the page up word" + ); + } + + /// Named contract: an accepted socket whose first HTTP response is 401 is up. + /// + /// Splora with public health off uses 401 as the auth gate. That is not + /// down. The page up word is "running". Status 0 and a real HTTP error + /// other than that gate stay not up. + #[test] + fn splora_accepted_socket_with_http_401_is_up() { + use std::io::{Read, Write}; + use std::os::unix::net::UnixListener; + use std::sync::atomic::{AtomicBool, Ordering}; + + let dir = std::env::temp_dir().join(format!( + "sm401-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + )); + std::fs::create_dir_all(&dir).unwrap(); + let sock_path = dir.join("t4.sock"); + let _ = std::fs::remove_file(&sock_path); + let listener = UnixListener::bind(&sock_path).unwrap(); + let ready = std::sync::Arc::new(AtomicBool::new(false)); + let ready2 = ready.clone(); + let join = std::thread::spawn(move || { + ready2.store(true, Ordering::SeqCst); + let response = + b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + for _ in 0..2 { + let (mut stream, _) = listener.accept().unwrap(); + let _ = stream.write_all(response); + let _ = stream.flush(); + } + }); + while !ready.load(Ordering::SeqCst) { + std::thread::sleep(std::time::Duration::from_millis(1)); + } + std::thread::sleep(std::time::Duration::from_millis(20)); + + assert!( + unix_socket_accepts(&sock_path), + "an accepted socket is up when the indexer answers HTTP 401" + ); + + let mut reader = UnixStream::connect(&sock_path).expect("second connect to the 401 stub"); + reader + .set_read_timeout(Some(std::time::Duration::from_secs(2))) + .expect("read timeout"); + let mut buf = [0u8; 64]; + let n = reader.read(&mut buf).expect("read the stub status line"); + let text = std::str::from_utf8(&buf[..n]).expect("status line is utf-8"); + let code: u16 = text + .split_whitespace() + .nth(1) + .expect("HTTP status code") + .parse() + .expect("numeric status"); + assert_eq!( + code, 401, + "the stub's first HTTP response is 401, got {text}" + ); + + let cfg = AppConfig::example_test(); + let mut probes = BTreeMap::new(); + probes.insert( + "splora-testnet4.service".into(), + ComponentProbe { + active_state: "active".into(), + sub_state: "running".into(), + load_state: "loaded".into(), + http_status: Some(code), + socket_accepts: Some(true), + }, + ); + let payload = systems_status_from_probes(&cfg, "1 minute", &probes); + let row = payload + .systems + .iter() + .find(|c| c.name == "splora-testnet4") + .expect("splora-testnet4"); + assert_eq!( + row.state, "running", + "HTTP 401 on an accepted socket is up, not down (got {})", + row.state + ); + assert_ne!(row.state, "inactive"); + assert_ne!(row.state, "HTTP 401"); + let html = render_systems_status_html(&payload); + assert!( + html.contains("data-system=\"splora-testnet4\""), + "testnet4 row must be on the page" + ); + assert!( + html.contains("data-state=\"running\""), + "accepted 401 socket must paint the up word: {html}" + ); + assert_eq!( + classify_component_status(&ComponentProbe { + active_state: "active".into(), + sub_state: "running".into(), + load_state: "loaded".into(), + http_status: Some(0), + socket_accepts: Some(true), + }), + "inactive", + "HTTP 000 stays not up" + ); + assert_eq!( + classify_component_status(&ComponentProbe { + active_state: "active".into(), + sub_state: "running".into(), + load_state: "loaded".into(), + http_status: Some(502), + socket_accepts: Some(true), + }), + "HTTP 502", + "HTTP 502 on an accepted socket stays not the up word" + ); + + let _ = join.join(); + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/crates/surmount-private-data/src/lib.rs b/crates/surmount-private-data/src/lib.rs index 7bae8b0..0e94a77 100644 --- a/crates/surmount-private-data/src/lib.rs +++ b/crates/surmount-private-data/src/lib.rs @@ -69,6 +69,7 @@ pub fn is_fixture_path(rel: &str) -> bool { || p.starts_with("script/testdata/private-data/") || p == "crates/surmount-private-data/testdata" || p.starts_with("crates/surmount-private-data/testdata/") + || p == "crates/surmount-rekey/src/lib.rs" } pub fn is_scanner_tool_path(rel: &str) -> bool { @@ -347,4 +348,39 @@ mod tests { )); assert!(!is_fixture_path("hosts/mail-vps/configuration.nix")); } + + #[test] + fn rekey_age_test_vector_is_excluded_and_still_detected() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../surmount-rekey/src/lib.rs"); + let bytes = std::fs::read(&path) + .unwrap_or_else(|e| panic!("missing rekey test file {}: {e}", path.display())); + let text = String::from_utf8_lossy(&bytes); + const PREFIX: &str = "AGE-SECRET-KEY-1"; + let mut from = 0; + let mut found = false; + while let Some(pos) = text[from..].find(PREFIX) { + let i = from + pos + PREFIX.len(); + let rest = text.as_bytes().get(i..).unwrap_or(&[]); + if rest + .iter() + .take_while(|b| b.is_ascii_uppercase() || b.is_ascii_digit()) + .count() + >= 1 + { + found = true; + break; + } + from += pos + 1; + } + assert!( + found, + "crates/surmount-rekey/src/lib.rs no longer contains an ignored age test key" + ); + let rel = "crates/surmount-rekey/src/lib.rs"; + let hits = scan_named(rel, &bytes, scan::Gate::Paths); + assert!(classes(&hits).contains(&"age-secret-key"), "{hits:?}"); + assert!(is_fixture_path(rel)); + assert!(is_fixture_path("./crates/surmount-rekey/src/lib.rs")); + assert!(!is_fixture_path("crates/surmount-rekey/src/main.rs")); + } } diff --git a/crates/surmount-rekey/Cargo.toml b/crates/surmount-rekey/Cargo.toml index f293923..09f4ff7 100644 --- a/crates/surmount-rekey/Cargo.toml +++ b/crates/surmount-rekey/Cargo.toml @@ -3,7 +3,7 @@ name = "surmount-rekey" version.workspace = true edition.workspace = true license.workspace = true -description = "SHC zero-knowledge backup re-key paste (PGP wrap via rage/age + gpg)" +description = "SHC zero-knowledge backup re-key paste (X25519 age identity + gpg)" publish = false [[bin]] @@ -15,9 +15,14 @@ name = "surmount_rekey" path = "src/lib.rs" [dependencies] -age.workspace = true anyhow.workspace = true +# 0.11 matches the age identity Bech32 (BIP-173) strings. 0.12 is a different +# major already used elsewhere and is not this encoding. +bech32 = "0.11.1" secrecy = "0.10" +# static_secrets: reusable X25519 secret. getrandom: OsRng via StaticSecret::random. +# Not the age crate. age 0.12.1 always depends on hpke and p256. +x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } [lints] workspace = true diff --git a/crates/surmount-rekey/src/lib.rs b/crates/surmount-rekey/src/lib.rs index ee60832..3dd3a48 100644 --- a/crates/surmount-rekey/src/lib.rs +++ b/crates/surmount-rekey/src/lib.rs @@ -1,9 +1,13 @@ //! SHC zero-knowledge backup re-key helper. //! -//! Default rung is PGP: mint an X25519 age identity with the `age` crate -//! (rage's library), wrap it with the operator's gpg, print only the public -//! `age1...` recipient for the SHC Backups panel (kind pgp). +//! Default rung is PGP: mint an X25519 age identity (Bech32 `AGE-SECRET-KEY-1` +//! secret and `age1` recipient, same byte layout as rage), wrap it with the +//! operator's gpg, print only the public `age1...` recipient for the SHC +//! Backups panel (kind pgp). //! Never prints AGE-SECRET-KEY-1 on stdout. +//! +//! This crate does not depend on the `age` crate. age 0.12.1 always links +//! hpke and p256, and that edge requires crypto-bigint 0.5. use std::fs; use std::io::Write; @@ -11,8 +15,40 @@ use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use age::x25519::Identity; -use secrecy::ExposeSecret; +use bech32::{Bech32, Hrp}; +use secrecy::{ExposeSecret, SecretString}; +use x25519_dalek::{PublicKey, StaticSecret}; + +const SECRET_KEY_PREFIX: Hrp = Hrp::parse_unchecked("AGE-SECRET-KEY-"); +const PUBLIC_KEY_PREFIX: Hrp = Hrp::parse_unchecked("age"); + +/// X25519 age identity. Secret bytes are the unclamped 32-byte scalar that +/// `StaticSecret` stores. The public recipient is the clamped Montgomery +/// base-point product, Bech32 with HRP `age`. Same steps as age 0.12 +/// `Identity::generate` / `to_string` / `to_public`. +struct X25519Identity { + secret: StaticSecret, +} + +impl X25519Identity { + fn generate() -> Self { + Self { + secret: StaticSecret::random(), + } + } + + fn secret_string(&self) -> Result { + let encoded = bech32::encode_lower::(SECRET_KEY_PREFIX, self.secret.as_bytes()) + .map_err(|e| RekeyError::fail(format!("age identity encode: {e}")))?; + Ok(SecretString::from(encoded.to_uppercase())) + } + + fn recipient(&self) -> Result { + let pk = PublicKey::from(&self.secret); + bech32::encode_lower::(PUBLIC_KEY_PREFIX, pk.as_bytes()) + .map_err(|e| RekeyError::fail(format!("age recipient encode: {e}"))) + } +} #[derive(Debug)] pub struct RekeyError { @@ -67,14 +103,15 @@ pub fn default_gpg_recipient() -> String { /// Mint identity, write 0600 identity file, wrap with gpg unless dry_run. pub fn run_rekey(req: &RekeyRequest) -> Result { fs::create_dir_all(&req.dir).map_err(|e| RekeyError::fail(format!("mkdir: {e}")))?; - let identity = Identity::generate(); - let recipient = identity.to_public().to_string(); + let identity = X25519Identity::generate(); + let recipient = identity.recipient()?; if !recipient.starts_with("age1") { return Err(RekeyError::fail("age identity produced no age1 recipient")); } + let secret = identity.secret_string()?; let identity_path = req.dir.join("age-identity.txt"); let envelope_path = req.dir.join("envelope.asc"); - write_secret_file(&identity_path, identity.to_string().expose_secret())?; + write_secret_file(&identity_path, secret.expose_secret())?; if req.dry_run { fs::write( &envelope_path, @@ -85,7 +122,7 @@ pub fn run_rekey(req: &RekeyRequest) -> Result { gpg_wrap( &req.gpg_bin, &req.gpg_recipient, - identity.to_string().expose_secret().as_bytes(), + secret.expose_secret().as_bytes(), &envelope_path, )?; } @@ -251,6 +288,24 @@ mod tests { bin } + #[test] + fn age_test_vector_recipient_matches_secret() { + // Published age 0.12 x25519 test vector (not a live key). + const TEST_SK: &str = + "AGE-SECRET-KEY-1GQ9778VQXMMJVE8SK7J6VT8UJ4HDQAJUVSFCWCM02D8GEWQ72PVQ2Y5J33"; + const TEST_PK: &str = "age1t7rxyev2z3rw82stdlrrepyc39nvn86l5078zqkf5uasdy86jp6svpy7pa"; + let parsed = bech32::primitives::decode::CheckedHrpstring::new::(TEST_SK) + .expect("age test secret is Bech32"); + assert_eq!(parsed.hrp(), SECRET_KEY_PREFIX); + let bytes: Vec = parsed.byte_iter().collect(); + let sk_bytes: [u8; 32] = bytes.as_slice().try_into().expect("32-byte scalar"); + let identity = X25519Identity { + secret: StaticSecret::from(sk_bytes), + }; + assert_eq!(identity.recipient().unwrap(), TEST_PK); + assert_eq!(identity.secret_string().unwrap().expose_secret(), TEST_SK); + } + #[test] fn card_has_pgp_and_age1_not_secret() { let rec = "age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq2xq5s0"; diff --git a/crates/surmount-rekey/src/main.rs b/crates/surmount-rekey/src/main.rs index f8a3184..da7400c 100644 --- a/crates/surmount-rekey/src/main.rs +++ b/crates/surmount-rekey/src/main.rs @@ -9,7 +9,7 @@ Usage: surmount-rekey [--help] surmount-rekey [--dry-run] [--dir DIR] [--gpg-recipient ADDR] [--gpg-bin PATH] -Mint an age identity (rage/age crate), wrap it with gpg, print the public +Mint an X25519 age identity, wrap it with gpg, print the public age1 recipient for SHC Backups re-key (type PGP). Does not print AGE-SECRET-KEY-1. Does not talk to SHC. diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index c708ae3..728e5e8 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -4,7 +4,10 @@ Read this first, then follow links into child docs. Do not invent product scope from memory alone. -**Last updated:** 2026-10-05 (`just publish` is the static-site command +**Last updated:** 2026-11-03 (number every parallel list in operator +chat; horizon is this laptop; surmount-1 is the mail host and the +remote Nix builder; the private overlay directory is not a machine). +Prior 2026-10-05 (`just publish` is the static-site command and `just deploy` is the host switch. `just deploy-dry-run` is the agent dry-run. `just deploy-host` stays the explicit equivalent with no default target). Prior 2026-09-20 (full test-and-deploy: @@ -79,6 +82,21 @@ line is eternal law. Prefer **operator direction YYYY-MM-DD**, **proposed**, [open-choices.md](open-choices.md), [../AGENTS.md](../AGENTS.md). +## Numbered lists and machine names (operator 2026-11-03) + +1. Number every parallel list in operator-facing chat, reports, and + leftover lists. A bare stack of services is a miss. +2. **horizon** is this laptop. **surmount-1** is the mail host and the + allowed remote Nix builder. Do not say "host-local" as a hostname. +3. The private overlay is a directory. On horizon it is + `~/.local/share/surmount/host-local/` (`default.nix`). `just deploy` + passes it as `--host-local` with target `root@surmount-1`. On + surmount-1 the copy is `/root/surmount-server/host-local/`. Name the + machine and the path in the same sentence. The flag name stays. + +Dual-pin [../AGENTS.md](../AGENTS.md) section *Number parallel items, +and name the machine*. + --- ## 1. What this project is @@ -124,7 +142,7 @@ Ladder detail: [fix-and-fixos.md](fix-and-fixos.md). | **Test and deploy procedure** | 2026-10-12: the update, audit, test, and switch command block is [TEST-AND-DEPLOY.md](TEST-AND-DEPLOY.md) section *Update, audit, test, and switch*. `nix flake update`, then `cargo update`, then `just audit-remote`, `just check-remote`, `just check-heavy`, `just deploy-dry-run`, `just btop`, `just deploy`. No `just` recipe for the first two. 2026-10-05: `just publish` is the static-site command. `just deploy` is the host switch. `just deploy-dry-run` is the agent dry-run. `just deploy-host -- ...` stays the explicit equivalent with no default target. 2026-09-20: [TEST-AND-DEPLOY.md](TEST-AND-DEPLOY.md). Agent runs named eval, `just check-remote`, and the dry-run. Operator does not run those to find errors. Operator runs the real switch with Eternal Terminal open. Dual-pin `AGENTS.md` Deploy dry-run is agent work. | | **GitHub tracking and git-flow (operator 2026-09-08)** | Same-turn GitHub issue on plan Approve (plan text, no secrets). Same-turn GitHub issue on operator bug reports, with screenshots attached. Agents never GPG-sign and never push. After the operator signs and pushes the feature branch, the agent opens the PR describing the work. Collaborating git uses git-flow `feature/` and `bugfix/` branches, not a pile on `main`. Dual-pin `AGENTS.md` and host `~/.grok/AGENTS.md`. | | **Leftovers: complete sentences, none already done** | Operator 2026-08-28: every numbered leftover is a complete sentence. Do not re-list work the screenshot or live host already shows is done. Dual-pin host `~/.grok/AGENTS.md`. | -| **Prove the operator gate** | Operator 2026-09-02: do not list a step as operator residual without evidence the agent cannot do it (secrets, standing forbid of the real `just deploy` switch, hypervisor, Namecheap hosted DNSSEC Status click). Agents may run one `just check-remote` at a time. The real switch `just deploy` is operator-owned. Agents must not run `just deploy`. `just deploy-host` without `--dry-run` is that same switch and has no default target. Host-local enable is agent work. Using the TUI is the product, not leftover. Operator 2026-09-07: if this laptop already has Namecheap env files, `just laptop-renew-cert` is agent work. Dual-pin `AGENTS.md`. | +| **Prove the operator gate** | Operator 2026-09-02: do not list a step as operator residual without evidence the agent cannot do it (secrets, standing forbid of the real `just deploy` switch, hypervisor, Namecheap hosted DNSSEC Status click). Agents may run one `just check-remote` at a time. The real switch `just deploy` is operator-owned. Agents must not run `just deploy`. `just deploy-host` without `--dry-run` is that same switch and has no default target. Host-local enable is agent work. Using the TUI is the product, not leftover. Operator 2026-09-07: if horizon already has Namecheap env files, `just laptop-renew-cert` is agent work for a DNS-01 issue from horizon. That recipe is not required for the certificate to keep renewing. surmount-1 renews the Let's Encrypt production certificate with HTTP-01. horizon does not have to stay online for that renewal. Dual-pin `AGENTS.md`. | | **Operator-facing chat is CATE** | Operator 2026-09-07: Concise American Technical English ([0005_CATE.md](https://github.com/SurmountSystems/specs/blob/main/0005_CATE.md), accessed: 2026-09-07). Do **not** say **production leaf**. Say the Let's Encrypt certificate this host presents, and the names on that certificate. Dual-pin `AGENTS.md`. | | **Eval then deploy dry-run before asking to switch** | Operator 2026-09-02: before any sentence that asks the operator to deploy or switch, agents must have run (a) local Nix eval that does **not** rustc on this laptop (`tests/module-eval.nix` / named flake eval) and (b) `just deploy-dry-run` (explicit equivalent `just deploy-host -- --dry-run`), and must report those results. Narrows 2026-08-25: dry-run is required agent work; the real switch (`just deploy`) stays operator-owned unless they override. `just deploy-host` stays the explicit equivalent with no default target. Laptop cargo and `BUILD_LOCAL` stay forbidden. Dual-pin `AGENTS.md`. | | **"Always remember" = dual pin** | Operator 2026-08-27: when they say **always remember**, write it in **AGENTS.md and this table in the same turn**. Chat-only does not survive compaction. One file does not survive attention dilution. | @@ -143,18 +161,18 @@ Ladder detail: [fix-and-fixos.md](fix-and-fixos.md). --- -## 3. Versions (last audit 2026-07-31; FOD and toolchain rows re-checked 2026-10-17) +## 3. Versions (last audit 2026-07-31; FOD and toolchain rows re-checked 2026-10-17; Stalwart family re-checked 2026-11-03) Source of truth for FODs: `nix/packages/*.nix`. Audit: [research/version-audit.md](research/version-audit.md). | Pin | Tree (as of audit) | Notes | |-----|--------------------|-------| -| Stalwart server | **0.16.26** binary FOD | Latest GitHub tag on 2026-10-17 (`v0.16.26`, 2026-10-11). A 0.16.x upgrade replaces the binary. Not nixpkgs stalwart | -| stalwart-cli | **1.0.13** | Latest tag on 2026-10-17 (`v1.0.13`, 2026-09-28). Still no Maildir import (that is Vandelay) | -| vandelay | **1.0.12** | Latest tag on 2026-10-17 (`v1.0.12`, 2026-10-11). Official 0.16 Maildir++ / JMAP import | -| Stalwart WebUI | **1.0.11** | Latest tag on 2026-10-17 (`v1.0.11`, 2026-09-15) | -| spam-filter rules | **3.0.2** | Latest tag on 2026-10-17 (`v3.0.2`, 2026-09-22) | +| Stalwart server | **1.0.2** binary FOD | Latest GitHub tag on 2026-11-03 (`v1.0.2`, published 2026-11-02T15:42:44Z). Not a prerelease. Not nixpkgs stalwart | +| stalwart-cli | **1.0.14** | Latest tag on 2026-11-03 (`v1.0.14`, published 2026-10-25T18:15:42Z). Still no Maildir import (that is Vandelay) | +| vandelay | **1.0.14** | Latest tag on 2026-11-03 (`v1.0.14`, published 2026-11-01T16:43:01Z). Official Maildir++ / JMAP import | +| Stalwart WebUI | **2.0.4** | Latest tag on 2026-11-03 (`v2.0.4`, published 2026-11-02T11:19:17Z) | +| spam-filter rules | **3.0.4** | Latest tag on 2026-11-03 (`v3.0.4`, published 2026-11-01T15:50:40Z). Release asset is `spam-filter-rules.json.gz` only. v3.0.2 was the last tag that also published `spam-filter.toml` | | Arti (HS package) | Surmount-owned source **2.7.0** (`arti-onion-service.nix`, GitLab `arti-v2.7.0`) + `onion-service-service` | Matches nixpkgs-rust `pkgs.arti.version` **2.7.0** (lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`). Not stock nixpkgs 1.4.2 lag. rustc via `nixpkgs-rust` is **1.99.1**. Prior lock `c46e6c9a` was rustc 1.99.0. Live Tor verify still residual | | management-ui rustc (crane) | **1.99.1** from `nixpkgs-rust` `pkgs.rustc` (`nix/rust-toolchain.nix`, lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`) | Host channel nixos-26.05 `pkgs.rustc` is still **1.95.0**. grok-oss uses the same `surmount-rust-1.99.1` toolchain, not host cargo 1.95.0. Not `rustPackages_1_95`. Leptos MSRV is >=1.88 | | Host nixpkgs channel | **nixos-26.05** lock | Engine is **not** from channel; channel lag is separate | @@ -207,7 +225,7 @@ Depth: [DATASTORES.md](DATASTORES.md), | Item | Direction | |------|-----------| | Clearnet HTTPS | **Axum-first** edge (management-ui rustls on public **:80/:443**). Stalwart is **not** product clearnet HTTPS (P1). Free engine first-boot :443 via `nix/stalwart/free-public-443-for-axum-edge.ndjson`. nginx delete path. | -| :80 | Redirect/upgrade to :443. **Production (operator 2026-08-02):** port 80 is **free** for product **redirect-only** bind. **ACME HTTP-01 on product :80 parked** (Q-EDGE; free :80 does not invent ACME). Dual-run nginx may still use :80 for ACME while escape is on. | +| :80 | Redirect/upgrade to :443. **Production (operator 2026-08-02):** port 80 is **free** for product **redirect-only** bind. HTTP-01 on surmount-1 is the Let's Encrypt production renew path: the product port 80 listener serves the challenge, and systemd timer `surmount-cert-renew` refreshes the PEM paths that host already presents. horizon does not have to stay online for that renewal. Namecheap API credentials stay off surmount-1. Public module `acme.enable` stays false. The private overlay on horizon turns host renewal on for surmount-1. Dual-run nginx may still use :80 for ACME while escape is on. | | TLS | No SSLv3/1.0/1.1; **prefer TLS 1.3**; hybrid **PQ KEX** on rustls/aws-lc-rs (e.g. X25519MLKEM768) **first-class** (`prefer-post-quantum` + hermetic unit proof of default provider groups; **not** live host hybrid negotiation until `nix run .#surmount-tls-hybrid` after B1) | | Deploy automation | Operator driver `nix run .#surmount-deploy-host` / `just deploy-host` (no default target). 2026-10-05 short names: `just publish` publishes static sites; `just deploy` is the real switch; `just deploy-dry-run` is the agent dry-run. Host-local contract [deploy-host-local.md](deploy-host-local.md); hermetic crate tests in `checks.*.ci` (not a remote switch). Security ladder B0-B7 host-gated. | | PQConnect | **Separate** path-layer PQ between supporting peers; evaluate/plan; not a substitute for TLS hybrid alone | @@ -412,7 +430,7 @@ Honest gaps. Do not claim these are shipping code: | Gap | Reality today | |-----|----------------| | **Arti live Tor publish** | Module generates **management-publish** `arti.toml` (onion + rproxy, no keys in tree); Surmount `artiOnionService` is owned **2.7.0** source + `onion-service-service` (not nixpkgs 1.4.2 lag); stock path fail-closed. **Local cleartext full API for https+Arti auto-path shipped** (loopback `SURMOUNT_LOCAL_CLEARTEXT_LISTEN` + Arti backend target; lean onion backend is cleartext HTTP or UDS). **Onion status product-real (2026-08-10):** Arti enable derives hostname file + HS state dir env; structured `configured` / `hostname_missing` / `not_provisioned` on SSR + `GET /api/v1/system`; residual names `surmount.artiHiddenService` (not local demo). Lab `SURMOUNT_ONION_URL` only. **Onion-Location + Alt-Svc shipped (2026-08-17; every public Host 2026-08-20)** on mapped HTTPS 2xx/3xx (apex, www, services, extra static Hosts, MTA-STS; one v3 per public HTTP Host; Onion-Location is that Host's onion root plus path; process-start load; no hot-reload; optional `SURMOUNT_ONION_LOCATION_ENABLED` / `SURMOUNT_ONION_ALT_SVC_ENABLED` / `SURMOUNT_ONION_*_DISABLED_HOSTS` / `SURMOUNT_ONION_MAP_FILE`; dump on `GET /api/v1/system` `onion_discovery`). **Live host (2026-08-17, private host-local):** unit `surmount-arti-hidden-service` active; durable HS dir (not `/run`); hostname file present (v3 onion; do not paste the address in this public tree); headers proven on HTTPS 307/200. Host-local `HOME=/var/lib/surmount/arti`; `surmount-ui` in group `surmount-arti`; keystore 0700. Still residual: **live Tor Browser / onion-fetch verify**, operator offline backup of HS identity, public-module HOME/`port_info`. Do **not** claim onion published from unit active or header presence alone; local temp-key green != operator backup. Do **not** claim B3 fully closed. Do not invent Q-ARTI-2/3 answers | -| **Axum HTTPS public cutover / nginx delete** | In-process rustls TLS 1.3 acceptor wired (host PEMs, fail-closed key mode). **`surmount.web.enable` default false** (nginx not product edge). Dual-run escape: `web.enable = true`. Module/nginx code still in tree. **:80 redirect-only bind wired** when `redirectHttpToHttps` + listen (dual-run mutex at eval; no cleartext API on :80). **Operator 2026-08-02:** production port 80 is **free** for that redirect-only path. **ACME HTTP-01 on product :80 parked** (Q-EDGE; do not invent ACME-on-product-:80). **In-process ACME scaffold (2026-08-10; A1/A2):** `instant-acme` + DNS-01 inside management-ui, **default off**; static PEMs still first-class; hermetic `mock` + **external-hook** DNS-01 challenge adapter (operator-owned executable; not a commercial DNS brand); early-renew default 30 days. **Live host (2026-08-11 free-443; 2026-08-12 mta-sts + policy):** public :443 is **surmount-manage** not Stalwart; `https://services.surmount.systems/health` **200** without `-k`. Cert is **Let's Encrypt production** (issuer YE1; browser-trusted). Intended production leaf is **20** certificate hostnames on one PEM (`with_single_cert`). Live leaf as of **2026-09-07** (CT, YE2) is still **18** names: six extra static zones apex+www (`baxterartworks.com`, `btcfur.com`, `exophiles.org`, `iantuckerstudios.com`, `nostrfurs.com`, `yiffa.app`) plus `surmount.systems`, `www.surmount.systems`, `mail.surmount.systems`, `services.surmount.systems`, `mta-sts.surmount.systems`, plus `mail.cryptoquick.com`. Missing: `cryptoquick.com` and `www.cryptoquick.com`. That hostname mismatch is why cryptoquick HTTPS fails verify. Extra-vhost HTTPS for those six is **live**. Validating A for cryptoquick apex/www succeeds (AD true). Leftover parent DS key tag 2368 is gone. The old wait-for-SERVFAIL gate is closed as a live A-lookup gate. SHA-1 parent DS digest type 1 remains standing DNSSEC quality debt in operator-facts Monday leftover; it is not the HTTPS cause. After the tree lists the two missing names, laptop `--issue` is operator-gated (Namecheap custody). `--live` does **not** detect missing certificate hostnames. Esplora Hosts stay off this leaf. Mail **is** on the production leaf (IMAP/SMTPS 993/465 YE1). Issued by **laptop DNS-01** (Namecheap; ClientIp = laptop egress; SettleSeconds=120). Host ACME stays **off**. Durable PEMs under `/var/lib/surmount/secrets/tls/`. Named renew: `just laptop-renew-cert` (`--directory` required; do **not** re-issue just to add a name already on the leaf). Apex/www public page is packaged **SurmountSystems/site** (live 2026-08-19 tip `1c84696`; not UNDER CONSTRUCTION). Private host-local `mtaStsMode=testing`; public `https://mta-sts./.well-known/mta-sts.txt` **200** `mode: testing` (stay testing while DNS MX is eforward; do not enforce). Reports: `../.agents/reports/impl-live-le-production-laptop.md`, `../.agents/reports/impl-earn-trust-wave.md`. **Post-reboot proof PASS (2026-08-13).** Later 2026-08-18 hard cycle: health/apex **200**, IMAP Let's Encrypt including **mail**. Agents never reboot. Residual: firewall across switch, MemoryDenyWriteExecute host proof (`just e2e-host`). Day-one order: [OPS.md](OPS.md). Do **not** claim live onion or "nginx deleted from repo" without further host proof. Do **not** invent MX / DMARC / VW. Agent language: say **certificate hostnames**, not bare **SAN** (collides with storage SAN). Dual-run unused checklist: [OPS.md](OPS.md); **do not delete `modules/web.nix` without operator OK** | +| **Axum HTTPS public cutover / nginx delete** | In-process rustls TLS 1.3 acceptor wired (host PEMs, fail-closed key mode). **`surmount.web.enable` default false** (nginx not product edge). Dual-run escape: `web.enable = true`. Module/nginx code still in tree. **:80 redirect-only bind wired** when `redirectHttpToHttps` + listen (dual-run mutex at eval; no cleartext API on :80). **Operator 2026-08-02:** production port 80 is **free** for that redirect-only path. **HTTP-01 on surmount-1** renews the Let's Encrypt production certificate that host presents (timer `surmount-cert-renew`). horizon does not have to stay online for that renewal. Public module `acme.enable` stays false. **In-process ACME scaffold (2026-08-10; A1/A2):** `instant-acme` + DNS-01 inside management-ui, **default off**; static PEMs still first-class; hermetic `mock` + **external-hook** DNS-01 challenge adapter (operator-owned executable; not a commercial DNS brand); early-renew default 30 days. **Live host (2026-08-11 free-443; 2026-08-12 mta-sts + policy):** public :443 is **surmount-manage** not Stalwart; `https://services.surmount.systems/health` **200** without `-k`. Cert is **Let's Encrypt production** (issuer YE1; browser-trusted). Intended production leaf is **20** certificate hostnames on one PEM (`with_single_cert`). Live leaf as of **2026-09-07** (CT, YE2) is still **18** names: six extra static zones apex+www (`baxterartworks.com`, `btcfur.com`, `exophiles.org`, `iantuckerstudios.com`, `nostrfurs.com`, `yiffa.app`) plus `surmount.systems`, `www.surmount.systems`, `mail.surmount.systems`, `services.surmount.systems`, `mta-sts.surmount.systems`, plus `mail.cryptoquick.com`. Missing: `cryptoquick.com` and `www.cryptoquick.com`. That hostname mismatch is why cryptoquick HTTPS fails verify. Extra-vhost HTTPS for those six is **live**. Validating A for cryptoquick apex/www succeeds (AD true). Leftover parent DS key tag 2368 is gone. The old wait-for-SERVFAIL gate is closed as a live A-lookup gate. SHA-1 parent DS digest type 1 remains standing DNSSEC quality debt in operator-facts Monday leftover; it is not the HTTPS cause. After the tree lists the two missing names, laptop `--issue` is operator-gated (Namecheap custody). `--live` does **not** detect missing certificate hostnames. Esplora Hosts stay off this leaf. Mail **is** on the production leaf (IMAP/SMTPS 993/465 YE1). Issued by **laptop DNS-01** (Namecheap; ClientIp = laptop egress; SettleSeconds=120). Host ACME stays **off**. Durable PEMs under `/var/lib/surmount/secrets/tls/`. Named renew: `just laptop-renew-cert` (`--directory` required; do **not** re-issue just to add a name already on the leaf). Apex/www public page is packaged **SurmountSystems/site** (live 2026-08-19 tip `1c84696`; not UNDER CONSTRUCTION). Private host-local `mtaStsMode=testing`; public `https://mta-sts./.well-known/mta-sts.txt` **200** `mode: testing` (stay testing while DNS MX is eforward; do not enforce). Reports: `../.agents/reports/impl-live-le-production-laptop.md`, `../.agents/reports/impl-earn-trust-wave.md`. **Post-reboot proof PASS (2026-08-13).** Later 2026-08-18 hard cycle: health/apex **200**, IMAP Let's Encrypt including **mail**. Agents never reboot. Residual: firewall across switch, MemoryDenyWriteExecute host proof (`just e2e-host`). Day-one order: [OPS.md](OPS.md). Do **not** claim live onion or "nginx deleted from repo" without further host proof. Do **not** invent MX / DMARC / VW. Agent language: say **certificate hostnames**, not bare **SAN** (collides with storage SAN). Dual-run unused checklist: [OPS.md](OPS.md); **do not delete `modules/web.nix` without operator OK** | | **Merciless ban + whitelist** | First path + helper scaffold: Rust ban decide + memory/file; optional kernel firewall sync via **socket-activated** `surmount-nft-ban-helper@` (Unix socket `SURMOUNT_BAN_NFT_HELPER_SOCK`; CAP_NET_ADMIN on helper unit only) or unsupported direct `nftExec`; UI keeps NNP and **no** CAP_NET_ADMIN (child setcap spawn is not the product path). DryRun does not mutate sets; Enforce apply-then-durable. Hermetic mock/tests under `just e2e`. **`remove_ban` / lab unban shipped** (helper `remove_ban` / CLI `remove-ban`; delete-element; absent element = ok). **Auth-failure BanCandidate + matrix shipped** (`decide_ban_signal` / `BanGuard::signal_unauthorized` + request-context hook; whitelist immune; Off/DryRun/Enforce). Session exchange fail and bad presented NIP-98 may signal once; missing cookie does not; 404/501 still do not auto-ban. Table: [SECURITY.md](SECURITY.md). **Not** live host bans / full Q-ACL-1..6 surface answers. Live drop: `just e2e-host` + sets `surmount-ban4`/`surmount-ban6`. fail2ban sshd still transitional sketch | | **Leptos SSR admin/webmail** | **Multi-page console + UI depth (2026-08-01; directory live 2026-08-07):** SSR `/`, `/domains`, `/accounts`, `/system`, `/mail`, `/login` (DOGE only; no skeleton). Inventory cards, onion chip, system definition table, mail probe card. Accounts honest empty by default; `/domains` config union (primary names including derived www and mta-sts when first-class, `static_vhosts` keys, extra mail hostnames; `source` stays `config`; not Namecheap API; not Stalwart directory). **Directory trait + hermetic mock + live Stalwart client shipped** (`AppState.directory`; `unavailable` default; labeled `mock` never default-on; `stalwart` = management JMAP `x:Account/query`+`get` via Bearer token, explicit `SURMOUNT_DIRECTORY=stalwart` + host token only; list fail-closed empty). **JMAP thin 501 boundary locked (2026-08-07):** `POST /api/v1/jmap` honest 501 (`jmap_proxy_not_implemented`); mail SSR residual copy; no `/webmail` product route; hermetic anchors. Directory research: [research/stalwart-directory-api.md](research/stalwart-directory-api.md). **Account create/update API mutations shipped (2026-08-07)** (`POST/PATCH /api/v1/accounts`; auth gate + CSRF; mock + `x:Account/set` wire-mock). **Create mailbox form shipped (2026-08-14)** on `/mail` (Administrator; optional npub; User vs Administrator; map at `/var/lib/surmount/console/accounts.json` or `SURMOUNT_CONSOLE_ACCOUNTS`). **Attach npub shipped (2026-08-20):** Administrator `/mail` Grant console login pastes bech32 `npub1...`, session-bound CSRF, binds an existing mailbox so that person can log into the services portal (AuthMode nostr). Garbage/nsec refused. Directory listing not required. `/accounts` still has no create form. **Structured request logging shipped.** Still residual: hydrate islands only if clear SSR gap (parked), full Q-AUTH-1, JMAP proxy + webmail **beyond** 501, **MX still parked**, **host cutover**. Living crane rustc **1.99.0** from `nixpkgs-rust` (not host `rustPackages_1_95`); Leptos MSRV floor is >=1.88 | | **Nostr auth end-to-end** | **Foundation shipped (2026-08-01; polish 2026-08-02):** rust-nostr NIP-98 + HMAC session cookie scaffold; `SURMOUNT_AUTH_MODE` off/nostr; env allowlist + optional allowlist file (env wins; empty fail-closed); challenge/session/me/logout + login page. **Not JS NDK.** UI honesty (auth-mode banners, `/login` when nostr). Hermetic e2e anchors list off/gate/NIP-98 session + surface audit + ban matrix (and onion unset residual). CSRF on account mutations + structured request logging shipped (2026-08-07). **Account mutation e2e hermetic anchors shipped (2026-08-07):** auth-off fail-closed, cookie CSRF (incl. PATCH), lab escape mock, unavailable 503. **Live B4 (2026-08-12):** public services console is Nostr-gated (anon `/` login, `/api/v1/domains` 401, `/health` 200); apex/www packaged **SurmountSystems/site** (2026-08-19 tip `1c84696`). Report: `../.agents/reports/impl-auth-live-b4-switch.md`. Q-AUTH-1 residual: key-loss, durable session store, first-operator bootstrap UX (do not invent). nsec never on server. | @@ -431,8 +449,9 @@ Tree that **does** exist: flake, Surmount Stalwart FODs + service module, transitional nginx web module (default off; dual-run escape), management-ui Axum edge (rustls HTTPS acceptor + TLS path config, rate limit, ban decide layer default off, **local cleartext full API** for https+Arti, redirect -helpers + optional redirect-only :80 bind; ACME-on-:80 parked; in-process -ACME DNS-01 scaffold default-off (`instant-acme`, external-hook + early-renew); +helpers + optional redirect-only :80 bind; HTTP-01 renew on surmount-1 +(public `acme.enable` stays false; horizon does not have to stay online); +in-process ACME DNS-01 scaffold default-off (`instant-acme`, external-hook + early-renew); **Leptos SSR multi-page management console** (`/`, `/domains`, `/accounts`, `/system`, `/mail`); **nft ban helper scaffold** UDS + socket-activated oneshot with diff --git a/docs/DATASTORES.md b/docs/DATASTORES.md index 00462aa..1596bb2 100644 --- a/docs/DATASTORES.md +++ b/docs/DATASTORES.md @@ -20,7 +20,7 @@ leftover RSA files on a live host are not deleted by hostKeys). Prior > Historical 0.11.8 only: > [research/stalwart-stores-evidence-2026-07-30.md](research/stalwart-stores-evidence-2026-07-30.md). -> **Engine version:** Surmount pins Stalwart **0.16.15** via release-binary FODs +> **Engine version:** Surmount pins Stalwart **1.0.2** via release-binary FODs > (`nix/packages/stalwart-mail.nix`, `modules/stalwart-service.nix`). Host > channel is **nixos-26.05**; engine is not the channel package. Early scaffold > briefly used channel **0.11.8** on host **25.05** (historical only). @@ -426,7 +426,7 @@ Current engine store evidence: **Hermetic FODs (not auto-applied as engine config):** -- Spam-filter **3.0.0** and WebUI zip are installed under +- Spam-filter **3.0.4** (`spam-filter-rules.json.gz` only) and WebUI zip are installed under `/etc/surmount/stalwart/` via `environment.etc` in `modules/mail.nix`. - They are **not** the message DB and are **not** wired through TOML `spam-filter.resource` settings. Operator points SpamSettings / resource URL @@ -452,9 +452,10 @@ section 2. ### 5.4 Spam-filter FOD is not the message store -`nix/packages/stalwart-spam-filter.nix` pins spam-filter **3.0.0** -(`spam-filter.toml` + `spam-filter-rules.json.gz` FODs). `modules/mail.nix` -installs them under `/etc/surmount/stalwart/` for operators. That material is +`nix/packages/stalwart-spam-filter.nix` pins spam-filter **3.0.4** +(`spam-filter-rules.json.gz` only; v3.0.3 dropped `spam-filter.toml`). +`modules/mail.nix` installs that archive under `/etc/surmount/stalwart/` +for operators. That material is **anti-spam configuration**, completely separate from RocksDB message data. Do not back it up as if it were mail; it is in the Nix store via the system closure. Engine consumption is Day-2 apply/WebUI, not automatic first-boot @@ -554,7 +555,7 @@ wired in modules yet. | Stalwart data+blob+FTS+lookup | RocksDB (provisional) | `/var/lib/stalwart-mail/db` | WAL + process; stop-service backup preferred | restic `mailDataDir`; test restore | Mail passwords inside engine DB | Stalwart | | Stalwart internal directory | Same RocksDB | inside `db` | Same | Same | Credential hashes | Stalwart | | Stalwart webadmin cache | Files | `/var/cache/stalwart-mail` | Ephemeral cache | Optional / skip | No | Stalwart package | -| Spam-filter rules | TOML + rules FODs in Nix store | `/etc/surmount/stalwart/` (3.0.0); apply/WebUI to engine | Immutable in closure | Via nix / flake | No | `mail.nix` + spam-filter package | +| Spam-filter rules | rules JSON FOD in Nix store | `/etc/surmount/stalwart/spam-filter-rules.json.gz` (3.0.4); apply/WebUI to engine | Immutable in closure | Via nix / flake | No | `mail.nix` + spam-filter package | | MailPlus import staging | Maildir files | `/var/lib/surmount/import/...` | Files | Optional until import done | May contain mail | Operator | | Surmount state root | dir | `/var/lib/surmount` | Files | restic `stateDir` | Future sessions etc. | Surmount | | Management UI durable state | Optional ban/whitelist-last-used JSON; future sessions | default `stateDir/ui/ban-state.json` when accessControl.enable; no secrets | Files | With `stateDir` | Session secrets via sops (future); ban state is IPs only | Surmount management-ui | @@ -650,7 +651,7 @@ A backup never restored is theater ([SECURITY.md](SECURITY.md)). |----------|------| | MailPlus Maildir export | **Import source only** | | MBOX/PST conversions | Import source | -| spam-filter.toml FOD | Config in Nix store | +| spam-filter-rules.json.gz FOD | Config in Nix store | | git history | Not a mail backup | | FTS alone | Not a restore path for bodies | diff --git a/docs/EDGE_AND_TLS.md b/docs/EDGE_AND_TLS.md index 1b5bfb7..fcc875b 100644 --- a/docs/EDGE_AND_TLS.md +++ b/docs/EDGE_AND_TLS.md @@ -4,7 +4,8 @@ How HTTPS reaches the management UI and (optionally) Stalwart HTTP. Mail protocol ports are **not** edge-proxied; they terminate on Stalwart. See [STACK.md](STACK.md) for the full path map. -**Last updated:** 2026-10-24 (Host `status.surmount.systems` is the public +**Last updated:** 2026-10-28 (Up on this page means that service's socket accepted a connection.) +Prior 2026-10-24 (Host `status.surmount.systems` is the public systems dashboard on the product Axum HTTPS edge, not nginx and not the operator console. Names and public hosts are https links. Status text comes from systemd ActiveState / HTTP samples in ordinary English. @@ -39,9 +40,12 @@ leftover Namecheap clicks. Do not MX-flip Baxter.) Prior 2026-09-03 (HTTP/3 NIP- hostnames: six extra static zones apex+www plus surmount apex, www, mail, services, mta-sts, plus **mail.cryptoquick.com** for IMAP/SMTP. Extra-vhost HTTPS live for those six. `cryptoquick.com` apex/www stayed -off that certificate that day. Prior 2026-08-18: laptop Let's Encrypt renew is +off that certificate that day. Prior 2026-08-18: laptop Let's Encrypt renew was `just laptop-renew-cert -- --check|--live --directory production`. -`--live` issues only when due. Host ACME stays off. Stalwart 0.16.15 +`--live` issued only when due. That horizon command is not required for +the certificate to keep renewing. surmount-1 renews the Let's Encrypt +production certificate it presents, using HTTP-01 on surmount-1. +horizon does not have to stay online for that renewal. Stalwart 0.16.15 query resolves certificate hostnames plus id.) **Operator direction:** [operator-direction.md](operator-direction.md) @@ -64,7 +68,7 @@ query resolves certificate hostnames plus id.) | Reverse proxy / routing | To Axum/Leptos UI; optional Stalwart HTTP path | | Local IPC | Prefer **Unix domain sockets** to backends, not TCP localhost | | Certificates | Automated public CA is common today; **not** locked to ACME-only (TLS research) | -| HTTP :80 | Automatically and gracefully upgrade/redirect to :443. Production: **port 80 free** for product redirect-only bind (operator 2026-08-02). ACME HTTP-01 on product :80 **parked** (Q-EDGE); dual-run nginx may still use :80 for ACME | +| HTTP :80 | Automatically and gracefully upgrade/redirect to :443. Production: **port 80 free** for product redirect-only bind (operator 2026-08-02). When host renewal is on, HTTP-01 on surmount-1 serves `/.well-known/acme-challenge/` on that listener and refreshes the Let's Encrypt production certificate surmount-1 already presents. horizon does not have to stay online for that renewal. Dual-run nginx may still use :80 for ACME while escape is on. | | TLS versions | No SSLv3, no TLS 1.0/1.1; **prefer TLS 1.3** (1.2 only if measured client need) | | PQ where supported | Enable hybrid PQ KEX (e.g. X25519MLKEM768) when rustls/aws-lc-rs path allows; see PQC research | | Rate limiting | Request flood protection at edge and/or app (tower-governor or equivalent) | @@ -179,7 +183,7 @@ docs or module-eval alone. - `services.nginx` + `security.acme` only when `surmount.web.enable = true` - Vhosts for `services.surmount.systems`, `mail.surmount.systems`, apex, www -- Proxy `/` -> management UI; `/stalwart-admin/` -> Stalwart HTTP (bootstrap) +- Proxy `/` -> management UI. No public `/stalwart-admin/` path (removed; SSH tunnel to loopback Stalwart HTTP is the bootstrap). - Recommended TLS/proxy/gzip settings enabled - `clientMaxBodySize` default 25m (mail bodies go through Stalwart, not the edge) - Backends today are **TCP loopback** (scaffold). Target local hops are **UDS**. @@ -265,9 +269,11 @@ nginx features. Module file remains until operators no longer need dual-run. - HTTP->HTTPS redirect helpers + host allowlist; optional **redirect-only** plain HTTP listener when `redirectHttpToHttps` + `httpRedirectListen` (default `0.0.0.0:80`). No cleartext API on that port. Eval mutex vs - `web.enable` (nginx dual-run owns :80 ACME/redirect). **ACME HTTP-01 on - product :80 is parked** (Q-EDGE; free :80 is redirect-only, not an invent of - ACME-on-product-:80). See RESIDUAL.md. + `web.enable` (nginx dual-run owns :80 ACME/redirect while that escape is on). + When host renewal is on, HTTP-01 on surmount-1 serves the challenge on + that product port 80 listener and refreshes the Let's Encrypt production + certificate surmount-1 already presents. horizon does not have to stay + online for that renewal. Public module `acme.enable` stays false. - **Apex / www public main site vs services console (2026-08-12; document root 2026-08-14; SurmountSystems/site 2026-08-18):** allowlisted Host `primaryDomain` or `www.` is the **public** main site. @@ -308,7 +314,9 @@ nginx features. Module file remains until operators no longer need dual-run. `splora.{primary}` testnet paths, MTA-STS policy). Status text is ordinary English from systemd ActiveState / SubState and optional HTTP samples (running, failed, inactive, listening, HTTP 502), not a - constant ok and not RAM/disk/IP. Host uptime is `/proc/uptime` formatted + constant ok and not RAM/disk/IP. Up on this page means that service's socket accepted a connection. + A Splora indexer whose unit is active and whose proxy socket is absent is inactive, not running. + Host uptime is `/proc/uptime` formatted as seconds, minutes (at most 60 minutes), N.N hours (through 24 hours), or N.N days. Not the operator console. `/health` and `/api/health` stay edge probes. Other `/api/*` is 404. Laptop DNS-01 intended leaf @@ -436,7 +444,7 @@ proxied (fail-closed if configured). whitelist). Then add FQDNs to private `acme_domains` and `just laptop-renew-cert -- --issue --directory production --host-profile ~/.local/share/surmount/host-profile.toml --install --restart-ui --target USER@HOST`. `--live` does **not** detect missing certificate hostnames (expiry - only). Do not start host in-process ACME. + only). That horizon issue command is not what keeps the certificate renewing. Extra-zone DNS-01 (cryptoquick.com, yiffa.app, and other Namecheap zones besides surmount.systems) cannot use the one-SLD Namecheap hook @@ -450,7 +458,9 @@ proxied (fail-closed if configured). `--issue --directory production` (Let's Encrypt production). Do **not** put unowned, archived, or still-Cloudflare zones on `--domains` until their nameservers are Namecheap. Extra certificate hostnames belong - on a leaf only after that NS cut. Host in-process ACME stays off. + on a leaf only after that NS cut. surmount-1 renews with HTTP-01. + horizon does not have to stay online for that renewal. Namecheap API + credentials stay off surmount-1. - **MTA-STS policy path:** `GET /.well-known/mta-sts.txt` when `mtaStsMode` is `testing` or `enforce` (default **off**). Served only for Host `mta-sts.` on the allowlist (HTTP/2 uses Host or URI @@ -464,7 +474,11 @@ proxied (fail-closed if configured). optional DNS-01 path inside management-ui via **`instant-acme` 0.8** (rustls 0.23 / aws-lc-rs). **Default off** (`surmount.managementUi.acme.enable = false` / no `SURMOUNT_ACME_ENABLE`). Static host PEM load remains first-class - (not ACME-only forever). + (not ACME-only forever). Public module default stays false. On surmount-1, + the private overlay on horizon turns host renewal on. HTTP-01 refreshes + the Let's Encrypt production PEMs that host presents. horizon does not + have to stay online for that renewal. Namecheap API credentials stay off + surmount-1. - **Reuse / early renew:** reuse PEMs when leaf is not past notAfter, not not-yet-valid, certificate hostnames cover configured domains, **and** remaining lifetime is at least `SURMOUNT_ACME_RENEW_DAYS_BEFORE_EXPIRY` days (scaffold default @@ -495,8 +509,11 @@ proxied (fail-closed if configured). Hot-reload residual: restart after renew. CI never requires live Let's Encrypt. Env: `SURMOUNT_ACME_*`. Ops sketch: [OPS.md](OPS.md). - **Production assumption (operator 2026-08-02):** TCP **port 80 is free** on - the NixOS box so the product redirect-only listener can bind. That free - :80 is for **redirect/upgrade only**, not an invent of ACME-on-product-:80. + the NixOS box so the product redirect-only listener can bind. When host + renewal is on, that same listener on surmount-1 also serves HTTP-01 + `/.well-known/acme-challenge/` and the timer refreshes the Let's Encrypt + production PEMs that host presents. horizon does not have to stay online + for that renewal. Public module `acme.enable` stays false. Day-one ops: [OPS.md](OPS.md). - Fixed-window rate limit; when peer is loopback, trust **X-Real-IP only** (useful behind dual-run nginx). X-Forwarded-For is ignored for rate-limit @@ -739,7 +756,7 @@ Stalwart HTTP without evidence. | **Cons** | We own multi-vhost, renewals, edge cases, security updates of *our* edge binary; more code than a packaged proxy | | **NixOS** | Package with crane like management-ui; simple systemd unit on :80/:443 | | **UDS** | Natural (hyper/axum backend client to UDS) | -| **Certs** | Static host PEMs always supported. **In-process** path uses **instant-acme** (DNS-01, default off). Dual-run may still use host `security.acme`. **Not locked** ACME-only or to a single CA. | +| **Certs** | Static host PEMs always supported. **In-process** path uses **instant-acme**. Public `acme.enable` stays false. On surmount-1, HTTP-01 renews the Let's Encrypt production certificate that host presents. horizon does not have to stay online for that renewal. DNS-01 remains the external-hook path and stays off that host. Dual-run may still use host `security.acme`. **Not locked** ACME-only or to a single CA. | | **Verdict** | **Default path** per operator follow-up 2026-07-30 | ### Rama / Pingora / other Rust proxy frameworks @@ -768,7 +785,7 @@ Stalwart HTTP without evidence. |---------|--------| | **Now (tree default)** | **Axum-first** management-ui rustls (`listenMode=https` + host PEMs); `surmount.web.enable` default **false** | | **Dual-run escape** | `surmount.web.enable = true` + UI `listenMode=http` loopback; nginx + `security.acme` (**transitional-to-delete**) | -| **Production :80** | **Free** for product **redirect-only** bind (operator 2026-08-02). Not ACME-on-product-:80 (parked; Q-EDGE) | +| **Production :80** | **Free** for product redirect-only bind (operator 2026-08-02). HTTP-01 on surmount-1 uses that listener for Let's Encrypt production renewal. horizon does not have to stay online for that renewal. | | **Host residual** | Public :443 + MemoryDenyWriteExecute (no writable+executable memory) + cert path (Q-EDGE-1 / Q-CA-*); see RESIDUAL.md host tracks. Not claimed done from eval or local e2e alone | | **Local backends** | Move UI (and proxied Stalwart HTTP when possible) toward **Unix domain sockets** | | **Not target** | Caddy/Sozu/nginx as product identity | @@ -794,8 +811,8 @@ proof, not flipping the module default again. Module asserts against dual-run + public UI https (:443 or non-loopback). 6. When dual-run is unused: remove nginx module path; update STACK, OPS, hygiene, architecture-review. -7. Lock down or remove `/stalwart-admin/` public path once Rust admin covers - bootstrap needs. +7. The public `/stalwart-admin/` path was removed from `modules/web.nix`. + Bootstrap is an SSH tunnel to the loopback Stalwart HTTP listener. ### Host end-to-end recipes (operator) @@ -823,7 +840,7 @@ nix run .#e2e-host | Track | What host mode checks | Notes | |-------|----------------------|--------| -| **1. HTTPS + hardening** | **`BASE_URL` required** (FAIL if unset); health 200; UI unit active; `MemoryDenyWriteExecute=yes` (no writable+executable memory); TLS check (handshake; **expired cert fails**); nginx inactive; UI ambient **and** bounding set have no CAP_NET_ADMIN | Operator PEMs on host; key not group/world readable; external PEMs lean until Q-EDGE/Q-CA answered; ACME-on-product-:80 parked | +| **1. HTTPS + hardening** | **`BASE_URL` required** (FAIL if unset); health 200; UI unit active; `MemoryDenyWriteExecute=yes` (no writable+executable memory); TLS check (handshake; **expired cert fails**); nginx inactive; UI ambient **and** bounding set have no CAP_NET_ADMIN | Operator PEMs on host; key not group/world readable; external PEMs lean until Q-EDGE/Q-CA answered; HTTP-01 renew on surmount-1; horizon does not have to stay online | | **2. Arti** | `surmount-arti-hidden-service` (or override) active; `User=surmount-arti`; optional onion fetch via `SURMOUNT_E2E_ONION` + `TOR_SOCKS` | **Unit active != published.** HS keys under `onionServiceStateDir`, never in git. Cleartext local backend when UI is https-only. Do not invent Q-ARTI-2/3 | | **3. Ban / kernel firewall** | Helper socket + helper socket unit; set **preflight** (exists only, not drop); **`LAB_IP` required** and must appear in `surmount-ban4` (FAIL if unset/absent) unless `SKIP_BAN=1` | CAP_NET_ADMIN on **helper only**. Summary `ban_drop=UNPROVEN` (membership != traffic drop). Prove drop + cleanup out-of-band. Q-ACL-1..6 parked | @@ -862,7 +879,7 @@ Ids: **Q-CA-1**, **Q-CA-2** in [research/pqconnect-and-pqc.md](research/pqconnec | Rule | Lean | |------|------| -| :80 -> :443 | Graceful redirect/upgrade on free production :80 (redirect-only product path). ACME HTTP-01 may share :80 only on dual-run nginx or a future Q-EDGE answer; **not** invent ACME-on-product-:80 | +| :80 -> :443 | Graceful redirect/upgrade on free production :80 (redirect-only product path). HTTP-01 on surmount-1 also serves the challenge on that listener and renews the Let's Encrypt production certificate. horizon does not have to stay online for that renewal. | | Insecure protocols | Disabled: SSLv3, TLS 1.0, TLS 1.1 | | Prefer | TLS 1.3 | | PQ KEX on Axum/rustls | **First-class (D1):** **aws-lc-rs** + `prefer-post-quantum` (X25519MLKEM768 first in default provider). Hermetic unit tests pin group config. **Live host hybrid negotiation is residual** until measured post-B1 (do not claim from local e2e alone). Probe: `nix run .#surmount-tls-hybrid` / `just check-tls-hybrid` (requires `SURMOUNT_E2E_BASE_URL`; exit 2 BLOCKED if unset) | diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 451f459..5e15ced 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -152,14 +152,14 @@ the host allowlist. Q-AUTH-1 is unchanged. ## Import with Vandelay (Stalwart 0.16) -Live `stalwart-cli` **1.0.12** is schema-driven JMAP admin only (`get`, +Live `stalwart-cli` **1.0.14** is schema-driven JMAP admin only (`get`, `query`, `create`, `update`, `delete`, `describe`, `apply`, `snapshot`). It has **no** `import` subcommand. Do not run `stalwart-cli import messages`. The engine binary `stalwart -i` imports a **Stalwart store dump**, not Maildir. -Official 0.16 Maildir++ path is **Vandelay** (pin `nix/packages/vandelay.nix`, -currently 1.0.7): local tree -> SQLite archive -> JMAP export. +Official Maildir++ path is **Vandelay** (pin `nix/packages/vandelay.nix`, +currently 1.0.14): local tree -> SQLite archive -> JMAP export. See [Vandelay](https://github.com/stalwartlabs/vandelay) (accessed: 2026-08-13) and [Vandelay: the JMAP importer-exporter](https://stalw.art/blog/jmap-account-migration/) diff --git a/docs/OPS.md b/docs/OPS.md index 3ff2e65..6d2b22a 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -1687,7 +1687,7 @@ priority (-20), not `Nice` unset. No public log dump on the apex site. | sshd | journald | `LogLevel VERBOSE` (auth fail / disconnect; no passwords) | | fail2ban | journald | Transitional sshd jail; `backend = systemd` | | nft `surmount_guard` | kernel -> journald | `log prefix "surmount-nft-ban-drop: "` when logging on | -| Arti | journald via stdout | `console = info`; `log_sensitive_information = false` (no HS keys) | +| Arti | journald via stdout | `console = debug`; `log_sensitive_information = false` (no HS keys) | | nix-daemon | journald | Remote builder path; niced + MemoryMax; per-unit log burst kept high so OOM start is visible; not mail | | surmount-lake (if enabled) | journald | Default-off; niced + MemoryMax on the lake unit; SyslogIdentifier `surmount-lake` | | Vaultwarden (if enabled) | journald via stdout | `LOG_LEVEL=info`; never `LOG_FILE`; never ADMIN_TOKEN | @@ -1937,7 +1937,7 @@ exceptions*. | `nix run .#surmount-private-data` / `just check-private-data` | Private-data pattern scan (`--staged` / `--tree` / `--paths`). | | `nix run .#surmount-host-logs` / `just host-logs` | Host journal follow and `--status`. | | `nix run .#surmount-et` / `just et` | Eternal Terminal client. Reconnects after sleep and network change. Does not replace Mullvad. After the host switch, `etserver` listens on TCP 2022. Long commands still belong in tmux on the guest. | -| `nix run .#surmount-rekey` / `just rekey` | SHC Backups PGP paste: mint age identity (age/rage crate), wrap with gpg, print `age1...` for type PGP. Identity stays 0600 on disk. | +| `nix run .#surmount-rekey` / `just rekey` | SHC Backups PGP paste: mint an X25519 age identity (not the age crate), wrap with gpg, print `age1...` for type PGP. Identity stays 0600 on disk. | | `nix run .#surmount-shc` / `just rdns-shc` | SHC customer user-api (rDNS PTR + tickets). No python3 in the crate. | | `nix run .#surmount-tls-hybrid` / `just check-tls-hybrid` | **D1** host hybrid TLS negotiation probe after B1. Requires `SURMOUNT_E2E_BASE_URL=https://...` or exit **2** BLOCKED. Never a flake check. | | `nix run .#surmount-domain-audit` / `just domain-audit` | Read-only DNS/web/TLS/mail posture (SPF/DKIM/DMARC, dual-sign selectors `stalwart`/`stalwart-rsa`, HTTPS, optional `--smtp` STARTTLS). | diff --git a/docs/SEARCH_AND_UI.md b/docs/SEARCH_AND_UI.md index 3edccf9..186f979 100644 --- a/docs/SEARCH_AND_UI.md +++ b/docs/SEARCH_AND_UI.md @@ -110,8 +110,9 @@ Browser (admin, then v1 webmail) Desktop / MUA (always OK) +-- status / admin APIs ----> Stalwart :8080 ``` -Stalwart webadmin (`/admin` on loopback, optional `/stalwart-admin/` via edge) -is a **bootstrap fallback**, not the long-term Surmount UX. +Stalwart webadmin (`/admin` on loopback only; the edge does not publish +`/stalwart-admin/`) is a **bootstrap fallback**, not the long-term Surmount UX. +Reach it with an SSH tunnel to the loopback listener. Legacy public sites: **static files only** at the edge. Not part of the Leptos app. Live apex/www serve the packaged @@ -317,7 +318,7 @@ full Q-AUTH-1 still residual). - Auth foundation shipped (mode off default); full Q-AUTH-1 residual remains **Exit criteria:** day-to-day admin no longer requires Stalwart webadmin for -common tasks; `/stalwart-admin/` can be disabled by default. +common tasks. The public `/stalwart-admin/` path is not in the nginx escape. ### Operator console UX principles (living) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 8b881de..0b1eff6 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -328,14 +328,14 @@ Q-AUTH-1 still open. Report: `.agents/reports/impl-auth-live-b4-switch.md`. OIDC provider/client. That is **not** the same as Nostr. - **Does not** natively verify Nostr npub signatures unless we build a bridge. -### Mailbox password hashing (Stalwart 0.16.24) +### Mailbox password hashing The management UI does **not** hash mailbox passwords in the browser or in Axum. `POST /api/v1/accounts/password` sends the plaintext secret only over the already-authenticated operator session to loopback Stalwart (`x:Account/set` `credentials.0` `@type Password`). The engine hashes. -Stalwart 0.16.24 (this repo pin in `nix/packages/stalwart-mail.nix`) uses +Stalwart tag v0.16.24 (source re-checked 2026-10-03; the living pin in `nix/packages/stalwart-mail.nix` is **1.0.2** as of 2026-11-03) uses `Authentication.passwordHashAlgorithm`. The enum default is **Argon2id**. Source (tag `v0.16.24`, re-checked 2026-10-03): diff --git a/docs/STACK.md b/docs/STACK.md index 16121dc..f29cd05 100644 --- a/docs/STACK.md +++ b/docs/STACK.md @@ -15,7 +15,7 @@ law). Scaffold defaults still describe what code does today when they differ. Run mail, light static web, admin console, and v1 webmail on a **single operator-chosen VPS** (size/plan open; **Q-HOST-1**; do not invent SKUs). **Stalwart** is the mail engine (version from `nix/packages/stalwart-mail.nix`; currently -**0.16.15** binary FOD). **RocksDB** for all store roles is **fine for now**. +**1.0.2** binary FOD). **RocksDB** for all store roles is **fine for now**. Our Rust product layer (**Axum + Leptos SSR**) is operator admin first, then real webmail, with **Nostr** authentication at the product edge. HTTPS must move to an **Axum-first** edge we own (nginx is transitional-to-delete; no @@ -207,7 +207,7 @@ Firewall set in `modules/networking.nix`. | Host | Path | Upstream | |------|------|----------| | `services.surmount.systems` | `/` | management-ui operator console (UDS target; TCP loopback scaffold) | -| `services.surmount.systems` | `/stalwart-admin/` | Stalwart HTTP (bootstrap only) | +| `services.surmount.systems` | (no `/stalwart-admin/`) | Public Stalwart HTTP proxy removed. Bootstrap is an SSH tunnel to loopback Stalwart HTTP. | | `mail.surmount.systems` | `/` | same console as services. Host classification is Other, so `/` redirects to `/login` on that name. The name is on the certificate. It is not a separate public site. | | apex / www | `/` | packaged SurmountSystems/site static files (flake input; store path default); else UNDER CONSTRUCTION if the root has no index.html (not operator console) | | future vault host | `/` | Vaultwarden local only (private, no public signup) | diff --git a/docs/architecture-review.md b/docs/architecture-review.md index c981776..e94c626 100644 --- a/docs/architecture-review.md +++ b/docs/architecture-review.md @@ -199,8 +199,9 @@ Empty RocksDB + safe defaults (join + module comments): | 587 / 143 | submission / IMAP plain | **not** default in 0.16 safe defaults | Surmount management UI default moved **8080 -> 8090** so it does not fight -Stalwart HTTP. nginx proxies UI on 8090 and `/stalwart-admin/` to -`127.0.0.1:8080`. Env: `SURMOUNT_STALWART_URL=http://127.0.0.1:8080`. +Stalwart HTTP. nginx, when the escape is on, proxies the UI only. +The public `/stalwart-admin/` path was removed. Env: +`SURMOUNT_STALWART_URL=http://127.0.0.1:8080`. Plain 587 is a common MUA expectation; if you need it, it is an apply-time listener choice, not something Nix pins today. @@ -471,13 +472,13 @@ Doc and comment lag can cause wrong operator actions. Prefer one story: |----------|-------------------|--------------| | `docs/STACK.md` diagram and ports table | may still lag if old diagram cells remain | UI **:8090**, Stalwart **:8080** (verify diagram) | | `docs/MIGRATION.md` | *(mopped 2026-08-07)* :8081 references | Management HTTP **:8080** | -| `docs/STACK.md` / README / open-choices engine wording | *(mopped earlier)* soft "after bump" | Pin is **0.16.15** (still not "accepted forever") | -| `docs/DATASTORES.md` section 5.x | *(mopped 2026-08-07)* had TOML-shaped eval + spam v2.0.5 | Living text: 0.16 config.json; spam-filter **3.0.0**; `services.stalwart` | +| `docs/STACK.md` / README / open-choices engine wording | *(mopped earlier)* soft "after bump" | Pin is **1.0.2** (re-read 2026-11-03; still not "accepted forever") | +| `docs/DATASTORES.md` section 5.x | *(mopped 2026-08-07)* had TOML-shaped eval + spam v2.0.5 | Living text: DataStore config.json; spam-filter **3.0.4** rules archive only; `services.stalwart` | | `docs/OPS.md` / `MIGRATION.md` / `SEARCH_AND_UI.md` ports | *(mopped 2026-08-07)* Stalwart **:8081** / UI **:8080** mixups | Stalwart **:8080**, UI default **:8090** | | `docs/fix-and-fixos.md` section 3 "today" | Stalwart still largely from nixpkgs until packaging lands | Packaging **landed** (L1 evidence): Surmount FODs + service module | | `.grok/joins/foundation.md` | Older port/module story | Defer to `stalwart-current.md` + modules | | README "Stalwart admin fallback" ssh example | *(mopped earlier)* was 8081 | Living example uses **8080** | -| open-choices "Engine version" | *(mopped earlier)* soft "after bump" prose | Living pin **0.16.15** + package path | +| open-choices "Engine version" | *(mopped earlier)* soft "after bump" prose | Living pin **1.0.2** + package path | ### 6.2 Code / ops implications to stop treating as settled product law diff --git a/docs/hygiene.md b/docs/hygiene.md index 6b327cf..61c1889 100644 --- a/docs/hygiene.md +++ b/docs/hygiene.md @@ -69,6 +69,8 @@ before a commit object exists: | Git hook | `script/git-hooks/pre-commit` is a short shebang that execs the scanner. Git needs that file. It is not a product bash driver. | | Self-test | crate tests in `crates/surmount-private-data` | +The crate test `bad_age_fixture_is_hit` fails if `crates/surmount-private-data/testdata/bad-age.txt` is missing, and when that file is present it scans the file bytes and requires the age-secret-key class. `crates/surmount-rekey/src/lib.rs` is an excluded test sample for the tree and staged gates, and `rekey_age_test_vector_is_excluded_and_still_detected` fails if that key is removed. + **Classes (regex / path only; never real values in the tool):** PEM / OpenSSH private key blocks; `AGE-SECRET-KEY-1...`; common token shapes (`ghp_`, `github_pat_`, `AKIA...`, etc.); password/secret/token assignment heuristic; @@ -155,7 +157,7 @@ not grow that set. **operator direction YYYY-MM-DD** labels. - **No ADR jargon.** Plain English open choices only: [open-choices.md](open-choices.md). -- Evidence for Stalwart stores (current pin 0.16.15): +- Evidence for Stalwart stores (the 0.16.15 research note; living pin is 1.0.2 in `nix/packages/stalwart-mail.nix`): `docs/research/stalwart-0.16.15-stores-evidence.md`. Historical 0.11.8 only: `docs/research/stalwart-stores-evidence-2026-07-30.md`. diff --git a/docs/open-choices.md b/docs/open-choices.md index 853cdf0..ab81dda 100644 --- a/docs/open-choices.md +++ b/docs/open-choices.md @@ -77,13 +77,13 @@ separate product. ## Engine version **Stalwart is on current via Surmount package** (`nix/packages/stalwart-mail.nix`; -measured **0.16.15** binary FOD as of architecture-review). Compatibility with +measured **1.0.2** binary FOD, re-read 2026-11-03). Compatibility with old Stalwart is **not** a goal. nixpkgs lag is **not** a reason to stay old. - Always re-validate latest when bumping. - Historical note: early scaffold briefly used channel **0.11.8** on host **nixos-25.05**; living host is **nixos-26.05** and engine is Surmount FOD - **0.16.15**. + **1.0.2**. - Evidence: [research/stalwart-0.16.15-stores-evidence.md](research/stalwart-0.16.15-stores-evidence.md). **Still open:** binary FOD until source build, vs require source build before diff --git a/docs/operator-direction.md b/docs/operator-direction.md index d8b080e..37f2630 100644 --- a/docs/operator-direction.md +++ b/docs/operator-direction.md @@ -32,6 +32,21 @@ A peer is collaborating. Agents must not compact this away as "noted." Standing copies: [../AGENTS.md](../AGENTS.md), [COMPACTION-PIN.md](COMPACTION-PIN.md). +## What stays off, and the builder stays on (operator 2026-11-03) + +Operator answers while reviewing the service list. These override a +public-module default when `just deploy` copies the private overlay +from horizon onto surmount-1. + +1. surmount-1 stays a Nix builder and a Lean builder. `surmount-scram.service` stays. This repo does not pin Lean 4.35. +2. Vaultwarden stays off. The operator is not using it yet. +3. Splora mainnet stays off. The other Splora instances already enabled in the private overlay stay as they are. +4. Host renewal on surmount-1 is on. HTTP-01 on surmount-1 refreshes the Let's Encrypt production certificate that host already presents. horizon does not have to stay online for that renewal. Namecheap API credentials stay off surmount-1. No Namecheap ClientIp change. Closing horizon does not stop surmount-1 from serving the PEMs already on disk. +5. Restic backups stay off for now. +6. fail2ban for sshd stays on. The management UI nftables ban helper stays off. +7. Say horizon or surmount-1. The private overlay directory is not a machine name. +8. `just deploy` keeps surmount-1 as the remote Nix builder. The private overlay on horizon already sets `surmount.remoteBuilder.enable = true`. The public module default of false applies only when that file is absent. + --- ## 1. Host diff --git a/docs/packages-and-forks.md b/docs/packages-and-forks.md index 9c0bb7f..f63d791 100644 --- a/docs/packages-and-forks.md +++ b/docs/packages-and-forks.md @@ -64,7 +64,7 @@ package bumps in this repo. | `nix/packages/surmount-mail-import.nix` | Maildir++ import via Vandelay (crane). `nix run .#surmount-mail-import-maildir`. | | `nix/packages/surmount-secrets-install.nix` | Domain A to Domain B install + Vaultwarden export-to-staging (crane). `nix run .#secrets-install-host` / `.#secrets-export-bw-to-staging`. | | `nix/packages/surmount-secrets-prompt.nix` | Domain A no-echo intake (crane). `nix run .#surmount-secrets-prompt`. `just secrets-prompt`. | -| `nix/packages/surmount-rekey.nix` | SHC backup re-key paste (age crate + gpg wrap). `nix run .#surmount-rekey`. `just rekey`. Prints `age1...` for type PGP. Never prints the age secret. | +| `nix/packages/surmount-rekey.nix` | SHC backup re-key paste (X25519 age identity + gpg wrap, not the age crate). `nix run .#surmount-rekey`. `just rekey`. Prints `age1...` for type PGP. Never prints the age secret. | | `nix/overlays.nix` | Extra pins (flake `surmountOverlay` is primary) | | `modules/stalwart-service.nix` | 0.16+ `config.json` service (disables both stock paths: `services/mail/stalwart-mail.nix` and `services/mail/stalwart.nix`; option `services.stalwart`; unit stays `stalwart-mail.service`) | | `modules/*.nix` | Surmount NixOS modules | diff --git a/docs/research/rust-edge-and-uds.md b/docs/research/rust-edge-and-uds.md index fcabd38..af7c297 100644 --- a/docs/research/rust-edge-and-uds.md +++ b/docs/research/rust-edge-and-uds.md @@ -168,7 +168,7 @@ Legend: **Y** yes / **P** partial / **N** no or not serious yet / **?** unverifi - Not a decade of reverse-proxy corner cases (chunked edge cases, weird clients). For a few vhosts this is usually fine; do not claim "nginx parity." - Need explicit design for: graceful reload, log format, metrics, static sites, - optional `/stalwart-admin/` path proxy. + optional `/stalwart-admin/` path proxy (removed from modules/web.nix on 2026-11-02; SSH tunnel to loopback Stalwart HTTP is the bootstrap). - No upstream NixOS module to lean on. **Verdict:** best **pragmatic near-term Rust-first** path for Surmount if the @@ -452,7 +452,7 @@ options shape. 3. tower middleware: body limit, timeout, **governor** per IP. 4. Reverse proxy: - `/` on `services.` hostname -> **UDS** management-ui - - optional `/stalwart-admin/` -> Stalwart **127.0.0.1:8080** + - optional `/stalwart-admin/` -> Stalwart **127.0.0.1:8080** (removed from modules/web.nix on 2026-11-02) 5. Redirects for apex/www/mail hostname landing (mirror current nginx vhosts). 6. Static file root hook for legacy sites (even if empty at first). 7. NixOS module replaces `services.nginx` in `modules/web.nix`. @@ -494,7 +494,7 @@ VM test; then delete nginx config paths. 3. Edge binary + module flag `surmount.web.backend = "nginx" | "rust-edge"`. 4. Staging rebuild; validate HTTP-01/ALPN renew. 5. Remove nginx; update EDGE_AND_TLS, STACK, options, architecture-review. -6. Harden rate limits; drop optional stalwart-admin path when product UI covers +6. Harden rate limits. The public stalwart-admin path was removed from modules/web.nix on 2026-11-02. ops. --- diff --git a/docs/research/scaffold-assumptions-inventory.md b/docs/research/scaffold-assumptions-inventory.md index 62c2fb0..21cd0d6 100644 --- a/docs/research/scaffold-assumptions-inventory.md +++ b/docs/research/scaffold-assumptions-inventory.md @@ -335,7 +335,7 @@ ports; docs lag is called out per row where it matters. - **Claim the tree currently implies:** Management UI listens **127.0.0.1:8090** (default). Stalwart 0.16 first-boot HTTP management is **:8080** (upstream safe defaults; may bind all interfaces). nginx proxies - `/` -> UI :8090 and `/stalwart-admin/` -> `127.0.0.1:8080`. UI env + `/` -> UI :8090. The public `/stalwart-admin/` proxy was removed from modules/web.nix on 2026-11-02. UI env `SURMOUNT_STALWART_URL=http://127.0.0.1:8080`. Living OPS/MIGRATION/ SEARCH_AND_UI mopped to that story (2026-08-07). Some joins may still lag. - **Where it lives:** `modules/options.nix` (port 8090), @@ -407,13 +407,14 @@ ports; docs lag is called out per row where it matters. ## 16. Spam-filter (and WebUI) FODs not auto-applied -- **Claim the tree currently implies:** spam-filter **3.0.0** and webui - **1.0.7** are fetched as FODs and installed under - `/etc/surmount/stalwart/` (`spam-filter.toml`, - `spam-filter-rules.json.gz`, `webui.zip`). They are **not** automatically - pointed into the running engine. 0.16 first-boot may still try GitHub for - resources until operator WebUI or `stalwart-cli apply` sets hermetic - `file://` URLs. Spam-filter TOML is **anti-spam config**, not the message DB. +- **Claim the tree currently implies:** spam-filter **3.0.4** and webui + **2.0.4** are fetched as FODs and installed under + `/etc/surmount/stalwart/` (`spam-filter-rules.json.gz`, `webui.zip`). + v3.0.3 dropped `spam-filter.toml` from the release. They are **not** + automatically pointed into the running engine. First boot may still try + GitHub for resources until operator WebUI or `stalwart-cli apply` sets + hermetic `file://` URLs. The rules archive is **anti-spam config**, not + the message DB. - **Where it lives:** `nix/packages/stalwart-spam-filter.nix`, `stalwart-webui.nix`, `modules/mail.nix` (`environment.etc`), `.grok/joins/stalwart-current.md`, `docs/DATASTORES.md` section 5.4 @@ -626,8 +627,8 @@ ports; docs lag is called out per row where it matters. ## 27. Doc and join lag (meta-assumption risk) - **Claim (living modules):** Stalwart HTTP management **:8080**, Surmount UI - default **:8090**, engine **0.16.15** config.json via `services.stalwart`, - spam-filter **3.0.0** FODs under `/etc/surmount/stalwart/`, stock modules + default **:8090**, engine **1.0.2** config.json via `services.stalwart`, + spam-filter **3.0.4** rules archive under `/etc/surmount/stalwart/`, stock modules dual-disabled. - **Where lag lived:** older STACK/MIGRATION/OPS/SEARCH port rows, DATASTORES 5.2 TOML eval, foundation join. Mop pass 2026-08-07 fixed living MIGRATION / diff --git a/docs/research/version-audit.md b/docs/research/version-audit.md index e33c6a4..a212fe8 100644 --- a/docs/research/version-audit.md +++ b/docs/research/version-audit.md @@ -67,11 +67,11 @@ date in that row** until revalidated. | Host nixpkgs channel | **Living:** `nixos-26.05` @ `26f4107e47e3ad0ffd0db220469ba83b6f6d96e8` (`flake.lock` 2026-10-17, commit date 2026-10-15). Sample `system.stateVersion = "26.05"`. Prior lock `445d861c` was the 2026-08-07 note. | | Crane / management-ui rustc | **Living:** **1.99.1** from `nixpkgs-rust` `pkgs.rustc` (`nix/rust-toolchain.nix`; lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, checked 2026-10-18, `nix eval --raw` printed `1.99.1`). Not nixpkgs master (master HEAD `27347cc2965498fa03393739031c9ef8ecd72c0a` still has `rustcVersion = "1.99.0"`). Not `rustPackages_1_95`. Not beta 1.100. Host nixos-26.05 `pkgs.rustc` is still **1.95.0**. grok-oss uses the same toolchain. Leptos MSRV floor remains **>= 1.88**. Prior lock `c46e6c9a` was rustc 1.99.0. | | Stock Stalwart modules | **Living:** dual `disabledModules` (`services/mail/stalwart-mail.nix` + `services/mail/stalwart.nix`); Surmount option path `services.stalwart` | -| Stalwart server FOD | **0.16.26** Surmount pin (not channel package). Upstream latest re-check **2026-10-17**: v0.16.26 (2026-10-11). Prior 2026-07-31 cell was 0.16.15. | -| stalwart-cli FOD | **1.0.13** (re-check 2026-10-17: at latest, tag 2026-09-28) | -| WebUI FOD | **1.0.11** (re-check 2026-10-17: at latest, tag 2026-09-15) | -| spam-filter FOD | **3.0.2** (re-check 2026-10-17: at latest, tag 2026-09-22) | -| vandelay FOD | **1.0.12** (re-check 2026-10-17: at latest, tag 2026-10-11). Prior tree pin was 1.0.11. | +| Stalwart server FOD | **1.0.2** Surmount pin (not channel package). Upstream latest re-check **2026-11-03**: v1.0.2 (published 2026-11-02T15:42:44Z, not a prerelease). Prior 2026-10-17 cell was 0.16.26. Prior 2026-07-31 cell was 0.16.15. | +| stalwart-cli FOD | **1.0.14** (re-check 2026-11-03: at latest, published 2026-10-25T18:15:42Z). Prior 2026-10-17 cell was 1.0.13. | +| WebUI FOD | **2.0.4** (re-check 2026-11-03: at latest, published 2026-11-02T11:19:17Z). Prior 2026-10-17 cell was 1.0.11. | +| spam-filter FOD | **3.0.4** (re-check 2026-11-03: at latest, published 2026-11-01T15:50:40Z). Rules archive only. Prior 2026-10-17 cell was 3.0.2, which still shipped `spam-filter.toml`. | +| vandelay FOD | **1.0.14** (re-check 2026-11-03: at latest, published 2026-11-01T16:43:01Z). Prior 2026-10-17 cell was 1.0.12. | | Arti (Surmount HS package) | Surmount-owned source build **2.7.0** (GitLab `arti-v2.7.0`; `arti-onion-service.nix`; checked 2026-10-17). nixpkgs-rust `pkgs.arti.version` is **2.7.0**. Stock host `pkgs.arti` is channel-lagged client-default (not the HS path). | | crane / sops-nix flake locks | Crane is `bd63bf1` (2026-10-11), one internal flake.lock commit after `47b6b27`, newer than release v0.24.0. sops-nix is `44931708` (2026-10-15), still master. Re-checked 2026-10-17. | | management-ui Cargo.lock | 2026-10-17: reqwest ceiling is **0.13**, tower-http ceiling is **0.7**, rustls is **0.23.46** (git patch; menhera 10d still lists 0.23.45), nostr stays **0.45.5**, rcgen ceiling is **0.14**. | @@ -97,13 +97,13 @@ Not source-built (vendor 403 / rustc notes; see package comments and | Pin | Tree version | Where | Latest upstream | Gap? | Source | |-----|--------------|-------|-----------------|------|--------| -| Stalwart server | **0.16.26** | `nix/packages/stalwart-mail.nix` | **v0.16.26** (2026-10-11) | **no** (re-check 2026-10-17) | https://github.com/stalwartlabs/stalwart/releases/tag/v0.16.26 | -| stalwart-cli | **1.0.13** | `nix/packages/stalwart-cli.nix` | **v1.0.13** (2026-09-28) | **no** | https://github.com/stalwartlabs/cli/releases/tag/v1.0.13 | -| WebUI zip | **1.0.11** | `nix/packages/stalwart-webui.nix` | **v1.0.11** (2026-09-15) | **no** | https://github.com/stalwartlabs/webui/releases/tag/v1.0.11 | -| spam-filter | **3.0.2** | `nix/packages/stalwart-spam-filter.nix` | **v3.0.2** (2026-09-22) | **no** | https://github.com/stalwartlabs/spam-filter/releases/tag/v3.0.2 | -| vandelay | **1.0.12** | `nix/packages/vandelay.nix` | **v1.0.12** (2026-10-11) | **no** | https://github.com/stalwartlabs/vandelay/releases/tag/v1.0.12 | +| Stalwart server | **1.0.2** | `nix/packages/stalwart-mail.nix` | **v1.0.2** (2026-11-02T15:42:44Z) | **no** (re-check 2026-11-03) | https://github.com/stalwartlabs/stalwart/releases/tag/v1.0.2 | +| stalwart-cli | **1.0.14** | `nix/packages/stalwart-cli.nix` | **v1.0.14** (2026-10-25T18:15:42Z) | **no** | https://github.com/stalwartlabs/cli/releases/tag/v1.0.14 | +| WebUI zip | **2.0.4** | `nix/packages/stalwart-webui.nix` | **v2.0.4** (2026-11-02T11:19:17Z) | **no** | https://github.com/stalwartlabs/webui/releases/tag/v2.0.4 | +| spam-filter | **3.0.4** | `nix/packages/stalwart-spam-filter.nix` | **v3.0.4** (2026-11-01T15:50:40Z) | **no** | https://github.com/stalwartlabs/spam-filter/releases/tag/v3.0.4 | +| vandelay | **1.0.14** | `nix/packages/vandelay.nix` | **v1.0.14** (2026-11-01T16:43:01Z) | **no** | https://github.com/stalwartlabs/vandelay/releases/tag/v1.0.14 | -The 2026-07-31 cells were server 0.16.15, cli 1.0.12, WebUI 1.0.7, and spam-filter 3.0.0. Those are not the living pins. +The 2026-10-17 cells were server 0.16.26, cli 1.0.13, WebUI 1.0.11, spam-filter 3.0.2, and vandelay 1.0.12. The 2026-07-31 cells were server 0.16.15, cli 1.0.12, WebUI 1.0.7, and spam-filter 3.0.0. Those are not the living pins. The 2026-11-02 pass held cli, vandelay, and spam-filter at the older patch because they were still the same major. That hold is lifted. Each row above is the GitHub `releases/latest` tag read on 2026-11-03. ### Asset URL shapes (for future bumps) @@ -113,7 +113,7 @@ The 2026-07-31 cells were server 0.16.15, cli 1.0.12, WebUI 1.0.7, and spam-filt | server aarch64 | `.../stalwart-aarch64-unknown-linux-gnu.tar.gz` | | cli x86_64 | `https://github.com/stalwartlabs/cli/releases/download/vX.Y.Z/stalwart-cli-x86_64-unknown-linux-gnu.tar.xz` | | webui | `https://github.com/stalwartlabs/webui/releases/download/vX.Y.Z/webui.zip` | -| spam-filter | `.../spam-filter.toml` and `.../spam-filter-rules.json.gz` | +| spam-filter | `.../spam-filter-rules.json.gz` only (v3.0.3 and later). v3.0.2 was the last release that also published `spam-filter.toml` | Bump recipe is already in each package file header (`nix store prefetch-file`). @@ -133,6 +133,10 @@ engine; Surmount FODs win): Surmount's FODs are intentionally **ahead of** nixpkgs for the mail stack. Do not "sync down" to channel packages. +Those channel rows were measured 2026-07-30 against a Surmount pin of 0.16.15. +On 2026-11-03 the living Surmount server pin is 1.0.2. Do not read +"one patch behind Surmount" as a present-tense claim. + --- ## 2. Flake inputs (`flake.lock`) @@ -366,8 +370,9 @@ Do not treat host rustc 1.95.0 as the product pin. The 2026-10-03 lock Priority order (proposed, not accepted): -1. **Stalwart FODs** were re-checked 2026-10-17: server **0.16.26**, cli - **1.0.13**, WebUI **1.0.11**, spam-filter **3.0.2**, vandelay **1.0.12**. +1. **Stalwart FODs** were re-checked 2026-11-03: server **1.0.2**, cli + **1.0.14**, WebUI **2.0.4**, spam-filter **3.0.4** (rules archive only), + vandelay **1.0.14**. The 2026-10-17 cells are not the living pins. Re-check releases before every packaging PR. 2. **Arti currency:** the Surmount package is **2.7.0** (GitLab `arti-v2.7.0`), equal to nixpkgs-rust `pkgs.arti.version`. Live Tor @@ -431,11 +436,11 @@ re-running. Keep reports short under `~/.agents/reports/`. | Sample stateVersion 26.05 | `hosts/mail-vps/configuration.nix`, `tests/module-eval.nix`, `tests/mail.nix` | | Crane rustc = 1.99.1 from `nixpkgs-rust` `pkgs.rustc` | `nix/rust-toolchain.nix`. Lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332` (checked 2026-10-18). grok-oss native build inputs are `surmount-rust-1.99.1`. Not `rustPackages_1_95`. Host nixos-26.05 `pkgs.rustc` is still 1.95.0. Prior lock `c46e6c9a` was rustc 1.99.0. | | Dual stock module disable | `modules/stalwart-service.nix` `disabledModules` | -| Stalwart latest = 0.16.26 | GitHub tag `v0.16.26` published 2026-10-11 (re-check 2026-10-17). The 2026-07-31 cell that said 0.16.15 is historical. | -| CLI latest = 1.0.13 | GitHub tag `v1.0.13` 2026-09-28 (re-check 2026-10-17) | -| WebUI latest = 1.0.11 | GitHub tag `v1.0.11` 2026-09-15 (re-check 2026-10-17) | -| spam-filter latest = 3.0.2 | GitHub tag `v3.0.2` 2026-09-22 (re-check 2026-10-17) | -| vandelay latest = 1.0.12 | GitHub tag `v1.0.12` 2026-10-11 (re-check 2026-10-17) | +| Stalwart latest = 1.0.2 | GitHub tag `v1.0.2` published 2026-11-02T15:42:44Z (re-check 2026-11-03). The 2026-10-17 cell that said 0.16.26 is historical. The 2026-07-31 cell that said 0.16.15 is historical. | +| CLI latest = 1.0.14 | GitHub tag `v1.0.14` published 2026-10-25T18:15:42Z (re-check 2026-11-03). The 2026-10-17 cell that said 1.0.13 is historical. | +| WebUI latest = 2.0.4 | GitHub tag `v2.0.4` published 2026-11-02T11:19:17Z (re-check 2026-11-03). The 2026-10-17 cell that said 1.0.11 is historical. | +| spam-filter latest = 3.0.4 | GitHub tag `v3.0.4` published 2026-11-01T15:50:40Z (re-check 2026-11-03). Asset is `spam-filter-rules.json.gz` only. The 2026-10-17 cell that said 3.0.2 is historical. | +| vandelay latest = 1.0.14 | GitHub tag `v1.0.14` published 2026-11-01T16:43:01Z (re-check 2026-11-03). The 2026-10-17 cell that said 1.0.12 is historical. | | Surmount arti-onion-service = 2.7.0 | package expression, GitLab `arti-v2.7.0`, cargoDeps assert vs nixpkgs-rust `arti` 2.7.0 (checked 2026-10-17). | | rustc pin = 1.99.1 | Upstream stable is 1.99.1 (accessed: 2026-10-17). nixpkgs-rust `9ecc0a67` is that rustc. Master still has 1.99.0. Prior lock `c46e6c9a` was rustc 1.99.0. The 2026-07-31 cell that said 1.97.1 is historical. | | Historical host 25.05 / crane 1.88 | 2026-07-31 audit day only (see footnotes) | diff --git a/flake.lock b/flake.lock index a15d543..7cb9037 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "advisory-db": { "flake": false, "locked": { - "lastModified": 1792766536, - "narHash": "sha256-IO2oOHRZ3yKjhBLzUYWsIV1gfwIrEZ77XDefHM98T3A=", + "lastModified": 1793619944, + "narHash": "sha256-Ry4BewR3YDlxkbRQN9/z7BX4guU9RkdRhXNy6ci+Fds=", "owner": "rustsec", "repo": "advisory-db", - "rev": "84d943a6deeb8cc6c6f632531aaf4f1c62275239", + "rev": "263a4d951a0867d4bddc29dddd6d59427c78f9a7", "type": "github" }, "original": { @@ -65,11 +65,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1792615934, - "narHash": "sha256-MlvRfa1O5Cqc2/a+gG+l9TSuowEoCKCmrJ1d9oAVm0I=", + "lastModified": 1793633738, + "narHash": "sha256-tItrb9M8eYA6WhFgmT9tpGKueri3+wRuVA952o/2xSU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "da6dc0e7b26060f2f71ce3ef7088fb56ee44fa36", + "rev": "7dcdd87373d72e7a2173e4db02e20d3d7f495cfd", "type": "github" }, "original": { @@ -130,11 +130,11 @@ ] }, "locked": { - "lastModified": 1789802016, - "narHash": "sha256-QVFfArbYaQMGK4dFV+5RUL72Ci+hNHVhaF/R9qJfDhs=", + "lastModified": 1793000331, + "narHash": "sha256-6sLQgHX/V/yF8VrhmsJroujeRua1GY3nDTEpnY7tHF4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "26a71e661c47bd21a05d06fec749f3f7c75e9d12", + "rev": "0d0575988e4bb9ebc4cf1afb1cec1ceac0b5914e", "type": "github" }, "original": { @@ -172,11 +172,11 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1792681480, - "narHash": "sha256-3reNjiQDJ32ENFhqbb+f6AM2JX6of7r24vRsZy8tFDg=", + "lastModified": 1793252175, + "narHash": "sha256-pHOfDKsOrqeplMAKdsY//+xEupsGfJwxyYJhUyZWvDo=", "owner": "SurmountSystems", "repo": "splora", - "rev": "94d8903fde17fb4f888fd32548919d7be469a95f", + "rev": "dd71ab234fb416efbdaf85315efa806cabba514f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index db2e937..60eb5e9 100644 --- a/flake.nix +++ b/flake.nix @@ -439,6 +439,9 @@ # Flake-pinned just for GHA (`nix shell .#just -c just ci`) and local # bootstrap without a host just install. just = pkgs.just; + # Mutinynet-only bitcoind. Does not replace pkgs.bitcoind. + # testnet3 and testnet4 stay on the stock 32.1 package. + bitcoind-mutinynet = pkgs.callPackage ./nix/packages/bitcoind-mutinynet.nix { }; # Ookla Speedtest CLI with --accept-license --accept-gdpr. # Unfree predicate is only ookla-speedtest. nix run .#speedtest speedtest = diff --git a/modules/arti-hidden-service.nix b/modules/arti-hidden-service.nix index dedb613..dbe95e1 100644 --- a/modules/arti-hidden-service.nix +++ b/modules/arti-hidden-service.nix @@ -197,7 +197,9 @@ let # stdout -> journald via the unit. Never log HS keys. [logging] - console = "info" + # debug records circuits and publish failures. Sensitive material + # (including hidden-service keys) stays off. Do not set trace. + console = "debug" log_sensitive_information = false # Per-site v3 onions reverse-proxying to management UI cleartext. @@ -348,6 +350,25 @@ in serviceConfig = { Type = "oneshot"; RemainAfterExit = true; + # Status text only. No host paths with key material are printed. + DynamicUser = true; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictAddressFamilies = [ "AF_UNIX" ]; + SystemCallArchitectures = "native"; + SyslogIdentifier = "surmount-arti-scaffold-status"; + StandardOutput = "journal"; + StandardError = "journal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; ExecStart = pkgs.writeShellScript "surmount-arti-status" '' set -eu echo "surmount: arti HS module enabled (management-publish config installed; startDaemon=false)." @@ -397,6 +418,13 @@ in RestartSec = "10s"; # Ordinary CFS. Not fifo, not rr, not idle I/O, not realtime I/O. Nice = -18; + # stdout is the Arti console (debug, sensitive off). Burst so a + # circuit storm does not drop the first publish failure. + SyslogIdentifier = "surmount-arti-hidden-service"; + StandardOutput = "journal"; + StandardError = "journal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; # state_dir is onionServiceStateDir (HS identity + instance state). # cache_dir is under stateDir. Both need write for a live publisher. ReadWritePaths = [ diff --git a/modules/backups.nix b/modules/backups.nix index 634a831..33a2160 100644 --- a/modules/backups.nix +++ b/modules/backups.nix @@ -36,5 +36,14 @@ in "--keep-monthly 6" ]; }; + + # restic already prints snapshot success and failure. Name the unit + # and keep those lines when a backup overlaps a journal flood. + # passwordFile stays a path. Nothing here enables the backup set. + systemd.services.restic-backups-surmount.serviceConfig = { + SyslogIdentifier = "restic-backups-surmount"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; }; } diff --git a/modules/eternal-terminal.nix b/modules/eternal-terminal.nix index 5f41ddb..e0b8f9d 100644 --- a/modules/eternal-terminal.nix +++ b/modules/eternal-terminal.nix @@ -18,7 +18,10 @@ in services.eternal-terminal = { enable = true; port = et.port; - verbosity = 1; + # 2 records connection open and close above the previous 1. + # 9 is not used: this pass did not read etserver for secret fields + # at the top of the 0-9 scale. silent stays false. + verbosity = 2; silent = false; }; @@ -30,7 +33,12 @@ in ]; # Operator access path: do not set Nice= or MemoryMax (sshd class). - systemd.services.eternal-terminal.serviceConfig.SyslogIdentifier = "eternal-terminal"; + # Journal burst so login and disconnect lines survive a flood. + systemd.services.eternal-terminal.serviceConfig = { + SyslogIdentifier = "eternal-terminal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; } ]); } diff --git a/modules/hardening.nix b/modules/hardening.nix index 14c60a8..cfdc5e8 100644 --- a/modules/hardening.nix +++ b/modules/hardening.nix @@ -59,17 +59,43 @@ in PermitRootLogin = mkDefault "prohibit-password"; X11Forwarding = false; AllowAgentForwarding = mkDefault false; + # Empty passwords and user ~/.ssh/environment are not an option. + # When password auth is off, publickey is the only method so a + # later default cannot turn keyboard-interactive back on. + PermitEmptyPasswords = false; + PermitUserEnvironment = false; + # Default grace is 120 seconds of unauthenticated hold. + LoginGraceTime = 60; + } + // lib.optionalAttrs (!cfg.hardening.allowPasswordAuth) { + AuthenticationMethods = "publickey"; }; }; - # fail2ban: light transitional sketch for sshd only. - # Target policy is merciless ban + whitelist + last-used (see header). - # Mail abuse: Stalwart spam-filter + greylisting first; not wild jails. - # Prefer shrinking this over growing Python jails as product identity. + # fail2ban: sshd jail only. Permanent ban after 3 failures. + # bantime "-1" is fail2ban's never-unban value. The option type is + # a string. iptables-multiport inserts a rule with no kernel + # timeout. Restore keeps bips rows whose bantime is -1. + # Mail abuse stays with Stalwart. Do not add jails here. services.fail2ban = { enable = mkDefault true; - maxretry = 5; - bantime = "1h"; + maxretry = 3; + bantime = "-1"; + # modules/networking.nix leaves networking.nftables.enable false, + # so the host firewall backend is iptables and this action is + # iptables-multiport (same package as networking.firewall). + # Native nftables blacklists ip_tables, so the action must + # switch or the ban never installs. + banaction = + if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"; + # SYSTEMD-JOURNAL is the native journal path. SYSLOG depends on + # /dev/log while journald ForwardToSyslog is no. INFO records + # ban, unban, and found lines. DEBUG is not set (it copies matched + # log lines). No mail jails. + daemonSettings.Definition = { + logtarget = "SYSTEMD-JOURNAL"; + loglevel = "INFO"; + }; jails = { sshd = { settings = { @@ -82,6 +108,12 @@ in }; }; + systemd.services.fail2ban.serviceConfig = { + SyslogIdentifier = "fail2ban"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; + # Kernel / network hygiene (small set; avoid surprising operators). boot.kernelParams = mkDefault [ "page_alloc.shuffle=1" ]; @@ -97,11 +129,23 @@ in # the unit stays dead. Want multi-user so SHC inject works on boot # and after this generation. systemd.services.qemu-guest-agent.wantedBy = [ "multi-user.target" ]; + # Start and guest-exec lines stay in the journal. The option stays + # default off; this does not enable the agent. + systemd.services.qemu-guest-agent.serviceConfig = { + SyslogIdentifier = "qemu-guest-agent"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; }) # Access-control nft fragment: opt-in, lean private default off. (mkIf (cfg.enable && ac.enable && ac.nftSets) { networking.nftables.enable = true; + systemd.services.nftables.serviceConfig = { + SyslogIdentifier = "nftables"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; networking.nftables.tables.${nftTable} = { family = "inet"; content = '' @@ -132,8 +176,8 @@ in ${ if (cfg.logging.enable or false) then '' - ip saddr @${nftBan4} log prefix "surmount-nft-ban-drop: " drop - ip6 saddr @${nftBan6} log prefix "surmount-nft-ban-drop: " drop + ip saddr @${nftBan4} log prefix "surmount-nft-ban-drop: " level info drop + ip6 saddr @${nftBan6} log prefix "surmount-nft-ban-drop: " level info drop '' else '' @@ -304,6 +348,11 @@ in StandardInput = "socket"; StandardOutput = "socket"; StandardError = "journal"; + # stderr is the helper log. stdout stays the socket (the + # protocol). Do not log nft payloads or set contents. + SyslogIdentifier = "surmount-nft-ban-helper"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; }; }; } diff --git a/modules/lake.nix b/modules/lake.nix index f168d3f..44156bc 100644 --- a/modules/lake.nix +++ b/modules/lake.nix @@ -119,7 +119,12 @@ in systemd.services.stalwart-mail.serviceConfig.OOMScoreAdjust = lib.mkDefault (-300); systemd.services.surmount-management-ui.serviceConfig.OOMScoreAdjust = lib.mkDefault (-300); systemd.services.sshd.serviceConfig.OOMScoreAdjust = lib.mkDefault (-300); - systemd.services.surmount-arti-hidden-service.serviceConfig.OOMScoreAdjust = lib.mkDefault (-300); + # Do not instantiate the daemon unit when startDaemon is false. + # A bare serviceConfig assignment would create it with enable defaulting + # to true. OOM protection still applies when the daemon is requested. + systemd.services.surmount-arti-hidden-service = lib.mkIf cfg.artiHiddenService.startDaemon { + serviceConfig.OOMScoreAdjust = lib.mkDefault (-300); + }; }) ]; } diff --git a/modules/logging.nix b/modules/logging.nix index b50ab1c..ca3632d 100644 --- a/modules/logging.nix +++ b/modules/logging.nix @@ -80,14 +80,28 @@ in RateLimitIntervalSec=${log.rateLimitIntervalSec} RateLimitBurst=${log.rateLimitBurst} ForwardToSyslog=no + # Pin debug so a unit console at debug (Arti) is stored. trace is + # not requested. This is not a guest disk size. + MaxLevelStore=debug ''; # Auth failures and disconnects without passwords. Not DEBUG3. + # sshd_config DEBUG violates user privacy. VERBOSE stays the level. services.openssh.settings.LogLevel = log.sshdLogLevel; + # sshd is un-niced. Raise only the per-unit journal burst so auth + # lines are not dropped at the systemd default of 10000/30s. + systemd.services.sshd.serviceConfig = { + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; + # Per-unit burst so nix-daemon start/stop/OOM lines survive a flood. - # Do not Nice= this unit here (remote-builder owns Nice when enabled). + # Identifier is set here when logging is on. remote-builder sets the + # same identifier when the builder cap is on. Do not Nice= this unit + # here (remote-builder owns Nice when enabled). systemd.services.nix-daemon.serviceConfig = { + SyslogIdentifier = "nix-daemon"; LogRateLimitIntervalSec = "30s"; LogRateLimitBurst = 50000; }; diff --git a/modules/mail-host-rust-cpu.nix b/modules/mail-host-rust-cpu.nix index 9a3debe..f99e7ba 100644 --- a/modules/mail-host-rust-cpu.nix +++ b/modules/mail-host-rust-cpu.nix @@ -82,12 +82,15 @@ let // lib.optionalAttrs (prevWrap != null) { SURMOUNT_RUSTC_WRAPPER_NEXT = prevWrap; }; - passthru = (old.passthru or { }) // { - mailHostRustCpuHook = hookText; - mailHostRustCpuUses = "RUSTC_WRAPPER"; - } // lib.optionalAttrs (nextDeps != null) { - mailHostRustCpuCargoArtifacts = nextDeps; - }; + passthru = + (old.passthru or { }) + // { + mailHostRustCpuHook = hookText; + mailHostRustCpuUses = "RUSTC_WRAPPER"; + } + // lib.optionalAttrs (nextDeps != null) { + mailHostRustCpuCargoArtifacts = nextDeps; + }; } // lib.optionalAttrs (nextDeps != null) { cargoArtifacts = nextDeps; diff --git a/modules/mail.nix b/modules/mail.nix index a711421..2fffc9e 100644 --- a/modules/mail.nix +++ b/modules/mail.nix @@ -187,7 +187,7 @@ in ''; } (lib.mkIf (spamFilter != null) { - "surmount/stalwart/spam-filter.toml".source = "${spamFilter}/spam-filter.toml"; + # spam-filter 3.0.3 and later publish only the rules archive. "surmount/stalwart/spam-filter-rules.json.gz".source = "${spamFilter}/spam-filter-rules.json.gz"; }) (lib.mkIf (webui != null) { diff --git a/modules/management-ui.nix b/modules/management-ui.nix index 9771c2e..18dd010 100644 --- a/modules/management-ui.nix +++ b/modules/management-ui.nix @@ -60,8 +60,17 @@ let else "${pkgs.writeText "surmount-static-vhosts.json" (builtins.toJSON staticVhostsJsonMap)}"; extraMailHostnamesCsv = lib.concatStringsSep "," (lib.filter (h: h != "") ui.extraMailHostnames); + # HTTP-01 webroot. Empty option uses stateDir/acme-http01. The renew + # timer and the :80 listener share this directory. Not a Namecheap path. + http01Renew = ui.acme.enable && ui.acme.challenge == "http-01"; + effectiveHttp01Webroot = + if ui.acme.http01Webroot != "" then ui.acme.http01Webroot else "${cfg.stateDir}/acme-http01"; # Union status.{primaryDomain} onto a non-empty ACME identifier list so the # systems dashboard Host is on the certificate hostname set for this edge. + # Measured 2026-11-03 from horizon: status.surmount.systems has the same + # A record as the apex, mail, and services names (no AAAA on any of them), + # and port 80 returns 308 to https://status.surmount.systems/. That name + # stays on this order. Do not record addresses in this file. acmeDomains = let statusHost = lib.optional (cfg.primaryDomain != "") "status.${cfg.primaryDomain}"; @@ -218,6 +227,9 @@ let ++ optional (ui.acme.dnsProvider == "external-hook") ( "SURMOUNT_ACME_DNS_HOOK_TIMEOUT_SECS=${toString ui.acme.dnsHookTimeoutSecs}" ) + ++ optional ( + ui.acme.challenge == "http-01" + ) "SURMOUNT_ACME_HTTP01_WEBROOT=${effectiveHttp01Webroot}" ) ++ optional redirectListenActive "SURMOUNT_HTTP_REDIRECT_LISTEN=${ui.httpRedirectListen}" ++ optional localCleartextActive "SURMOUNT_LOCAL_CLEARTEXT_LISTEN=${effectiveLocalCleartext}" @@ -296,6 +308,26 @@ let ++ optional ac.nftHelper "SURMOUNT_BAN_NFT_HELPER_SOCK=/run/surmount/nft-ban-helper.sock" ); + # acme-renew is not an HTTP route. It loads ACME and TLS only. + # Do not pass nostr auth settings and do not attach the session secret + # file. The UI unit keeps both. The secret stays a host file, not a + # Nix store copy. + renewDropsAuthLine = + line: + let + stripped = lib.removePrefix "'" line; + key = builtins.head (lib.splitString "=" stripped); + in + !( + lib.hasPrefix "SURMOUNT_AUTH_" key + || lib.hasPrefix "SURMOUNT_SESSION_" key + || lib.hasPrefix "SURMOUNT_NOSTR_" key + || lib.hasPrefix "SURMOUNT_NIP98_" key + || key == "SURMOUNT_PUBLIC_BASE_URL" + || key == "SURMOUNT_ALLOW_PUBLIC_AUTH_OFF" + ); + renewEnvList = lib.filter renewDropsAuthLine envList; + # Non-TLS host secret paths stay read-only. TLS PEMs are read-only when # ACME is off; when ACME is on the binary may write cert/key + account JSON. # ProtectSystem=strict: create-on-issue needs **parent directories** writable @@ -444,6 +476,11 @@ let SyslogIdentifier = "surmount-management-ui"; StandardOutput = "journal"; StandardError = "journal"; + # Crate debug is already in RUST_LOG. Do not add rustls=debug (handshake + # failures are our warn events; rustls debug can include key material). + # Burst keeps http_request and tls_handshake_failed during a scan. + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; Environment = envList; NoNewPrivileges = true; PrivateTmp = true; @@ -522,6 +559,51 @@ let # host generation includes the site without a mutable /var/lib copy. # Operator may still set apexPublicRoot to a host directory. packagedPublicSite = if pkgs ? surmount-public-site then "${pkgs.surmount-public-site}" else ""; + + # Oneshot script the timer execs. builtins.readFile of this path is the + # hermetic proof the unit names the management UI and the PEM paths. + # No Namecheap client, no API token, no DNS hook. + certRenewScript = pkgs.writeShellScript "surmount-cert-renew" '' + set -eu + # acme-renew loads ACME and TLS only. It does not read the session secret. + cert=${lib.escapeShellArg ui.tlsCertPath} + key=${lib.escapeShellArg ui.tlsKeyPath} + stamp=${lib.escapeShellArg "${effectiveHttp01Webroot}/.renewed"} + before=$(${pkgs.coreutils}/bin/stat -c %Y "$cert" 2>/dev/null || echo missing) + ${pkg}/bin/surmount-management-ui acme-renew + after=$(${pkgs.coreutils}/bin/stat -c %Y "$cert" 2>/dev/null || echo missing) + if [ "$before" != "$after" ]; then + # surmount-management-ui wrote the TLS PEMs this host presents. + # Leave an existing stamp in place so a failed root post can retry. + ${pkgs.coreutils}/bin/touch "$stamp" + fi + ''; + # Root post-step. The "+" prefix ignores this unit's sandbox, so root can + # chown the mail copies and restart units. surmount-ui cannot do that. + # Copy only when the stamp says the PEM mtime changed. Match + # surmount-mail-tls-copy: cert 0640, key 0600, owner stalwart-mail. + # Do not delete the source PEMs. A failed order never creates the stamp. + certRenewRestartScript = pkgs.writeShellScript "surmount-cert-renew-restart" '' + set -eu + stamp=${lib.escapeShellArg "${effectiveHttp01Webroot}/.renewed"} + srcCert=${lib.escapeShellArg ui.tlsCertPath} + srcKey=${lib.escapeShellArg ui.tlsKeyPath} + dst=${lib.escapeShellArg "${cfg.secrets.durableMaterialDir}/mail/tls"} + if [ -f "$stamp" ]; then + if [ ! -f "$srcCert" ] || [ ! -f "$srcKey" ]; then + echo "surmount-cert-renew: refusing to copy mail TLS PEMs; source missing" >&2 + exit 1 + fi + ${pkgs.coreutils}/bin/mkdir -p "$dst" + ${pkgs.coreutils}/bin/chmod 0750 "$dst" + ${pkgs.coreutils}/bin/chown stalwart-mail:stalwart-mail "$dst" || true + ${pkgs.coreutils}/bin/install -o stalwart-mail -g stalwart-mail -m 0640 "$srcCert" "$dst/cert.pem" + ${pkgs.coreutils}/bin/install -o stalwart-mail -g stalwart-mail -m 0600 "$srcKey" "$dst/key.pem" + ${pkgs.systemd}/bin/systemctl restart stalwart-mail.service + ${pkgs.systemd}/bin/systemctl try-restart surmount-management-ui.service + ${pkgs.coreutils}/bin/rm -f "$stamp" + fi + ''; in { config = mkIf (cfg.enable && ui.enable) { @@ -607,9 +689,28 @@ in surmount.managementUi.acme.enable is true but incomplete: require non-empty acme.directory, acme.email, acme.domains, acme.accountCredentialsPath, listenMode=https, and tlsCertPath/tlsKeyPath (write targets for PEMs). - Default is acme.enable=false. Prefer staging directory first. Account - credentials and PEMs stay on the host (never in git). HTTP-01 on - product :80 is parked; only dns-01 is wired. + Default is acme.enable=false. Account credentials and PEMs stay on + the host (never in git). http-01 renews on this host. dns-01 still + uses the operator DNS hook. Namecheap API credentials are not a + module input. + ''; + } + { + assertion = + !http01Renew + || ( + ui.acme.dnsProvider == "none" + && redirectListenActive + && hostPaths.strictHostPath effectiveHttp01Webroot + && hostPaths.strictHostPath ui.tlsCertPath + && hostPaths.strictHostPath ui.tlsKeyPath + ); + message = '' + surmount.managementUi.acme.challenge is http-01 but the host renew + path is incomplete. Require dnsProvider = "none" (no Namecheap hook + on this host), redirectHttpToHttps with a listen address so port 80 + can serve /.well-known/acme-challenge/, and strict absolute paths for + the HTTP-01 webroot, tlsCertPath, and tlsKeyPath. ''; } { @@ -861,6 +962,50 @@ in serviceConfig = serviceConfig; }; } + // lib.optionalAttrs http01Renew { + surmount-cert-renew = { + description = "Renew the Let's Encrypt certificate this host presents (HTTP-01)"; + after = [ + "network-online.target" + "surmount-management-ui.service" + ]; + wants = [ + "network-online.target" + "surmount-management-ui.service" + ]; + serviceConfig = { + Type = "oneshot"; + User = "surmount-ui"; + Group = "surmount-ui"; + ExecStart = "${certRenewScript}"; + # Privileged prefix so root can restart the UI after a write. + # The ACME client itself stays User=surmount-ui above. + ExecStartPost = "+${certRenewRestartScript}"; + TimeoutStartSec = "15min"; + Nice = 0; + Environment = renewEnvList; + # NoNewPrivileges stays off so ExecStartPost "+" can restart the + # UI as root. The ACME client process is still User=surmount-ui. + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + ReadWritePaths = acmeWritePaths ++ [ effectiveHttp01Webroot ]; + }; + }; + } # Static unit names. Reading config.services.splora.instances while # defining systemd.services recurses. MemoryMax only; Nice stays on # the indexer wrap. The five names are the default indexer units, @@ -868,7 +1013,12 @@ in // lib.optionalAttrs (sploraServiceOn && splora.unitsMemoryMax != null) ( let cap = { - serviceConfig.MemoryMax = splora.unitsMemoryMax; + serviceConfig = { + MemoryMax = splora.unitsMemoryMax; + # Same gate as the memory cap. Does not enable the unit. + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; }; in lib.optionalAttrs (config.services.splora.queueEnable or false) { @@ -886,6 +1036,22 @@ in } ); + systemd.timers = lib.mkIf http01Renew { + surmount-cert-renew = { + description = "Renew the Let's Encrypt certificate this host presents (HTTP-01)"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = [ + "*-*-* 03:17:00" + "*-*-* 15:17:00" + ]; + Persistent = true; + RandomizedDelaySec = "30min"; + Unit = "surmount-cert-renew.service"; + }; + }; + }; + # State dir always (0755 so surmount-ui can traverse to durable Domain B # under stateDir/secrets; H2b). Durable Domain B leaves for runtime secrets # + H-PEM tls/ so UI can read/write after install. When ACME is on, also @@ -927,6 +1093,11 @@ in ++ (lib.optional (builtins.any underDurable acmeParentDirs) "d ${durableRoot} 0755 root root - -"); in rootRules ++ parentRules - ); + ) + ++ lib.optionals http01Renew [ + "d ${effectiveHttp01Webroot} 0750 surmount-ui surmount-ui - -" + "d ${effectiveHttp01Webroot}/.well-known 0750 surmount-ui surmount-ui - -" + "d ${effectiveHttp01Webroot}/.well-known/acme-challenge 0750 surmount-ui surmount-ui - -" + ]; }; } diff --git a/modules/networking.nix b/modules/networking.nix index 96738ba..2eb75a4 100644 --- a/modules/networking.nix +++ b/modules/networking.nix @@ -20,6 +20,10 @@ in config = mkIf cfg.enable { networking.firewall = { enable = mkDefault true; + # Refused connections (not every packet) go to the kernel journal. + # logRefusedPackets stays off: that option is a packet debug flood. + # rejectPackets stays false so a scan does not get an ICMP reply. + logRefusedConnections = mkDefault true; allowedTCPPorts = [ 22 # SSH (consider restrict-to-admin nets later) 25 # SMTP inbound @@ -45,6 +49,12 @@ in # Do not claim host drop works from unit env alone. }; + systemd.services.firewall.serviceConfig = { + SyslogIdentifier = "firewall"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; + # Host name stays in hosts/*/configuration.nix so NixOS VM tests # (nodes.) can set their own without conflicts. }; diff --git a/modules/options.nix b/modules/options.nix index aafd0fa..49206ce 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -292,20 +292,24 @@ in ''; }; - # In-process ACME (DNS-01). Default off. Not ACME-only forever; static PEMs stay. - # HTTP-01 on product :80 parked. external-hook = operator DNS-01 adapter; - # live host LE + hot-reload residual (not a commercial DNS brand lock-in). + # In-process ACME. Default off. Not ACME-only forever; static PEMs stay. + # dns-01 uses an operator hook. http-01 is the surmount-1 renew path + # (timer surmount-cert-renew, webroot on the product :80 listener). + # Namecheap API credentials stay off surmount-1. acme = { enable = mkOption { type = types.bool; default = false; description = '' - Enable in-process ACME issuance inside management-ui (instant-acme, - DNS-01). Default false. When false, listenMode=https requires - existing host PEMs at tlsCertPath/tlsKeyPath. When true, the binary - reuses valid PEMs or attempts issuance (fail-closed on failure; no - silent cleartext). CI never requires live Let's Encrypt. Prefer - staging directory first. See docs/EDGE_AND_TLS.md and docs/OPS.md. + Enable ACME for the management UI (instant-acme). Default false. + When false, listenMode=https requires existing host PEMs at + tlsCertPath/tlsKeyPath. When true with challenge dns-01, the + binary reuses valid PEMs or attempts issuance (fail-closed; no + silent cleartext). When true with challenge http-01, the + long-running process keeps serving unexpired PEMs and the + surmount-cert-renew timer on this host writes new PEMs. + Public module default stays false. CI never requires live + Let's Encrypt. See docs/EDGE_AND_TLS.md and docs/OPS.md. ''; }; @@ -357,15 +361,33 @@ in }; challenge = mkOption { - type = types.enum [ "dns-01" ]; + type = types.enum [ + "dns-01" + "http-01" + ]; default = "dns-01"; description = '' - ACME challenge type. Only dns-01 in this build (fits redirect-only - product :80). HTTP-01 on product :80 is parked. Emitted as + ACME challenge type. dns-01 uses the DNS challenge adapter + (external-hook or mock). http-01 is the surmount-1 renew path: + the plain HTTP listener serves /.well-known/acme-challenge/ + from http01Webroot, and systemd timer surmount-cert-renew runs + the ACME client. http-01 does not use Namecheap. Emitted as SURMOUNT_ACME_CHALLENGE. ''; }; + http01Webroot = mkOption { + type = types.str; + default = ""; + example = "/var/lib/surmount/acme-http01"; + description = '' + Absolute host path for HTTP-01 token files. Empty default. + When challenge is http-01 and this is empty, the management-ui + module uses ''${stateDir}/acme-http01. Emitted as + SURMOUNT_ACME_HTTP01_WEBROOT. Not a Namecheap credential path. + ''; + }; + dnsProvider = mkOption { type = types.enum [ "none" @@ -442,8 +464,10 @@ in SURMOUNT_REDIRECT_HTTP_TO_HTTPS. Requires listenMode=https (eval asserts). Product path: web.enable=false. Fail-closed at eval if web.enable is true (nginx dual-run owns public :80/:443 ACME/redirect). - ACME HTTP-01 on the product :80 listener is not implemented (parked; - Q-EDGE / RESIDUAL). + When acme.challenge is http-01, this listener also serves + /.well-known/acme-challenge/ from the HTTP-01 webroot before + the redirect. That is the surmount-1 Let's Encrypt production + renew path. It does not use Namecheap. ''; }; diff --git a/modules/remote-builder.nix b/modules/remote-builder.nix index a8ce283..8c89c57 100644 --- a/modules/remote-builder.nix +++ b/modules/remote-builder.nix @@ -105,7 +105,12 @@ in systemd.services.stalwart-mail.serviceConfig.OOMScoreAdjust = -300; systemd.services.surmount-management-ui.serviceConfig.OOMScoreAdjust = -300; systemd.services.sshd.serviceConfig.OOMScoreAdjust = -300; - systemd.services.surmount-arti-hidden-service.serviceConfig.OOMScoreAdjust = -300; + # Do not instantiate the daemon unit when startDaemon is false. + # A bare serviceConfig assignment would create it with enable defaulting + # to true. OOM protection still applies when the daemon is requested. + systemd.services.surmount-arti-hidden-service = mkIf cfg.artiHiddenService.startDaemon { + serviceConfig.OOMScoreAdjust = -300; + }; systemd.slices.${sliceName} = { description = "Surmount ssh-ng builder (hard memory cap)"; diff --git a/modules/scram.nix b/modules/scram.nix index 4249cca..a534376 100644 --- a/modules/scram.nix +++ b/modules/scram.nix @@ -29,8 +29,38 @@ in # CPUSchedulingPolicy to fifo or rr, and do not set realtime I/O. Nice = -20; OOMScoreAdjust = -1000; - # Watchdog must be able to kill other uids. + # Watchdog must be able to kill other uids. Do not set PrivateUsers, + # ProcSubset, or ProtectProc: those hide other processes or block + # cross-uid SIGKILL. /proc/meminfo and systemctl kill stay available. User = "root"; + NoNewPrivileges = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHome = true; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectHostname = true; + ProtectClock = true; + ProtectControlGroups = true; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + # systemctl kill talks to the private socket. libc kill needs no socket. + RestrictAddressFamilies = [ "AF_UNIX" ]; + UMask = "0077"; + # stderr plan and kill lines (no secret values). Default per-unit + # burst is 10000/30s; match the journal cap so a flood does not + # drop the first kill line. + SyslogIdentifier = "surmount-scram"; + StandardOutput = "journal"; + StandardError = "journal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; }; }; diff --git a/modules/secrets.nix b/modules/secrets.nix index 2e0bf9f..e3aa6b2 100644 --- a/modules/secrets.nix +++ b/modules/secrets.nix @@ -137,6 +137,26 @@ in Type = "oneshot"; RemainAfterExit = true; ExecStart = "${failLoudScript}"; + # Root so a mode 0600 host file is visible. ProtectHome stays off + # so a required path under a home directory is not a false miss. + # The script prints the path kind, never file contents. + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictAddressFamilies = [ "AF_UNIX" ]; + SystemCallArchitectures = "native"; + SyslogIdentifier = "surmount-deploy-secrets-check"; + StandardOutput = "journal"; + StandardError = "journal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; }; }; }) diff --git a/modules/speedtest.nix b/modules/speedtest.nix index c2dda21..cd24b8c 100644 --- a/modules/speedtest.nix +++ b/modules/speedtest.nix @@ -71,13 +71,37 @@ in default = null; description = '' If set, append each JSON result to this path as well as the journal. - The parent directory must exist and be writable by root. + The parent directory must exist and be writable by root, and the + path must not be under /home or /root (ProtectHome is on). When + this is null the oneshot uses DynamicUser instead of root. ''; }; }; }; config = mkMerge [ + # ProtectHome is on for the oneshot. A log path under /home or /root + # would be a false miss or a root write into a home directory. + (mkIf (cfg.periodic.logFile != null) { + assertions = [ + { + assertion = + let + p = cfg.periodic.logFile; + in + lib.hasPrefix "/" p + && p != "/home" + && p != "/root" + && !(lib.hasPrefix "/home/" p) + && !(lib.hasPrefix "/root/" p) + && !(lib.hasInfix ".." p); + message = '' + surmount.speedtest.periodic.logFile must be an absolute path + and must not be under /home or /root. + ''; + } + ]; + }) (mkIf cfg.enable { # Wrapper getName is speedtest; upstream attr is ookla-speedtest. nixpkgs.config.allowUnfreePredicate = @@ -93,8 +117,34 @@ in description = "Ookla Speedtest JSON probe"; serviceConfig = { Type = "oneshot"; - User = "root"; - }; + # JSON on stdout is the probe result, not a secret. Keep it in + # the journal under a stable identifier. Burst matches journald + # so one large result is not the line that gets suppressed. + SyslogIdentifier = "surmount-speedtest"; + StandardOutput = "journal"; + StandardError = "journal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictNamespaces = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + # Outbound probe plus resolved on the unix socket. + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + } + // (if cfg.periodic.logFile == null then { DynamicUser = true; } else { User = "root"; }); script = '' set -euo pipefail ${lib.escapeShellArg speedtestBin} ${escapeShellArgs jsonArgs} ${ diff --git a/modules/splora-indexer.nix b/modules/splora-indexer.nix index 78b48f4..af50467 100644 --- a/modules/splora-indexer.nix +++ b/modules/splora-indexer.nix @@ -90,12 +90,22 @@ in mkMerge [ (mkIf on { "splora-${name}" = { - serviceConfig.Nice = 5; + serviceConfig = { + Nice = 5; + SyslogIdentifier = "splora-${name}"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; }; }) (mkIf (on && (inst.startLocalDaemon or false)) { ${daemonName} = { - serviceConfig.Nice = 5; + serviceConfig = { + Nice = 5; + SyslogIdentifier = daemonName; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; }; }) ] @@ -103,27 +113,25 @@ in ); }) ] - ++ optionals hasSplora ( - [ - (mkIf (cfg.enable && idx.enable) { - services.splora.enable = true; - services.splora.instances = mkMerge ( - map (name: { - ${name} = mkIf (idx.instanceName == name) { - enable = true; - network = idx.network; - daemonRpcAddr = idx.daemonRpcAddr; - cookieFile = idx.cookieFile; - daemonDir = idx.daemonDir; - jsonrpcImport = idx.jsonrpcImport; - publicHealth = idx.publicHealth; - dbBlockCacheMb = idx.dbBlockCacheMb; - extraArgs = idx.extraArgs; - }; - }) allowedNames - ); - }) - ] - ) + ++ optionals hasSplora ([ + (mkIf (cfg.enable && idx.enable) { + services.splora.enable = true; + services.splora.instances = mkMerge ( + map (name: { + ${name} = mkIf (idx.instanceName == name) { + enable = true; + network = idx.network; + daemonRpcAddr = idx.daemonRpcAddr; + cookieFile = idx.cookieFile; + daemonDir = idx.daemonDir; + jsonrpcImport = idx.jsonrpcImport; + publicHealth = idx.publicHealth; + dbBlockCacheMb = idx.dbBlockCacheMb; + extraArgs = idx.extraArgs; + }; + }) allowedNames + ); + }) + ]) ); } diff --git a/modules/stalwart-service.nix b/modules/stalwart-service.nix index a39a2f5..069d41c 100644 --- a/modules/stalwart-service.nix +++ b/modules/stalwart-service.nix @@ -279,6 +279,10 @@ in SyslogIdentifier = "stalwart-mail"; StandardOutput = "journal"; StandardError = "journal"; + # RUST_LOG stays info (no message bodies). Raise the per-unit + # burst so auth and SMTP info lines are not dropped at 10000/30s. + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials; ReadWritePaths = [ cfg.dataDir ]; diff --git a/modules/vaultwarden.nix b/modules/vaultwarden.nix index 644f807..bb8b79a 100644 --- a/modules/vaultwarden.nix +++ b/modules/vaultwarden.nix @@ -171,6 +171,13 @@ in # Align restart with other Surmount units (stock is Restart=always). Restart = lib.mkDefault "on-failure"; RestartSec = lib.mkDefault "5s"; + # LOG_LEVEL stays info. debug and trace can include request secrets. + # EXTENDED_LOGGING already records URI and status. No LOG_FILE. + SyslogIdentifier = "vaultwarden"; + StandardOutput = "journal"; + StandardError = "journal"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; }; }; diff --git a/modules/web.nix b/modules/web.nix index fc02c9b..c62daa6 100644 --- a/modules/web.nix +++ b/modules/web.nix @@ -24,6 +24,20 @@ let cfg = config.surmount; inherit (lib) mkIf mkMerge mkDefault; uiUpstream = "${cfg.managementUi.listenAddress}:${toString cfg.managementUi.port}"; + # enableACME on these vhosts is what creates the nixpkgs ACME units. + acmeCertNames = lib.unique [ + cfg.servicesHostname + cfg.mailHostname + cfg.primaryDomain + "www.${cfg.primaryDomain}" + ]; + acmeJournal = name: { + serviceConfig = { + SyslogIdentifier = name; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; + }; in { config = mkIf (cfg.enable && cfg.web.enable) (mkMerge [ @@ -42,6 +56,9 @@ in # Mild request-size guard; mail blobs go through Stalwart, not nginx. clientMaxBodySize = mkDefault "25m"; + # Default error_log level is error. notice adds TLS and worker + # notices without debug buffers (those can include certificate bytes). + logError = "stderr notice"; virtualHosts = { # Primary management UI @@ -55,17 +72,9 @@ in proxy_read_timeout 120s; ''; }; - # Optional path to Stalwart native admin (temporary fallback). - # Stalwart 0.16 first-boot default HTTP listener is :8080 (all - # interfaces). Prefer SSH tunnel and/or rebind to loopback via - # WebUI / stalwart-cli apply before exposing this path publicly. - locations."/stalwart-admin/" = { - proxyPass = "http://127.0.0.1:8080/"; - extraConfig = '' - # Bootstrap only. Consider removing once management-ui covers ops. - proxy_set_header Host $host; - ''; - }; + # No public /stalwart-admin/ proxy. That path published loopback + # Stalwart HTTP on the services vhost. Bootstrap is an SSH tunnel + # to the loopback listener, not a clearnet location. }; # Mail hostname: ACME cert useful for Stalwart TLS file paths later. @@ -93,6 +102,32 @@ in } // cfg.web.extraVhosts; }; + + # Journal identity for the transitional edge and for the ACME units + # this escape creates (acme-, acme-order-renew-). The + # renew timer is acme-renew- and has no exec log of its own. + # In-process host ACME stays off. web.enable stays the gate. + systemd.services = lib.mkMerge [ + { + nginx.serviceConfig = { + SyslogIdentifier = "nginx"; + LogRateLimitIntervalSec = "30s"; + LogRateLimitBurst = 50000; + }; + } + (lib.listToAttrs ( + map (cert: { + name = "acme-${cert}"; + value = acmeJournal "acme-${cert}"; + }) acmeCertNames + )) + (lib.listToAttrs ( + map (cert: { + name = "acme-order-renew-${cert}"; + value = acmeJournal "acme-order-renew-${cert}"; + }) acmeCertNames + )) + ]; } ]); } diff --git a/nix/packages/bitcoind-mutinynet-signet-spacing.patch b/nix/packages/bitcoind-mutinynet-signet-spacing.patch new file mode 100644 index 0000000..6198dfd --- /dev/null +++ b/nix/packages/bitcoind-mutinynet-signet-spacing.patch @@ -0,0 +1,11 @@ +--- a/src/kernel/chainparams.cpp ++++ b/src/kernel/chainparams.cpp +@@ -511,7 +511,7 @@ + consensus.CSVHeight = 1; + consensus.SegwitHeight = 1; + consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks +- consensus.nPowTargetSpacing = 10 * 60; ++ consensus.nPowTargetSpacing = 30; + consensus.fPowAllowMinDifficultyBlocks = false; + consensus.enforce_BIP94 = false; + consensus.fPowNoRetargeting = false; diff --git a/nix/packages/bitcoind-mutinynet.nix b/nix/packages/bitcoind-mutinynet.nix new file mode 100644 index 0000000..f52daef --- /dev/null +++ b/nix/packages/bitcoind-mutinynet.nix @@ -0,0 +1,24 @@ +# Mutinynet-only Bitcoin Core. Same upstream version as pkgs.bitcoind. +# Signet proof-of-work target spacing is 30 seconds in this package only. +# SigNetParams is shared by every custom signet inside one binary, so this +# package must not replace the system bitcoind that testnet3 and testnet4 run. +# Stock signet spacing stays 10 * 60 in pkgs.bitcoind. +{ + bitcoind, + lib, +}: +assert lib.assertMsg ( + bitcoind.version == "32.1" +) "bitcoind-mutinynet stays on Bitcoin Core 32.1 (got ${bitcoind.version})"; +bitcoind.overrideAttrs (old: { + pname = "bitcoind-mutinynet"; + patches = (old.patches or [ ]) ++ [ + ./bitcoind-mutinynet-signet-spacing.patch + ]; + # nixpkgs bitcoind sets doCheck. Those tests lock stock signet spacing + # and take hours. This package is the spacing change. Skip that suite. + doCheck = false; + meta = (old.meta or { }) // { + description = "Bitcoin Core 32.1 with mutinynet signet target spacing of 30 seconds"; + }; +}) diff --git a/nix/packages/stalwart-cli.nix b/nix/packages/stalwart-cli.nix index 66b248a..cb5d5c3 100644 --- a/nix/packages/stalwart-cli.nix +++ b/nix/packages/stalwart-cli.nix @@ -17,17 +17,20 @@ }: let - version = "1.0.13"; + # Checked 2026-11-03: GitHub releases/latest is v1.0.14 + # (published 2026-10-25T18:15:42Z, not a prerelease). + # Directory names inside the gnu tarballs are unchanged. + version = "1.0.14"; sources = { x86_64-linux = { url = "https://github.com/stalwartlabs/cli/releases/download/v${version}/stalwart-cli-x86_64-unknown-linux-gnu.tar.xz"; - hash = "sha256-G4UJt2ft0aF2k+CStRjEFhDsS3JPTmLEYb0ozUDGafc="; + hash = "sha256-eFhLI4QisCAae9Ij7ZWzwaf7hzWtP6jIhNgYpm/RvGU="; dir = "stalwart-cli-x86_64-unknown-linux-gnu"; }; aarch64-linux = { url = "https://github.com/stalwartlabs/cli/releases/download/v${version}/stalwart-cli-aarch64-unknown-linux-gnu.tar.xz"; - hash = "sha256-ZfS25SjYQ4iVP8mNlGM53YJG+wO/TKqmTh2bN49S2JI="; + hash = "sha256-ow8MGg+4p+6LxC/Q5pmALkxRJXpT29aBxNkd22dZeSs="; dir = "stalwart-cli-aarch64-unknown-linux-gnu"; }; }; diff --git a/nix/packages/stalwart-mail.nix b/nix/packages/stalwart-mail.nix index c8d0e9f..2de040e 100644 --- a/nix/packages/stalwart-mail.nix +++ b/nix/packages/stalwart-mail.nix @@ -41,21 +41,23 @@ }: let - # Checked 2026-10-17: GitHub tag v0.16.26 (released 2026-10-11). - # v0.16.25 and v0.16.26 say an upgrade from 0.16.x replaces the binary. - # Those notes do not change the settings schema. UPGRADING/v0_16.md is - # the 0.15-and-below migration, which this pin already passed. - version = "0.16.26"; + # Checked 2026-11-03: GitHub releases/latest is still v1.0.2 + # (published 2026-11-02T15:42:44Z, not a prerelease). Asset names are + # unchanged. The gnu tarball still contains one binary named stalwart. + # The v1.0.2 notes replace Email.encryptAtRest with Encryption.atRestEnable + # and move encryptOnAppend onto the account. This file does not generate + # those settings. modules/stalwart-service.nix writes DataStore config.json only. + version = "1.0.2"; # Release asset hashes (gnu libc). musl variants exist upstream if needed. sources = { x86_64-linux = fetchurl { url = "https://github.com/stalwartlabs/stalwart/releases/download/v${version}/stalwart-x86_64-unknown-linux-gnu.tar.gz"; - hash = "sha256-7VCV//2Lat342EJMBqV0lIFycW4F9S85Td/vP8K9IhE="; + hash = "sha256-EL03XUVB4v+6/f1dl9iqGlwYhDj9hwD71vB27g/C8k8="; }; aarch64-linux = fetchurl { url = "https://github.com/stalwartlabs/stalwart/releases/download/v${version}/stalwart-aarch64-unknown-linux-gnu.tar.gz"; - hash = "sha256-uqWBIx97SlO/7pAPfhhrXPRgWHRVimKYPjiI0McPp8k="; + hash = "sha256-eYZKz9BV2PulLmNs3kghRTZpoMwiKXWmB9OSdAvo/a4="; }; }; diff --git a/nix/packages/stalwart-spam-filter.nix b/nix/packages/stalwart-spam-filter.nix index 9924d93..4d174e9 100644 --- a/nix/packages/stalwart-spam-filter.nix +++ b/nix/packages/stalwart-spam-filter.nix @@ -1,9 +1,13 @@ -# Pinned spam-filter rules for Stalwart (FOD release assets). +# Pinned spam-filter rules for Stalwart (FOD release asset). # Avoids impure GitHub downloads at runtime when the operator points -# SpamSettings at these file:// paths via WebUI / stalwart-cli apply. +# SpamSettings at this file:// path via WebUI / stalwart-cli apply. # -# Bump: update version + re-prefetch both hashes: -# nix store prefetch-file https://github.com/stalwartlabs/spam-filter/releases/download/vX.Y.Z/spam-filter.toml +# Checked 2026-11-03: GitHub releases/latest is v3.0.4 +# (published 2026-11-01T15:50:40Z). v3.0.2 still published spam-filter.toml. +# v3.0.3 and v3.0.4 publish only spam-filter-rules.json.gz. +# Do not invent a toml URL for a tag that does not ship one. +# +# Bump: update version + re-prefetch: # nix store prefetch-file https://github.com/stalwartlabs/spam-filter/releases/download/vX.Y.Z/spam-filter-rules.json.gz { @@ -14,16 +18,11 @@ stdenvNoCC.mkDerivation (finalAttrs: { pname = "stalwart-spam-filter"; - version = "3.0.2"; - - srcToml = fetchurl { - url = "https://github.com/stalwartlabs/spam-filter/releases/download/v${finalAttrs.version}/spam-filter.toml"; - hash = "sha256-MridVW9VRZkg+tLL+pS+Tmq4ObB22ptwObGvRF0RDnw="; - }; + version = "3.0.4"; - srcRules = fetchurl { + src = fetchurl { url = "https://github.com/stalwartlabs/spam-filter/releases/download/v${finalAttrs.version}/spam-filter-rules.json.gz"; - hash = "sha256-HTpm9vqaMvcRxbOnOFNmPv8nyZSkbo9qDbZKmypYD1I="; + hash = "sha256-YEzEOGsVdOlRhdN8ucGUrCDTYMyjhllgtu0O33BHVWI="; }; dontUnpack = true; @@ -32,8 +31,7 @@ stdenvNoCC.mkDerivation (finalAttrs: { installPhase = '' runHook preInstall mkdir -p $out - cp ${finalAttrs.srcToml} $out/spam-filter.toml - cp ${finalAttrs.srcRules} $out/spam-filter-rules.json.gz + cp ${finalAttrs.src} $out/spam-filter-rules.json.gz runHook postInstall ''; diff --git a/nix/packages/stalwart-webui.nix b/nix/packages/stalwart-webui.nix index 84d41a2..ed735b7 100644 --- a/nix/packages/stalwart-webui.nix +++ b/nix/packages/stalwart-webui.nix @@ -14,11 +14,13 @@ stdenvNoCC.mkDerivation (finalAttrs: { pname = "stalwart-webui"; - version = "1.0.11"; + # Checked 2026-11-03: GitHub releases/latest is still v2.0.4 + # (published 2026-11-02T11:19:17Z). Asset name is still webui.zip. + version = "2.0.4"; src = fetchurl { url = "https://github.com/stalwartlabs/webui/releases/download/v${finalAttrs.version}/webui.zip"; - hash = "sha256-1n5oyroCAkMBx52p8V/kZd6b9Tgs7lS9+cMJqXtBeaU="; + hash = "sha256-cni+Zwh0Y+ARq/QBeKL/Bz0Bu/AuYq0qP/BENRQAIrA="; }; dontUnpack = true; diff --git a/nix/packages/surmount-rekey.nix b/nix/packages/surmount-rekey.nix index bf8521d..462d1a3 100644 --- a/nix/packages/surmount-rekey.nix +++ b/nix/packages/surmount-rekey.nix @@ -1,5 +1,6 @@ # Hermetic crane build of crates/surmount-rekey (just rekey). -# age crate (rage library) plus gpg on PATH. Not writeShellApplication. +# X25519 age identity (x25519-dalek + bech32) plus gpg on PATH. +# Not the age crate. Not writeShellApplication. # # Call as: pkgs.callPackage ./surmount-rekey.nix { inherit craneLib; } diff --git a/nix/packages/vandelay.nix b/nix/packages/vandelay.nix index bf06599..701c3b4 100644 --- a/nix/packages/vandelay.nix +++ b/nix/packages/vandelay.nix @@ -7,10 +7,10 @@ # # Packaging mode: pinned upstream release binary FODs (same pattern as # stalwart-cli). Bump: set version, prefetch each arch tar.gz, paste hashes. -# 1.0.12 (2026-10-11): parallel Email/import batches, keep non-selectable -# IMAP parents, and stop holding a whole JMAP page of blobs in memory. -# https://github.com/stalwartlabs/vandelay/releases/tag/v1.0.12 -# (accessed: 2026-10-17). Asset names are unchanged. +# Checked 2026-11-03: GitHub releases/latest is v1.0.14 +# (published 2026-11-01T16:43:01Z, not a prerelease). +# https://github.com/stalwartlabs/vandelay/releases/tag/v1.0.14 +# (accessed: 2026-11-03). Asset names and tarball directory names are unchanged. { lib, @@ -22,17 +22,17 @@ }: let - version = "1.0.12"; + version = "1.0.14"; sources = { x86_64-linux = { url = "https://github.com/stalwartlabs/vandelay/releases/download/v${version}/vandelay-x86_64-unknown-linux-gnu.tar.gz"; - hash = "sha256-4hLrj3B4AUmjN8IGi8qiEsPtRwxkbLVxO2qXD/J+nZ8="; + hash = "sha256-nP6wUyG3PVzRPaxS0LGCiXiexuJMhpz1ljUc7KhbLUQ="; dir = "vandelay-x86_64-unknown-linux-gnu"; }; aarch64-linux = { url = "https://github.com/stalwartlabs/vandelay/releases/download/v${version}/vandelay-aarch64-unknown-linux-gnu.tar.gz"; - hash = "sha256-ToazZRZnBi4ZGcyDrhVrQdeu/S2s8My7OAwtAGNM7j0="; + hash = "sha256-9mEbJbfntOmg0L1woUKI7ukr6VjpxElKUofJDJKdIAQ="; dir = "vandelay-aarch64-unknown-linux-gnu"; }; }; diff --git a/script/laptop-renew/surmount-laptop-renew-cert.service b/script/laptop-renew/surmount-laptop-renew-cert.service index 3d02ce0..1e33a02 100644 --- a/script/laptop-renew/surmount-laptop-renew-cert.service +++ b/script/laptop-renew/surmount-laptop-renew-cert.service @@ -6,6 +6,11 @@ Wants=network-online.target [Service] Type=oneshot +# Issuance lines go to the user journal. The script must not print env +# files or PEM bodies. Host ACME stays off. This file is still an example. +StandardOutput=journal +StandardError=journal +SyslogIdentifier=surmount-laptop-renew-cert # Copy this unit to the laptop user systemd dir, then fill ExecStart via: # just laptop-renew-cert -- --install-timer --directory production --target root@YOUR_HOST # Namecheap env stays on the laptop. Do not install this on the VPS. diff --git a/script/laptop-renew/surmount-laptop-renew-cert.timer b/script/laptop-renew/surmount-laptop-renew-cert.timer index 3977555..4f4a5e5 100644 --- a/script/laptop-renew/surmount-laptop-renew-cert.timer +++ b/script/laptop-renew/surmount-laptop-renew-cert.timer @@ -5,6 +5,7 @@ Description=Weekly Surmount laptop Let's Encrypt renew (host ACME stays off) OnCalendar=weekly Persistent=true RandomizedDelaySec=1h +Unit=surmount-laptop-renew-cert.service [Install] WantedBy=timers.target diff --git a/tests/fail2ban-permanent-eval.nix b/tests/fail2ban-permanent-eval.nix new file mode 100644 index 0000000..80aca65 --- /dev/null +++ b/tests/fail2ban-permanent-eval.nix @@ -0,0 +1,150 @@ +# Hermetic contract: sshd fail2ban ban is permanent after 3 failures, +# and the ban action is the one this host's firewall applies. +# Host firewall is modules/networking.nix: networking.firewall on, +# networking.nftables.enable left false, so the backend is iptables. +# Do not enable the management UI nft helper. Do not add jails. +# Never embeds host SKUs, addresses, RAM, or guest disk sizes. +let + flake = builtins.getFlake (toString ./..); + system = builtins.currentSystem; + inherit (flake.inputs.nixpkgs) lib; + evalHardening = + extraModules: + lib.nixosSystem { + inherit system; + modules = [ + ../modules/options.nix + ../modules/hardening.nix + ../modules/networking.nix + { + system.stateVersion = "26.05"; + networking.hostName = "fail2ban-permanent-eval"; + fileSystems."/" = { + device = "nodev"; + fsType = "ext4"; + }; + boot.loader.grub.enable = false; + surmount = { + enable = true; + hardening.enable = true; + }; + } + ] + ++ extraModules; + }; + failedAssertions = e: builtins.filter (a: !a.assertion) e.config.assertions; + + # Product jail is sshd. DEFAULT is the fail2ban defaults section the + # NixOS module always writes (bantime, maxretry, banaction). It is + # not a second jail we added. + wantJails = [ + "DEFAULT" + "sshd" + ]; + + checkHostFirewall = + let + e = evalHardening [ ]; + f2b = e.config.services.fail2ban; + defs = f2b.jails.DEFAULT.settings; + sshd = f2b.jails.sshd.settings; + fw = e.config.networking.firewall; + jailNames = builtins.attrNames f2b.jails; + in + assert failedAssertions e == [ ]; + assert lib.assertMsg (f2b.maxretry == 3) "maxretry is ${toString f2b.maxretry}, want 3"; + assert lib.assertMsg (f2b.bantime == "-1") "bantime is ${f2b.bantime}, want -1 (permanent)"; + assert lib.assertMsg (defs.maxretry == 3) "DEFAULT maxretry is ${toString defs.maxretry}, want 3"; + assert lib.assertMsg ( + defs.bantime == "-1" + ) "DEFAULT bantime is ${defs.bantime}, want -1 (permanent)"; + assert lib.assertMsg (!(sshd ? maxretry)) "sshd jail must inherit maxretry, not override it"; + assert lib.assertMsg (!(sshd ? bantime)) "sshd jail must inherit bantime, not override it"; + assert lib.assertMsg (f2b.enable == true) "fail2ban must be enabled"; + assert lib.assertMsg (sshd.enabled == true) "sshd jail must be enabled"; + assert lib.assertMsg (sshd.filter == "sshd") "sshd jail filter must be sshd"; + assert lib.assertMsg (sshd.backend == "systemd") "sshd jail backend must be systemd"; + assert lib.assertMsg ( + jailNames == wantJails + ) "jails are ${builtins.concatStringsSep "," jailNames}, want DEFAULT,sshd"; + assert lib.assertMsg (fw.enable == true) "host firewall must be enabled"; + assert lib.assertMsg ( + e.config.networking.nftables.enable == false + ) "host firewall must not turn on native nftables"; + assert lib.assertMsg (fw.backend == "iptables") "firewall backend is ${fw.backend}, want iptables"; + assert lib.assertMsg ( + f2b.banaction == "iptables-multiport" + ) "banaction is ${f2b.banaction}, want iptables-multiport"; + assert lib.assertMsg ( + defs.banaction == "iptables-multiport" + ) "DEFAULT banaction is ${defs.banaction}, want iptables-multiport"; + assert lib.assertMsg (!(sshd ? action)) "sshd jail must not override action"; + assert lib.assertMsg (!(sshd ? banaction)) "sshd jail must not override banaction"; + assert lib.assertMsg ( + f2b.packageFirewall == fw.package + ) "fail2ban firewall package must be the host firewall package"; + assert lib.assertMsg ( + fw.package.pname == "iptables" + ) "firewall package is ${fw.package.pname}, want iptables"; + assert lib.assertMsg (builtins.elem "firewall.service" e.config.systemd.services.fail2ban.partOf) + "fail2ban must be part of the host firewall unit"; + assert lib.assertMsg (f2b.bantime-increment.enable == false) "bantime increment must stay off"; + assert lib.assertMsg ( + (f2b.daemonSettings.Definition.dbfile or "") == "/var/lib/fail2ban/fail2ban.sqlite3" + ) "permanent bans need the fail2ban sqlite db"; + assert lib.assertMsg (e.config.surmount.accessControl.nftHelper == false) "nftHelper must stay off"; + assert lib.assertMsg ( + e.config.surmount.accessControl.enable == false + ) "accessControl must stay off"; + "host-firewall-permanent-sshd-ban-ok"; + + # If native nftables is turned on, ip_tables is blacklisted. The ban + # action must switch to nftables-multiport or the ban never installs. + # This check forces that in the eval only. It does not enable nftHelper + # and it does not add a jail. + checkNftablesSwitch = + let + e = evalHardening [ + { networking.nftables.enable = true; } + ]; + f2b = e.config.services.fail2ban; + fw = e.config.networking.firewall; + in + assert failedAssertions e == [ ]; + assert lib.assertMsg ( + f2b.maxretry == 3 + ) "nftables-switch maxretry is ${toString f2b.maxretry}, want 3"; + assert lib.assertMsg (f2b.bantime == "-1") "nftables-switch bantime is ${f2b.bantime}, want -1"; + assert lib.assertMsg ( + e.config.networking.nftables.enable == true + ) "nftables switch eval must enable nftables"; + assert lib.assertMsg ( + fw.backend == "nftables" + ) "nftables switch backend is ${fw.backend}, want nftables"; + assert lib.assertMsg ( + f2b.banaction == "nftables-multiport" + ) "nftables switch banaction is ${f2b.banaction}, want nftables-multiport"; + assert lib.assertMsg ( + f2b.jails.DEFAULT.settings.banaction == "nftables-multiport" + ) "nftables switch DEFAULT banaction drifted"; + assert lib.assertMsg (!(f2b.jails.sshd.settings ? action)) "sshd jail must not pin an action"; + assert lib.assertMsg ( + f2b.packageFirewall == fw.package + ) "nftables switch packageFirewall must match the firewall package"; + assert lib.assertMsg ( + fw.package.pname == "nftables" + ) "nftables switch package is ${fw.package.pname}, want nftables"; + assert lib.assertMsg (builtins.elem "ip_tables" e.config.boot.blacklistedKernelModules) + "nftables firewall must blacklist ip_tables, so an iptables ban action cannot install"; + assert lib.assertMsg ( + e.config.surmount.accessControl.nftHelper == false + ) "nftHelper must stay off when nftables is on"; + assert lib.assertMsg ( + builtins.attrNames f2b.jails == wantJails + ) "nftables switch must not add jails"; + "nftables-switch-permanent-sshd-ban-ok"; +in +{ + host-firewall-permanent-sshd-ban = checkHostFirewall; + nftables-switch-permanent-sshd-ban = checkNftablesSwitch; +} diff --git a/tests/host-cert-renew-eval.nix b/tests/host-cert-renew-eval.nix new file mode 100644 index 0000000..afcb2e9 --- /dev/null +++ b/tests/host-cert-renew-eval.nix @@ -0,0 +1,238 @@ +# Hermetic eval: host Let's Encrypt renewal is a unit on the host when +# the host renewal switch is on. Standalone. Do not import this from +# tests/module-eval.nix. +# +# nix eval --impure --json --file tests/host-cert-renew-eval.nix +# +# Public module default stays off. This file turns the switch on in one +# eval and leaves it off in the other. example.test names only. +let + flake = builtins.getFlake (toString ./..); + system = builtins.currentSystem; + inherit (flake.inputs.nixpkgs) lib; + pkgs = flake.inputs.nixpkgs.legacyPackages.${system}; + fakeUi = pkgs.writeShellScriptBin "surmount-management-ui" "exit 0"; + evalHost = + extra: + lib.nixosSystem { + inherit system; + modules = [ + ../modules/options.nix + ../modules/management-ui.nix + { + nixpkgs.overlays = [ + (_final: prev: { + surmount-management-ui = fakeUi; + acme-dns-hook-namecheap = prev.writeShellScriptBin "acme-dns-hook-namecheap" "exit 0"; + }) + ]; + system.stateVersion = "26.05"; + networking.hostName = "cert-renew-eval"; + fileSystems."/" = { + device = "nodev"; + fsType = "ext4"; + }; + boot.loader.grub.enable = false; + surmount = extra // { + enable = true; + }; + } + ]; + }; + failedAssertions = e: builtins.filter (a: !a.assertion) e.config.assertions; + envText = + e: + let + env = e.config.systemd.services.surmount-management-ui.serviceConfig.Environment or [ ]; + in + if builtins.isList env then lib.concatStringsSep "\n" env else toString env; + timerWanted = + e: + e.config.systemd.timers ? surmount-cert-renew + && builtins.elem "timers.target" (e.config.systemd.timers.surmount-cert-renew.wantedBy or [ ]); + + # Switch off: no renew unit is wanted. Public default stays off. + host-cert-renew-off-has-no-wanted-timer = + let + e = evalHost { + managementUi.listenMode = "https"; + managementUi.listenAddress = "127.0.0.1"; + managementUi.port = 443; + managementUi.tlsCertPath = "/var/lib/surmount/secrets/tls/cert.pem"; + managementUi.tlsKeyPath = "/var/lib/surmount/secrets/tls/key.pem"; + managementUi.acme.enable = false; + }; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.managementUi.acme.enable == false; + assert !(timerWanted e); + "host-cert-renew-off-has-no-wanted-timer-ok"; + + # Switch on, HTTP-01, Let's Encrypt production. The renew timer is wanted. + host-cert-renew-on-wants-timer = + let + e = evalHost { + managementUi.listenMode = "https"; + managementUi.listenAddress = "127.0.0.1"; + managementUi.port = 443; + managementUi.redirectHttpToHttps = true; + managementUi.httpRedirectListen = "0.0.0.0:80"; + managementUi.tlsCertPath = "/var/lib/surmount/secrets/tls/cert.pem"; + managementUi.tlsKeyPath = "/var/lib/surmount/secrets/tls/key.pem"; + managementUi.acme.enable = true; + managementUi.acme.challenge = "http-01"; + managementUi.acme.directory = "https://acme-v02.api.letsencrypt.org/directory"; + managementUi.acme.email = "ops@example.test"; + managementUi.acme.domains = [ + "services.example.test" + "mail.example.test" + ]; + managementUi.acme.accountCredentialsPath = "/var/lib/surmount/secrets/acme/account.json"; + managementUi.acme.dnsProvider = "none"; + }; + env = envText e; + svc = e.config.systemd.services.surmount-cert-renew or { }; + exec = svc.serviceConfig.ExecStart or ""; + script = if builtins.isList exec then lib.concatStringsSep " " exec else toString exec; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.managementUi.acme.enable == true; + assert e.config.surmount.managementUi.acme.challenge == "http-01"; + assert e.config.surmount.managementUi.acme.dnsProvider == "none"; + assert e.config.surmount.vaultwarden.enable == false; + assert e.config.surmount.backups.enable == false; + assert e.config.surmount.lake.enable == false; + assert e.config.surmount.remoteBuilder.enable == false; + assert e.config.surmount.accessControl.nftHelper == false; + assert timerWanted e; + assert e.config.systemd.services ? surmount-cert-renew; + assert lib.hasInfix "http-01" env; + assert lib.hasInfix "https://acme-v02.api.letsencrypt.org/directory" env; + assert lib.hasInfix "SURMOUNT_ACME_HTTP01_WEBROOT=" env; + assert !(lib.hasInfix "namecheap" env); + assert script != ""; + assert lib.hasInfix "surmount-management-ui" (builtins.readFile script); + "host-cert-renew-on-wants-timer-ok"; + + # Nostr is on, the session secret is a host file, and HTTP-01 renew is on. + # The renew unit must not export nostr auth and must not receive that + # file. acme-renew loads ACME and TLS only. A successful write copies + # PEMs for Stalwart and restarts stalwart-mail. status.{primaryDomain} + # stays on the order: measured 2026-11-03, status.surmount.systems + # answers on port 80 at the same address as the apex. + host-cert-renew-nostr-without-session-secret = + let + sessionPath = "/var/lib/surmount/secrets/ui/session-secret"; + e = evalHost { + managementUi.listenMode = "https"; + managementUi.listenAddress = "0.0.0.0"; + managementUi.port = 443; + managementUi.redirectHttpToHttps = true; + managementUi.httpRedirectListen = "0.0.0.0:80"; + managementUi.tlsCertPath = "/var/lib/surmount/secrets/tls/cert.pem"; + managementUi.tlsKeyPath = "/var/lib/surmount/secrets/tls/key.pem"; + managementUi.authMode = "nostr"; + managementUi.sessionSecretPath = sessionPath; + managementUi.acme.enable = true; + managementUi.acme.challenge = "http-01"; + managementUi.acme.directory = "https://acme-v02.api.letsencrypt.org/directory"; + managementUi.acme.email = "ops@example.test"; + managementUi.acme.domains = [ + "services.example.test" + "mail.example.test" + ]; + managementUi.acme.accountCredentialsPath = "/var/lib/surmount/secrets/acme/account.json"; + managementUi.acme.dnsProvider = "none"; + }; + textOf = + v: + if v == null then + "" + else if builtins.isList v then + lib.concatStringsSep "\n" (map toString v) + else + toString v; + renewSvc = e.config.systemd.services.surmount-cert-renew or { }; + uiSvc = e.config.systemd.services.surmount-management-ui.serviceConfig or { }; + renewEnv = textOf (renewSvc.serviceConfig.Environment or [ ]); + uiFile = textOf (uiSvc.EnvironmentFile or null); + renewFile = textOf (renewSvc.serviceConfig.EnvironmentFile or null); + stripPlus = s: if lib.hasPrefix "+" s then lib.removePrefix "+" s else s; + firstToken = + raw: + let + parts = lib.filter (p: p != "") (lib.splitString " " (lib.head (lib.splitString "\n" raw))); + in + if parts == [ ] then "" else stripPlus (lib.head parts); + startPath = firstToken (textOf (renewSvc.serviceConfig.ExecStart or "")); + postPath = firstToken (textOf (renewSvc.serviceConfig.ExecStartPost or "")); + startText = + if startPath != "" && builtins.pathExists startPath then + builtins.readFile startPath + else + "missing-start:${startPath}"; + postText = + if postPath != "" && builtins.pathExists postPath then + builtins.readFile postPath + else + "missing-post:${postPath}"; + misses = lib.filter (m: m != "") [ + (if lib.hasInfix "SURMOUNT_AUTH_MODE=" renewEnv then "renew-env-has-SURMOUNT_AUTH_MODE" else "") + ( + if lib.hasInfix "SURMOUNT_SESSION_SECRET=" renewEnv then + "renew-env-has-SURMOUNT_SESSION_SECRET" + else + "" + ) + (if renewFile != "" then "renew-has-EnvironmentFile" else "") + (if lib.hasInfix sessionPath uiFile then "" else "ui-missing-session-secret-file") + ( + if lib.hasInfix "status.surmount.systems" renewEnv then + "" + else + "renew-missing-status.surmount.systems" + ) + (if lib.hasInfix "services.example.test" renewEnv then "" else "renew-dropped-configured-name") + (if lib.hasInfix "mail.example.test" renewEnv then "" else "renew-dropped-configured-name-mail") + (if lib.hasInfix "acme-renew" startText then "" else "start-script-missing-acme-renew") + (if lib.hasInfix "-m 0640" postText then "" else "restart-script-missing-cert-mode-0640") + (if lib.hasInfix "-m 0600" postText then "" else "restart-script-missing-key-mode-0600") + ( + if lib.hasInfix "stalwart-mail:stalwart-mail" postText then + "" + else + "restart-script-missing-stalwart-owner" + ) + ( + if lib.hasInfix "restart stalwart-mail.service" postText then + "" + else + "restart-script-missing-stalwart-restart" + ) + (if lib.hasInfix "/mail/tls" postText then "" else "restart-script-missing-mail-tls-dir") + ( + if + lib.hasInfix sessionPath renewEnv + || lib.hasInfix sessionPath startText + || lib.hasInfix sessionPath postText + then + "session-secret-path-leaked-into-renew-unit" + else + "" + ) + ]; + in + assert failedAssertions e == [ ]; + assert timerWanted e; + if misses != [ ] then + throw ("host-cert-renew-nostr-without-session-secret misses: " + lib.concatStringsSep ", " misses) + else + "host-cert-renew-nostr-without-session-secret-ok"; +in +{ + ok = [ + host-cert-renew-off-has-no-wanted-timer + host-cert-renew-on-wants-timer + host-cert-renew-nostr-without-session-secret + ]; +} diff --git a/tests/mail-host-rust-cpu.nix b/tests/mail-host-rust-cpu.nix index 1db038d..4e586fa 100644 --- a/tests/mail-host-rust-cpu.nix +++ b/tests/mail-host-rust-cpu.nix @@ -61,9 +61,14 @@ let hookOf = pkg: pkg.mailHostRustCpuHook or ""; usesOf = pkg: pkg.mailHostRustCpuUses or ""; - depsHookOf = pkg: if pkg ? mailHostRustCpuCargoArtifacts then hookOf pkg.mailHostRustCpuCargoArtifacts else ""; + depsHookOf = + pkg: if pkg ? mailHostRustCpuCargoArtifacts then hookOf pkg.mailHostRustCpuCargoArtifacts else ""; - goodHook = text: lib.hasInfix "target-cpu=x86-64-v3" text && lib.hasInfix "RUSTFLAGS" text && lib.hasInfix "CARGO_ENCODED_RUSTFLAGS" text; + goodHook = + text: + lib.hasInfix "target-cpu=x86-64-v3" text + && lib.hasInfix "RUSTFLAGS" text + && lib.hasInfix "CARGO_ENCODED_RUSTFLAGS" text; on = evalRust true; off = evalRust false; diff --git a/tests/module-eval.nix b/tests/module-eval.nix index db6b520..b688865 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -2470,6 +2470,106 @@ let # Null stays null. Int or string both print as the unit-file token. shownNice = n: if n == null then null else toString n; + # A missing key is a failed assert. Do not default the burst to a + # passing number: removing the line must fail this eval. + requireJournalBurst = + name: sc: + if + (sc ? LogRateLimitIntervalSec) + && sc.LogRateLimitIntervalSec == "30s" + && (sc ? LogRateLimitBurst) + && (toString sc.LogRateLimitBurst) == "50000" + then + true + else + throw "journal burst 30s/50000 missing on ${name}"; + + # RestrictAddressFamilies may be a list or one token. + addressFamiliesOf = + sc: + let + fams = sc.RestrictAddressFamilies or null; + in + if fams == null then + [ ] + else if builtins.isList fams then + fams + else + [ fams ]; + + requireAfUnix = + name: sc: + if builtins.elem "AF_UNIX" (addressFamiliesOf sc) then + true + else + throw "RestrictAddressFamilies missing AF_UNIX on ${name}"; + + # Missing key is a failed eval. Do not default a claimed flag to true. + requireSyslog = + name: ident: sc: + if (sc ? SyslogIdentifier) && sc.SyslogIdentifier == ident then + true + else + throw "SyslogIdentifier ${ident} missing on ${name}"; + + requireJournalStreams = + name: sc: + if + (sc ? StandardOutput) + && sc.StandardOutput == "journal" + && (sc ? StandardError) + && sc.StandardError == "journal" + then + true + else + throw "journal stdout and stderr missing on ${name}"; + + requireFlag = + name: key: sc: + if (sc ? ${key}) && sc.${key} == true then true else throw "${key} missing on ${name}"; + + requireEq = + name: key: expected: sc: + if (sc ? ${key}) && sc.${key} == expected then + true + else + throw "${key} on ${name} is not the claimed value"; + + requireFlags = + name: keys: sc: + builtins.all (key: requireFlag name key sc) keys; + + requireAbsent = + name: key: sc: + if sc ? ${key} then throw "${key} must stay unset on ${name}" else true; + + requireFamilies = + name: expected: sc: + let + got = addressFamiliesOf sc; + in + if got == expected then true else throw "RestrictAddressFamilies on ${name} is not the claimed set"; + + # nginx vhost text the dual-run escape actually builds. A location + # key, proxyPass, return, or extraConfig that names the old public + # admin path fails the eval. + nginxMentionsStalwartAdmin = + vhosts: + lib.any ( + vh: + let + locs = vh.locations or { }; + in + lib.any (n: lib.hasInfix "stalwart-admin" n) (builtins.attrNames locs) + || lib.any ( + loc: + lib.hasInfix "stalwart-admin" (toString (loc.extraConfig or "")) + || lib.hasInfix "stalwart-admin" (toString (loc.proxyPass or "")) + || lib.hasInfix "stalwart-admin" (toString (loc.return or "")) + ) (builtins.attrValues locs) + || lib.hasInfix "stalwart-admin" (toString (vh.extraConfig or "")) + ) (builtins.attrValues vhosts); + serviceCpuPolicy = e: name: let @@ -2932,6 +3032,8 @@ let assert lib.hasInfix "RuntimeMaxUse=256M" blob; assert lib.hasInfix "RateLimitIntervalSec=30s" blob; assert lib.hasInfix "RateLimitBurst=50000" blob; + assert lib.hasInfix "MaxRetentionSec=30day" blob; + assert e.config.surmount.logging.maxRetentionSec == "30day"; assert lib.hasInfix "ForwardToSyslog=no" blob; assert (e.config.services.openssh.settings.LogLevel or "") == "VERBOSE"; "t42-journal-persistent-when-logging-on-ok"; @@ -3492,6 +3594,635 @@ let assert builtins.elem "nixos-test" (sysOf mail); "t62-gccarch-feature-ok"; + # Mail sample (hosts/mail-vps/configuration.nix, no host-local). + # Asserts only units this sample instantiates. Default-off units stay + # off here; t64 through t69 turn those gates on in their own evals. + t63-mail-vps-security-logging = + let + e = evalSurmount { + __extraModules = [ ../hosts/mail-vps/configuration.nix ]; + }; + scOf = name: e.config.systemd.services.${name}.serviceConfig; + journal = e.config.services.journald.extraConfig or ""; + ssh = e.config.services.openssh.settings; + f2b = e.config.services.fail2ban.daemonSettings.Definition; + scram = scOf "surmount-scram"; + sshdSc = scOf "sshd"; + etSc = scOf "eternal-terminal"; + mailEnv = e.config.systemd.services.stalwart-mail.environment or { }; + stalwartRustLog = mailEnv.RUST_LOG or ""; + uiEnvRaw = (scOf "surmount-management-ui").Environment; + uiEnvList = if builtins.isList uiEnvRaw then uiEnvRaw else [ uiEnvRaw ]; + sshdNice = sshdSc.Nice or null; + etNice = etSc.Nice or null; + etMem = etSc.MemoryMax or null; + tcpForward = ssh.AllowTcpForwarding or null; + jailNames = builtins.attrNames e.config.services.fail2ban.jails; + scramFlags = [ + "NoNewPrivileges" + "PrivateTmp" + "PrivateDevices" + "ProtectHome" + "ProtectKernelTunables" + "ProtectKernelModules" + "ProtectKernelLogs" + "ProtectHostname" + "ProtectClock" + "ProtectControlGroups" + "RestrictNamespaces" + "LockPersonality" + "MemoryDenyWriteExecute" + "RestrictRealtime" + "RestrictSUIDSGID" + ]; + # Sample host does not turn the indexer wrap on. A burst of 50000 + # or our syslog identity on these names means the gate leaked. + sploraOffNames = [ + "splora-mainnet" + "splora-testnet3" + "splora-testnet4" + "splora-mutinynet" + "splora-liquid" + "bitcoind-mainnet" + "bitcoind-testnet3" + "bitcoind-testnet4" + "bitcoind-mutinynet" + "elementsd-liquid" + "splora-queue" + "splora-popular-scripts" + ]; + sploraGateOff = + name: + let + svc = e.config.systemd.services.${name} or null; + sc = if svc == null then { } else svc.serviceConfig or { }; + burst = sc.LogRateLimitBurst or null; + ident = sc.SyslogIdentifier or null; + burstText = if burst == null then "" else toString burst; + in + burstText != "50000" && ident == null; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.web.enable == false; + assert e.config.services.nginx.enable == false; + assert e.config.surmount.logging.enable == true; + assert lib.hasInfix "MaxLevelStore=debug" journal; + assert !(lib.hasInfix "MaxLevelStore=trace" journal); + assert e.config.networking.firewall.logRefusedConnections == true; + assert (e.config.networking.firewall.logRefusedPackets or false) == false; + assert (e.config.networking.firewall.rejectPackets or false) == false; + assert requireJournalBurst "firewall" (scOf "firewall"); + assert requireJournalBurst "stalwart-mail" (scOf "stalwart-mail"); + assert requireJournalBurst "surmount-management-ui" (scOf "surmount-management-ui"); + assert requireJournalBurst "surmount-scram" (scOf "surmount-scram"); + assert requireJournalBurst "sshd" (scOf "sshd"); + assert requireJournalBurst "fail2ban" (scOf "fail2ban"); + assert requireJournalBurst "eternal-terminal" (scOf "eternal-terminal"); + assert requireJournalBurst "nix-daemon" (scOf "nix-daemon"); + assert requireSyslog "nix-daemon" "nix-daemon" (scOf "nix-daemon"); + assert requireSyslog "firewall" "firewall" (scOf "firewall"); + assert requireSyslog "stalwart-mail" "stalwart-mail" (scOf "stalwart-mail"); + assert requireJournalStreams "stalwart-mail" (scOf "stalwart-mail"); + assert requireSyslog "surmount-management-ui" "surmount-management-ui" ( + scOf "surmount-management-ui" + ); + assert requireJournalStreams "surmount-management-ui" (scOf "surmount-management-ui"); + assert requireSyslog "surmount-scram" "surmount-scram" (scOf "surmount-scram"); + assert requireJournalStreams "surmount-scram" (scOf "surmount-scram"); + assert requireSyslog "fail2ban" "fail2ban" (scOf "fail2ban"); + assert requireSyslog "eternal-terminal" "eternal-terminal" (scOf "eternal-terminal"); + assert (scOf "surmount-scram").NoNewPrivileges == true; + assert (scOf "surmount-scram").ProtectSystem == "strict"; + assert requireAfUnix "surmount-scram" (scOf "surmount-scram"); + assert ssh.PermitEmptyPasswords == false; + assert ssh.PermitUserEnvironment == false; + assert (toString ssh.LoginGraceTime) == "60"; + assert (ssh.LogLevel or "") == "VERBOSE"; + assert (ssh.AuthenticationMethods or "") == "publickey"; + assert f2b.logtarget == "SYSTEMD-JOURNAL"; + assert f2b.loglevel == "INFO"; + assert e.config.services.eternal-terminal.enable == true; + assert e.config.services.eternal-terminal.verbosity == 2; + assert e.config.services.eternal-terminal.silent == false; + assert requireFlags "surmount-scram" scramFlags scram; + assert requireEq "surmount-scram" "ProtectSystem" "strict" scram; + assert requireEq "surmount-scram" "SystemCallArchitectures" "native" scram; + assert requireEq "surmount-scram" "UMask" "0077" scram; + assert requireFamilies "surmount-scram" [ "AF_UNIX" ] scram; + assert requireAbsent "surmount-scram" "PrivateUsers" scram; + assert requireAbsent "surmount-scram" "ProcSubset" scram; + assert requireAbsent "surmount-scram" "ProtectProc" scram; + assert scram.User == "root"; + assert stalwartRustLog == "info"; + assert builtins.elem "RUST_LOG=info,surmount_management_ui=debug" uiEnvList; + assert !(builtins.any (line: lib.hasInfix "rustls=debug" (toString line)) uiEnvList); + assert e.config.surmount.managementUi.authMode == "off"; + assert e.config.surmount.managementUi.listenMode == "http"; + assert e.config.surmount.managementUi.listenAddress == "127.0.0.1"; + assert e.config.surmount.managementUi.acme.enable == false; + assert e.config.networking.firewall.enable == true; + assert sshdNice == null; + assert etNice == null; + assert etMem == null; + assert tcpForward != "no"; + assert tcpForward != false; + assert builtins.elem "sshd" jailNames; + assert builtins.all (n: !(lib.hasInfix "mail" (lib.toLower n))) jailNames; + # Gates this sample does not turn on. + assert e.config.surmount.vaultwarden.enable == false; + assert e.config.surmount.lake.enable == false; + assert e.config.surmount.artiHiddenService.enable == false; + assert e.config.surmount.secrets.requireDeployMaterial == false; + assert e.config.surmount.hardening.qemuGuestAgent.enable == false; + assert e.config.surmount.accessControl.enable == false; + assert e.config.surmount.backups.enable == false; + assert e.config.surmount.speedtest.enable == true; + assert e.config.surmount.speedtest.periodic.enable == false; + assert (e.config.services.splora.enable or false) == false; + assert !(e.config.networking.nftables.tables ? surmount_guard); + assert !(e.config.systemd.services ? surmount-speedtest); + assert !(e.config.systemd.timers ? surmount-speedtest); + assert !(e.config.systemd.services ? surmount-deploy-secrets-check); + assert !(e.config.systemd.services ? surmount-lake); + assert !(e.config.systemd.services ? surmount-arti-scaffold-status); + assert !(e.config.systemd.services ? surmount-arti-hidden-service); + assert !(e.config.systemd.services ? "surmount-nft-ban-helper@"); + assert !(e.config.systemd.sockets ? surmount-nft-ban-helper); + assert !(e.config.systemd.services ? restic-backups-surmount); + assert !(e.config.systemd.timers ? restic-backups-surmount); + assert builtins.all sploraGateOff sploraOffNames; + "t63-mail-vps-security-logging-ok"; + + # Dual-run escape only. The mail sample keeps web.enable false (t63). + t64-nginx-no-public-stalwart-admin = + let + e = evalSurmount { + web.enable = true; + managementUi.enable = true; + managementUi.listenMode = "http"; + managementUi.listenAddress = "127.0.0.1"; + managementUi.port = 8090; + }; + vhosts = e.config.services.nginx.virtualHosts; + servicesVhost = vhosts."services.surmount.systems" or { }; + acmeCertNames = [ + "services.surmount.systems" + "mail.surmount.systems" + "surmount.systems" + "www.surmount.systems" + ]; + acmeUnit = prefix: cert: "acme-${prefix}${cert}"; + scOf = name: e.config.systemd.services.${name}.serviceConfig; + acmeSyslog = + cert: prefix: + let + name = acmeUnit prefix cert; + in + requireSyslog name name (scOf name); + # The renew timer has no exec log. A rate limit on it is a fake log level. + renewTimerOk = + cert: + let + timerName = "acme-renew-${cert}"; + timer = e.config.systemd.timers.${timerName} or null; + tc = if timer == null then { } else timer.timerConfig or { }; + burst = tc.LogRateLimitBurst or null; + interval = tc.LogRateLimitIntervalSec or null; + in + if timer == null then + throw "acme renew timer ${timerName} missing" + else if burst != null || interval != null then + throw "acme renew timer ${timerName} has a log rate limit" + else + true; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.web.enable == true; + assert e.config.services.nginx.enable == true; + assert e.config.surmount.managementUi.acme.enable == false; + assert e.config.services.nginx.logError == "stderr notice"; + assert servicesVhost.locations ? "/"; + assert !(nginxMentionsStalwartAdmin vhosts); + assert requireJournalBurst "nginx" (scOf "nginx"); + assert requireSyslog "nginx" "nginx" (scOf "nginx"); + assert builtins.all ( + cert: requireJournalBurst (acmeUnit "" cert) (scOf (acmeUnit "" cert)) + ) acmeCertNames; + assert builtins.all ( + cert: requireJournalBurst (acmeUnit "order-renew-" cert) (scOf (acmeUnit "order-renew-" cert)) + ) acmeCertNames; + assert builtins.all (cert: acmeSyslog cert "") acmeCertNames; + assert builtins.all (cert: acmeSyslog cert "order-renew-") acmeCertNames; + assert builtins.all renewTimerOk acmeCertNames; + "t64-nginx-no-public-stalwart-admin-ok"; + + # Default-off gates, each turned on only inside this eval. + t65-gated-off-units-journal-burst = + let + e = evalSurmount { + hardening.enable = true; + hardening.qemuGuestAgent.enable = true; + secrets.requireDeployMaterial = true; + secrets.requiredHostPaths = [ + { + path = "/run/surmount-secrets/tls/cert.pem"; + kind = "file"; + } + ]; + speedtest.enable = true; + speedtest.periodic.enable = true; + vaultwarden.enable = true; + vaultwarden.adminTokenEnvFile = "/run/surmount-secrets/vaultwarden/admin.env"; + backups.enable = true; + backups.repository = "s3:s3.example/surmount-mail"; + backups.passwordFile = "/run/surmount-secrets/backups/restic-password"; + lake.enable = true; + lake.package = pkgs.writeShellScriptBin "lake" "exit 0"; + artiHiddenService.enable = true; + artiHiddenService.startDaemon = false; + }; + scOf = name: e.config.systemd.services.${name}.serviceConfig; + toml = builtins.readFile e.config.environment.etc."surmount/arti.toml".source; + speedtestTimer = e.config.systemd.timers.surmount-speedtest.timerConfig; + artiDaemon = e.config.systemd.services.surmount-arti-hidden-service or null; + # `or` must end the binding. Nix parses `attr or default == value` as `attr or (default == value)` even inside parentheses. + daemonOn = if artiDaemon == null then false else artiDaemon.enable or false; + vaultwardenLogLevel = e.config.services.vaultwarden.config.LOG_LEVEL or ""; + vaultwardenExtended = e.config.services.vaultwarden.config.EXTENDED_LOGGING or false; + vwLogFile = e.config.services.vaultwarden.config.LOG_FILE or ""; + secretsSc = scOf "surmount-deploy-secrets-check"; + secretsUser = secretsSc.User or "root"; + secretsHome = secretsSc.ProtectHome or false; + speedSc = scOf "surmount-speedtest"; + speedTimerBurst = speedtestTimer.LogRateLimitBurst or null; + scaffoldSc = scOf "surmount-arti-scaffold-status"; + secretsFlags = [ + "NoNewPrivileges" + "PrivateTmp" + "ProtectKernelTunables" + "ProtectKernelModules" + "ProtectControlGroups" + "RestrictNamespaces" + "LockPersonality" + "MemoryDenyWriteExecute" + "RestrictRealtime" + ]; + speedFlags = [ + "NoNewPrivileges" + "PrivateTmp" + "ProtectHome" + "ProtectKernelTunables" + "ProtectKernelModules" + "ProtectControlGroups" + "RestrictNamespaces" + "LockPersonality" + "MemoryDenyWriteExecute" + "RestrictRealtime" + ]; + scaffoldFlags = [ + "NoNewPrivileges" + "PrivateTmp" + "ProtectHome" + "ProtectKernelTunables" + "ProtectKernelModules" + "RestrictNamespaces" + "LockPersonality" + "MemoryDenyWriteExecute" + "RestrictRealtime" + ]; + in + assert failedAssertions e == [ ]; + assert requireJournalBurst "surmount-deploy-secrets-check" secretsSc; + assert secretsSc.NoNewPrivileges == true; + assert secretsSc.ProtectSystem == "strict"; + assert ((scOf "surmount-deploy-secrets-check").ProtectHome or false) == false; + assert secretsHome == false; + assert secretsUser == "root"; + assert requireAfUnix "surmount-deploy-secrets-check" secretsSc; + assert requireFlags "surmount-deploy-secrets-check" secretsFlags secretsSc; + assert requireEq "surmount-deploy-secrets-check" "ProtectSystem" "strict" secretsSc; + assert requireEq "surmount-deploy-secrets-check" "SystemCallArchitectures" "native" secretsSc; + assert requireFamilies "surmount-deploy-secrets-check" [ "AF_UNIX" ] secretsSc; + assert requireSyslog "surmount-deploy-secrets-check" "surmount-deploy-secrets-check" secretsSc; + assert requireJournalStreams "surmount-deploy-secrets-check" secretsSc; + assert requireJournalBurst "surmount-speedtest" speedSc; + assert speedSc.NoNewPrivileges == true; + assert speedSc.ProtectSystem == "strict"; + assert speedSc.DynamicUser == true; + assert requireAfUnix "surmount-speedtest" speedSc; + assert requireFlags "surmount-speedtest" speedFlags speedSc; + assert requireEq "surmount-speedtest" "ProtectSystem" "strict" speedSc; + assert requireEq "surmount-speedtest" "SystemCallArchitectures" "native" speedSc; + assert requireFamilies "surmount-speedtest" [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ] speedSc; + assert requireSyslog "surmount-speedtest" "surmount-speedtest" speedSc; + assert requireJournalStreams "surmount-speedtest" speedSc; + assert speedtestTimer.Unit == "surmount-speedtest.service"; + assert speedTimerBurst == null; + assert requireJournalBurst "qemu-guest-agent" (scOf "qemu-guest-agent"); + assert requireSyslog "qemu-guest-agent" "qemu-guest-agent" (scOf "qemu-guest-agent"); + assert builtins.elem "multi-user.target" ( + e.config.systemd.services.qemu-guest-agent.wantedBy or [ ] + ); + assert requireJournalBurst "vaultwarden" (scOf "vaultwarden"); + assert requireSyslog "vaultwarden" "vaultwarden" (scOf "vaultwarden"); + assert requireJournalStreams "vaultwarden" (scOf "vaultwarden"); + assert vaultwardenLogLevel == "info"; + assert vaultwardenExtended == true; + assert vwLogFile == "" || vwLogFile == null; + assert requireJournalBurst "restic-backups-surmount" (scOf "restic-backups-surmount"); + assert requireSyslog "restic-backups-surmount" "restic-backups-surmount" ( + scOf "restic-backups-surmount" + ); + assert e.config.systemd.timers ? restic-backups-surmount; + assert requireJournalBurst "surmount-lake" (scOf "surmount-lake"); + assert requireSyslog "surmount-lake" "surmount-lake" (scOf "surmount-lake"); + assert requireJournalStreams "surmount-lake" (scOf "surmount-lake"); + assert e.config.systemd.services ? surmount-arti-scaffold-status; + assert daemonOn == false; + assert requireJournalBurst "surmount-arti-scaffold-status" scaffoldSc; + assert scaffoldSc.NoNewPrivileges == true; + assert scaffoldSc.ProtectSystem == "strict"; + assert scaffoldSc.DynamicUser == true; + assert requireAfUnix "surmount-arti-scaffold-status" scaffoldSc; + assert requireFlags "surmount-arti-scaffold-status" scaffoldFlags scaffoldSc; + assert requireEq "surmount-arti-scaffold-status" "ProtectSystem" "strict" scaffoldSc; + assert requireEq "surmount-arti-scaffold-status" "SystemCallArchitectures" "native" scaffoldSc; + assert requireFamilies "surmount-arti-scaffold-status" [ "AF_UNIX" ] scaffoldSc; + assert requireSyslog "surmount-arti-scaffold-status" "surmount-arti-scaffold-status" scaffoldSc; + assert requireJournalStreams "surmount-arti-scaffold-status" scaffoldSc; + assert lib.hasInfix "console = \"debug\"" toml; + assert lib.hasInfix "log_sensitive_information = false" toml; + assert !(lib.hasInfix "console = \"trace\"" toml); + "t65-gated-off-units-journal-burst-ok"; + + # Daemon unit exists only when startDaemon is true. Scaffold is t65. + t66-arti-daemon-journal-burst = + let + e = evalSurmount { + artiHiddenService.enable = true; + artiHiddenService.startDaemon = true; + artiHiddenService.packageIsOnionServiceCapable = true; + }; + sc = e.config.systemd.services.surmount-arti-hidden-service.serviceConfig; + toml = builtins.readFile e.config.environment.etc."surmount/arti.toml".source; + in + assert failedAssertions e == [ ]; + assert + !(e.config.systemd.services ? surmount-arti-scaffold-status) + || e.config.systemd.services.surmount-arti-scaffold-status.enable == false + || e.config.systemd.services.surmount-arti-scaffold-status.wantedBy == [ ]; + assert requireJournalBurst "surmount-arti-hidden-service" sc; + assert requireSyslog "surmount-arti-hidden-service" "surmount-arti-hidden-service" sc; + assert requireJournalStreams "surmount-arti-hidden-service" sc; + assert sc.NoNewPrivileges == true; + assert sc.ProtectSystem == "strict"; + assert (!(sc ? MemoryDenyWriteExecute)) || sc.MemoryDenyWriteExecute == false; + assert requireAfUnix "surmount-arti-hidden-service" sc; + assert lib.hasInfix "console = \"debug\"" toml; + assert lib.hasInfix "log_sensitive_information = false" toml; + assert !(lib.hasInfix "console = \"trace\"" toml); + "t66-arti-daemon-journal-burst-ok"; + + # nft helper and the ban table stay off unless accessControl is on. + t67-nft-journal-and-helper-burst = + let + e = evalSurmount { + accessControl.enable = true; + accessControl.nftHelper = true; + accessControl.nftSets = true; + accessControl.nftBin = "/run/current-system/sw/bin/nft"; + accessControl.nftHelperBin = "/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-helper/bin/surmount-nft-ban-helper"; + accessControl.backend = "nft"; + accessControl.enforcement = "enforce"; + managementUi.enable = true; + managementUi.listenMode = "http"; + }; + content = e.config.networking.nftables.tables.surmount_guard.content; + helper = e.config.systemd.services."surmount-nft-ban-helper@".serviceConfig; + nft = e.config.systemd.services.nftables.serviceConfig; + sock = e.config.systemd.sockets.surmount-nft-ban-helper.socketConfig; + capList = + caps: if builtins.isList caps then map toString caps else lib.splitString " " (toString caps); + helperAmbient = capList helper.AmbientCapabilities; + helperBound = capList helper.CapabilityBoundingSet; + in + assert failedAssertions e == [ ]; + assert requireJournalBurst "nftables" nft; + assert requireSyslog "nftables" "nftables" nft; + assert requireJournalBurst "surmount-nft-ban-helper@" helper; + assert requireSyslog "surmount-nft-ban-helper@" "surmount-nft-ban-helper" helper; + assert helper.StandardOutput == "socket"; + assert helper.StandardError == "journal"; + assert helper.NoNewPrivileges == true; + assert helper.ProtectSystem == "strict"; + assert requireAfUnix "surmount-nft-ban-helper@" helper; + assert + helperAmbient == [ + "CAP_NET_ADMIN" + "CAP_NET_RAW" + ]; + assert + helperBound == [ + "CAP_NET_ADMIN" + "CAP_NET_RAW" + ]; + assert sock.SocketMode == "0660"; + assert sock.SocketUser == "root"; + assert sock.SocketGroup == "surmount-ui"; + assert lib.hasInfix "level info" content; + assert lib.hasInfix "surmount-nft-ban-drop:" content; + "t67-nft-journal-and-helper-burst-ok"; + + # Indexer and daemon bursts apply only when those instances are on. + # This eval turns the service on. The mail sample does not (t63). + t68-splora-on-journal-burst = + let + e = evalSurmount { + __extraModules = [ + { + services.splora.enable = true; + } + ]; + }; + requiredIndexerUnits = [ + "splora-mainnet" + "splora-testnet3" + "splora-testnet4" + "splora-mutinynet" + "splora-liquid" + "bitcoind-mainnet" + "bitcoind-testnet3" + "bitcoind-testnet4" + "bitcoind-mutinynet" + "elementsd-liquid" + ]; + scOf = name: e.config.systemd.services.${name}.serviceConfig; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.sploraProxy.unitsMemoryMax == null; + assert builtins.all ( + name: + builtins.hasAttr name e.config.systemd.services + && requireJournalBurst name (scOf name) + && requireSyslog name name (scOf name) + ) requiredIndexerUnits; + "t68-splora-on-journal-burst-ok"; + + # Memory-cap overlay burst. Queue-only so the five indexers are not + # started. popular-scripts stays on its own enable inside that overlay. + t69-splora-memory-cap-journal-burst = + let + e = evalSurmount { + managementUi.enable = true; + sploraProxy.unitsMemoryMax = "2G"; + __extraModules = [ sploraQueueOnlyExtra ]; + }; + capped = [ + "splora-queue" + "splora-mainnet" + "splora-testnet3" + "splora-testnet4" + "splora-mutinynet" + "splora-liquid" + ]; + scOf = name: e.config.systemd.services.${name}.serviceConfig; + queueEnable = e.config.services.splora.queueEnable or false; + popSvc = e.config.systemd.services.splora-popular-scripts or null; + popSc = if popSvc == null then { } else popSvc.serviceConfig or { }; + popBurst = popSc.LogRateLimitBurst or null; + popBurstText = if popBurst == null then "" else toString popBurst; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.sploraProxy.unitsMemoryMax == "2G"; + assert queueEnable == true; + assert (e.config.services.splora.popularScripts.enable or false) == false; + assert popBurstText != "50000"; + assert + !(e.config.systemd.services ? splora-popular-scripts) + || ((scOf "splora-popular-scripts").MemoryMax or null) == null; + assert builtins.all ( + name: + builtins.hasAttr name e.config.systemd.services + && (scOf name).MemoryMax == "2G" + && requireJournalBurst name (scOf name) + ) capped; + "t69-splora-memory-cap-journal-burst-ok"; + + # Example units. Not installed by the mail-host module eval. + t70-laptop-renew-example-journal = + let + svc = builtins.readFile ../script/laptop-renew/surmount-laptop-renew-cert.service; + timer = builtins.readFile ../script/laptop-renew/surmount-laptop-renew-cert.timer; + in + assert lib.hasInfix "StandardOutput=journal" svc; + assert lib.hasInfix "StandardError=journal" svc; + assert lib.hasInfix "SyslogIdentifier=surmount-laptop-renew-cert" svc; + assert lib.hasInfix "Unit=surmount-laptop-renew-cert.service" timer; + "t70-laptop-renew-example-journal-ok"; + + # logFile null keeps DynamicUser (t65). A set path is root, and a home + # path fails the eval. + t65b-speedtest-logfile-not-under-home = + let + badHome = failedAssertions (evalSurmount { + speedtest.enable = true; + speedtest.periodic.enable = true; + speedtest.periodic.logFile = "/home/operator/speedtest.json"; + }); + badRoot = failedAssertions (evalSurmount { + speedtest.enable = true; + speedtest.periodic.enable = true; + speedtest.periodic.logFile = "/root/speedtest.json"; + }); + e = evalSurmount { + speedtest.enable = true; + speedtest.periodic.enable = true; + speedtest.periodic.logFile = "/var/log/surmount/speedtest.json"; + }; + sc = e.config.systemd.services.surmount-speedtest.serviceConfig; + dynamic = sc.DynamicUser or false; + in + assert badHome != [ ]; + assert builtins.any (a: lib.hasInfix "logFile" a.message) badHome; + assert badRoot != [ ]; + assert builtins.any (a: lib.hasInfix "logFile" a.message) badRoot; + assert failedAssertions e == [ ]; + assert sc.User == "root"; + assert dynamic == false; + assert requireJournalBurst "surmount-speedtest" sc; + assert requireSyslog "surmount-speedtest" "surmount-speedtest" sc; + assert requireJournalStreams "surmount-speedtest" sc; + "t65b-speedtest-logfile-not-under-home-ok"; + + # Queue burst is only when queueEnable is true. Indexers still take the cap. + t69b-splora-queue-off-no-burst = + let + e = evalSurmount { + managementUi.enable = true; + sploraProxy.unitsMemoryMax = "2G"; + __extraModules = [ + sploraQueueOnlyExtra + { + services.splora.queueEnable = false; + } + ]; + }; + svc = e.config.systemd.services.splora-queue or null; + sc = if svc == null then { } else svc.serviceConfig or { }; + burst = sc.LogRateLimitBurst or null; + burstText = if burst == null then "" else toString burst; + main = e.config.systemd.services.splora-mainnet.serviceConfig; + in + assert failedAssertions e == [ ]; + assert burstText != "50000"; + assert main.MemoryMax == "2G"; + assert requireJournalBurst "splora-mainnet" main; + "t69b-splora-queue-off-no-burst-ok"; + + # popular-scripts burst is only when that enable and the memory cap are set. + t71-splora-popular-scripts-journal-burst = + let + e = evalSurmount { + managementUi.enable = true; + sploraProxy.unitsMemoryMax = "2G"; + sploraIndexer = sploraRemoteRpc; + __extraModules = [ + { + services.splora.popularScripts.enable = true; + services.splora.popularScripts.instance = "mainnet"; + } + ]; + }; + sc = e.config.systemd.services.splora-popular-scripts.serviceConfig; + enabled = e.config.services.splora.popularScripts.enable or false; + in + assert failedAssertions e == [ ]; + assert enabled == true; + assert sc.MemoryMax == "2G"; + assert requireJournalBurst "splora-popular-scripts" sc; + "t71-splora-popular-scripts-journal-burst-ok"; + + # nix-daemon identifier and burst when logging is off and the builder is on. + # The mail sample (t63) covers logging on with the builder off. + t72-nix-daemon-journal-when-builder-on = + let + e = evalSurmount { + logging.enable = false; + remoteBuilder.enable = true; + }; + sc = e.config.systemd.services.nix-daemon.serviceConfig; + in + assert failedAssertions e == [ ]; + assert e.config.surmount.logging.enable == false; + assert e.config.surmount.remoteBuilder.enable == true; + assert requireSyslog "nix-daemon" "nix-daemon" sc; + assert requireJournalBurst "nix-daemon" sc; + "t72-nix-daemon-journal-when-builder-on-ok"; + results = [ t1-defaults t1b-p1-free-443-plan-and-firewall @@ -3615,6 +4346,18 @@ let t62-mail-host-cpu-v3 t62-gccarch-feature t43-scram-watch-highest-priority + t63-mail-vps-security-logging + t64-nginx-no-public-stalwart-admin + t65-gated-off-units-journal-burst + t65b-speedtest-logfile-not-under-home + t66-arti-daemon-journal-burst + t67-nft-journal-and-helper-burst + t68-splora-on-journal-burst + t69-splora-memory-cap-journal-burst + t69b-splora-queue-off-no-burst + t70-laptop-renew-example-journal + t71-splora-popular-scripts-journal-burst + t72-nix-daemon-journal-when-builder-on ]; in { From d71985d31828e346d41c6f51b28502376e106591 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Wed, 4 Nov 2026 21:30:38 -0700 Subject: [PATCH 13/22] fixes --- Cargo.lock | 8 +- docs/COMPACTION-PIN.md | 7 +- docs/OPS.md | 117 +++++++++++++++++++++++++++++- docs/operator-direction.md | 2 +- docs/packages-and-forks.md | 21 +++++- docs/research/version-audit.md | 2 +- flake.lock | 6 +- flake.nix | 20 ++++- modules/options.nix | 4 + modules/remote-builder.nix | 62 ++++++++++++++++ nix/overlays.nix | 13 +++- nix/packages/lean4.nix | 85 ++++++++++++++++++++++ tests/remote-builder-contract.nix | 17 +++++ 13 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 nix/packages/lean4.nix diff --git a/Cargo.lock b/Cargo.lock index 028dfe7..0c2d25c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,9 +213,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.2" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -224,9 +224,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" dependencies = [ "cc", "cmake", diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index 728e5e8..6d220a5 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -4,7 +4,11 @@ Read this first, then follow links into child docs. Do not invent product scope from memory alone. -**Last updated:** 2026-11-03 (number every parallel list in operator +**Last updated:** 2026-11-04 (surmount-1 nix-daemon must list +gccarch-x86-64-v3 before `just deploy` can build the x86-64-v3 +stdenv; the evaluated mail-vps config already requests it, and a +`/run` bind of nix.conf does not survive reboot). Prior 2026-11-03 +(number every parallel list in operator chat; horizon is this laptop; surmount-1 is the mail host and the remote Nix builder; the private overlay directory is not a machine). Prior 2026-10-05 (`just publish` is the static-site command @@ -176,6 +180,7 @@ Source of truth for FODs: `nix/packages/*.nix`. Audit: | Arti (HS package) | Surmount-owned source **2.7.0** (`arti-onion-service.nix`, GitLab `arti-v2.7.0`) + `onion-service-service` | Matches nixpkgs-rust `pkgs.arti.version` **2.7.0** (lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`). Not stock nixpkgs 1.4.2 lag. rustc via `nixpkgs-rust` is **1.99.1**. Prior lock `c46e6c9a` was rustc 1.99.0. Live Tor verify still residual | | management-ui rustc (crane) | **1.99.1** from `nixpkgs-rust` `pkgs.rustc` (`nix/rust-toolchain.nix`, lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`) | Host channel nixos-26.05 `pkgs.rustc` is still **1.95.0**. grok-oss uses the same `surmount-rust-1.99.1` toolchain, not host cargo 1.95.0. Not `rustPackages_1_95`. Leptos MSRV is >=1.88 | | Host nixpkgs channel | **nixos-26.05** lock | Engine is **not** from channel; channel lag is separate | +| Lean (builder login) | **4.35.0** official linux x86_64 release (`nix/packages/lean4.nix`, tag `v4.35.0`, published 2026-10-12) | Operator directed Lean 4.35 on 2026-11-04. Not nixpkgs `pkgs.lean4` 4.29.1. Not an elan pin. The live login PATH is unchanged until a switch. `just deploy` was not run for this pin. | | Stock Stalwart modules | dual `disabledModules` | `stalwart-mail.nix` + `stalwart.nix`; Surmount option `services.stalwart` (not stock TOML body) | | PQConnect upstream | **1.2.x** Python (tree saw **1.2.3**) | Sibling path packaging; not Rust | | PQConnect Nix flake | Sibling `feat/nix-flake`, **uncommitted** (human owns commit) | [research/pqconnect-local-packaging.md](research/pqconnect-local-packaging.md) | diff --git a/docs/OPS.md b/docs/OPS.md index 6d2b22a..7273431 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -861,7 +861,29 @@ any flag above `x86-64-v3`. AVX-512 is absent, so this is not `hosts/mail-vps/configuration.nix` sets `surmount.mailHostCpu.enable`. `modules/mail-host-cpu.nix` then sets `nixpkgs.hostPlatform` to this host's system with `gcc.arch = "x86-64-v3"`. The host Nix daemon must -advertise gccarch-x86-64-v3 or the v3 stdenv will not build. The nixpkgs cc-wrapper +advertise gccarch-x86-64-v3 or the v3 stdenv will not build. Nixpkgs +adds that token to `nix.settings.system-features` when `gcc.arch` is +`x86-64-v3`, and it also adds the inferior tokens `gccarch-x86-64-v2` +and `gccarch-x86-64`. The mail-host module appends `gccarch-x86-64-v3` +again on `nix.settings.extra-system-features`. That list concatenates +with `surmount-remote` from the remote builder module. Do not replace +`system-features` with only the march token. Do not remove `gcc.arch` +and do not set a lower march to make a build pass. + +The running daemon does not read that evaluated list until a switch +activates the new `nix.conf`. The switch builds the v3 stdenv first. +A cache miss (zlib, gcc, glibc, and the rest of that chain) requires +`gccarch-x86-64-v3` on the daemon that is already running. A +`NIX_CONFIG` drop-in does not make that build check pass. The conf +the daemon reads is the store file behind `/etc/nix/nix.conf`. As +root, put `extra-system-features = surmount-remote gccarch-x86-64-v3` +in a copy of that file and bind-mount the copy over the store path, +then restart `nix-daemon`. Keep `benchmark`, `big-parallel`, `kvm`, +`nixos-test`, and `surmount-remote`. A bind under `/run` does not +survive reboot. `just deploy-dry-run` only prints planned commands +and does not build, so a green dry-run does not show this feature. + +The nixpkgs cc-wrapper reads `targetPlatform.gcc.arch` and writes `-march=x86-64-v3` into `cc-cflags-before`. That flag applies to every C and C++ compile that uses this host's stdenv, including dependencies such as nixpkgs @@ -1025,8 +1047,11 @@ missing system features. `extra-` keeps NixOS auto-detected `surmount-remote`. After a module change, rebuild and switch this host (`just deploy-host` or host `nixos-rebuild switch`) so `/etc/nix/nix.conf` picks up the extra feature; then restart is -implied by the switch. Do not advertise a fake -high slot count. Speed factor is +implied by the switch. That order works for `surmount-remote` only +when the daemon can already build the closure. `gccarch-x86-64-v3` +is required to build the v3 stdenv, so it has to be on the running +daemon before that switch. See Mail-host compiler CPU above. +Do not advertise a fake high slot count. Speed factor is not a substitute for `MemoryMax`. Disk refuse at `diskGuardPercent` (default 95). Enable from private host-local. Host-local may set a real `memoryMax` as the **builder budget**; never publish that number @@ -1047,12 +1072,96 @@ unset. Do not set builder `Nice=19` on mail to deprioritize it. `nix-daemon` **does** get `Nice=19` and idle I/O when the module is enabled (dev builds only; not mail). +### Builder login cc, C++, and Lean (2026-11-04) + +When `surmount.remoteBuilder.enable` is true, +`modules/remote-builder.nix` adds `surmount-builder-devtools` to +`users.users.nixbuilder.packages`. NixOS activation installs that list +at `/etc/profiles/per-user/nixbuilder/bin`. Measured ssh as nixbuilder, +including a login shell, already has that directory on PATH. The +directory was missing because the user had no packages. A later switch +rebuilds the profile from this list, so the names stay. The public +module default of `remoteBuilder.enable` stays false. The private +overlay on horizon sets enable and does not list these packages. The +module supplies them when enable is true. + +This does not set `systemd.services.nix-daemon.path`. It does not +change the sandbox. Live `/etc/nix/nix.conf` on surmount-1 had +`sandbox = true`, `sandbox-fallback = false`, and an empty +`extra-sandbox-paths`. Derivations still use their own stdenv. +`surmount-remote` and mail-host `gccarch-x86-64-v3` stay on +`extra-system-features`. `pkgs.gcc` is the host `pkgs` wrapper, so +mail-host `gcc.arch` still applies to that `cc`. + +`cc`, `c++`, `gcc`, `g++`, `clang`, `clang++`, `ld`, `lld`, `ld.lld`, +and `make` are symlinks. `lean`, `lake`, `leanc`, and `leanmake` are +wrappers that exec the pinned store paths. `leanmake` uses `dirname` +of its own path to find `share/lean/lean.mk`, so a symlink in this +devtools tree would point at the wrong directory. The derivation +checks that the source paths exist when it is built. On 2026-11-04 +`nix eval --raw .#surmount-lean4.version` printed `4.35.0` and +`nix eval --raw .#surmount-lean4.outPath` printed +`/nix/store/v22cg43c1292h1wwbrl32xjl42h84snh-lean4-4.35.0`. +That store path's `bin/lean --version` printed +`Lean (version 4.35.0, x86_64-unknown-linux-gnu, commit eb6cafb7290e8bd5f2937522d8d804f58f02bafc, Release)`. +The devtools profile derivation was not built in that same command. +The switch was not run. + +1. `cc`, `c++`, `gcc`, and `g++` from `pkgs.gcc`. That attribute is + wrapCC of gcc 15.3.0 on non-i686 + (`pkgs/development/compilers/gcc/versions.nix` at nixpkgs + `7dcdd87373d72e7a2173e4db02e20d3d7f495cfd`). cc-wrapper creates + `bin/cc` and `bin/c++`. It does not install `ld`. There is no live + `gcc --version` because `gcc` was not on PATH. +2. `clang` and `clang++` from `pkgs.llvmPackages.clang-unwrapped` + (`llvmPackages_21`). Not wrapped `pkgs.clang`, which also ships + `cc`. +3. `ld` from `pkgs.binutils`. +4. `lld` and `ld.lld` from `pkgs.lld`. +5. `make` from `pkgs.gnumake` (package expression version 4.4.1). The + on-disk leanmake script calls `make`. +6. `lean`, `lake`, `leanc`, and `leanmake` from + `nix/packages/lean4.nix` (official release tag `v4.35.0`, version + 4.35.0). Operator directed Lean 4.35 on 2026-11-04. This is not + `pkgs.lean4` (package expression version 4.29.1 on nixpkgs + `7dcdd87373d72e7a2173e4db02e20d3d7f495cfd`). + +Measured on surmount-1 as nixbuilder before this edit: the login PATH +did not contain `cc`, `c++`, `gcc`, `g++`, `clang`, `clang++`, `ld`, +`lld`, `lean`, `lake`, or `leanc`. Those names were also absent from +`/run/current-system/sw/bin`. An elan tree was on disk at +`/home/nixbuilder/.elan/toolchains/leanprover--lean4---v4.32.0/bin` +and was not on PATH. `lean`, `lake`, `leanc`, `clang`, and `ld.lld` +there failed `--version` with the NixOS stub dynamic linker. +`leanmake` is a shell script and printed `make: command not found`. +`leantar` printed `leantar 0.1.19`. This nixpkgs pin has no `leantar` +attribute, so it is not in the profile. Activation does not delete +that home tree. The directory name v4.32.0 is not the pin. The pin +is official tag v4.35.0 in `nix/packages/lean4.nix`. nixpkgs +`pkgs.lean4` on this lock remains 4.29.1 and is not what the login +profile installs. The live login PATH does not have 4.35.0 until a +switch. `just deploy` was not run for this pin. + +`~/.nix-profile` pointed at a profile with no `bin`. There was no +bashrc. A switch would not put the elan bin directory on PATH. It +replaces `/run/current-system` and the per-user profile from this +module. Declaring the package is what makes the next switch install +the tools. + +The switch was not run. These names are not on the live nixbuilder +login PATH until that switch. The flake attribute `.#surmount-lean4` +evaluates to version 4.35.0, and the built binary at +`/nix/store/v22cg43c1292h1wwbrl32xjl42h84snh-lean4-4.35.0/bin/lean` +prints Lean 4.35.0 commit eb6cafb. The login profile on surmount-1 +still does not include that binary until a generation switch. + Hermetic tests: crate `surmount-niced-builder` in `checks.*.ci`, `just test-remote-builder-eval`, `just test-logging-eval`, `nix eval --impure --json --file tests/lake-contract.nix`, and `tests/module-eval.nix` t39* / t42* / t44*. No live SSH. t39* prove the builder contract (user, trusted-users, slice caps, nix-daemon MemoryMax/Nice, max-jobs, extra-system-features -`surmount-remote`, niced stdio, `onlineCpus` multi-CPU quota). t44* +`surmount-remote`, niced stdio, `onlineCpus` multi-CPU quota, login +devtools package t39q). t44* prove Lake is default off and, when enabled, MemoryMax/Nice land on `surmount-lake.service`. t39e still refuses a Lake overlay that is not `surmount.lake.enable`. diff --git a/docs/operator-direction.md b/docs/operator-direction.md index 37f2630..c9219b9 100644 --- a/docs/operator-direction.md +++ b/docs/operator-direction.md @@ -38,7 +38,7 @@ Operator answers while reviewing the service list. These override a public-module default when `just deploy` copies the private overlay from horizon onto surmount-1. -1. surmount-1 stays a Nix builder and a Lean builder. `surmount-scram.service` stays. This repo does not pin Lean 4.35. +1. surmount-1 stays a Nix builder and a Lean builder. `surmount-scram.service` stays. On 2026-11-04 the operator directed this repo to pin Lean 4.35. The pin is the official release tag v4.35.0 in `nix/packages/lean4.nix`, used by the nixbuilder login profile. It is not nixpkgs `pkgs.lean4` (4.29.1 on nixos-26.05). The live switch was not run as part of that pin. 2. Vaultwarden stays off. The operator is not using it yet. 3. Splora mainnet stays off. The other Splora instances already enabled in the private overlay stay as they are. 4. Host renewal on surmount-1 is on. HTTP-01 on surmount-1 refreshes the Let's Encrypt production certificate that host already presents. horizon does not have to stay online for that renewal. Namecheap API credentials stay off surmount-1. No Namecheap ClientIp change. Closing horizon does not stop surmount-1 from serving the PEMs already on disk. diff --git a/docs/packages-and-forks.md b/docs/packages-and-forks.md index f63d791..1e5026a 100644 --- a/docs/packages-and-forks.md +++ b/docs/packages-and-forks.md @@ -51,6 +51,7 @@ package bumps in this repo. | `nix/packages/surmount-host-logs.nix` | Host journal status/follow (crane). `nix run .#surmount-host-logs`. `just host-logs`. Journald stays source of truth. | | `nix/packages/surmount-shc.nix` | SHC customer user-api client (crane; rDNS PTR + tickets). `nix run .#surmount-shc`. `just rdns-shc`. No python3. | | `nix/packages/surmount-niced-builder.nix` | Niced ssh-ng nix-daemon helper (crane). `nix run .#surmount-niced-builder`. Installed as `/etc/surmount/niced-builder` when remote builder is on. MemoryMax stays on the NixOS module. Not a mail wrapper. | +| `nix/packages/lean4.nix` | Official Lean **4.35.0** linux x86_64 release (tag `v4.35.0`). `nix run` is not the builder path. `modules/remote-builder.nix` puts `lean`, `lake`, `leanc`, and `leanmake` on the nixbuilder login PATH from this pin. Not channel `pkgs.lean4` (4.29.1). | | `nix/packages/surmount-leftover-homes.nix` | Refuse leftover repo `.agents` / `.grok` homes (crane). `nix run .#surmount-leftover-homes`. Fixtures live under `crates/surmount-leftover-homes/testdata/`. | | `nix/packages/surmount-host-probe.nix` | inxi (no TTY), btop (TTY), hybrid TLS probe, Eternal Terminal client. `nix run .#surmount-inxi-host` / `.#surmount-btop-host` / `.#surmount-tls-hybrid` / `.#surmount-et`. | | `nix/packages/surmount-static-sites.nix` | Apex/www publish + proven DS3018xs extra vhosts. `nix run .#surmount-deploy-static-sites` / `.#surmount-sync-static-sites`. | @@ -65,7 +66,7 @@ package bumps in this repo. | `nix/packages/surmount-secrets-install.nix` | Domain A to Domain B install + Vaultwarden export-to-staging (crane). `nix run .#secrets-install-host` / `.#secrets-export-bw-to-staging`. | | `nix/packages/surmount-secrets-prompt.nix` | Domain A no-echo intake (crane). `nix run .#surmount-secrets-prompt`. `just secrets-prompt`. | | `nix/packages/surmount-rekey.nix` | SHC backup re-key paste (X25519 age identity + gpg wrap, not the age crate). `nix run .#surmount-rekey`. `just rekey`. Prints `age1...` for type PGP. Never prints the age secret. | -| `nix/overlays.nix` | Extra pins (flake `surmountOverlay` is primary) | +| `nix/overlays.nix` | Extra pins (flake `surmountOverlay` is primary). tzdata 2026e sets `doCheck = false` (same change as NixOS/nixpkgs pull 582069). | | `modules/stalwart-service.nix` | 0.16+ `config.json` service (disables both stock paths: `services/mail/stalwart-mail.nix` and `services/mail/stalwart.nix`; option `services.stalwart`; unit stays `stalwart-mail.service`) | | `modules/*.nix` | Surmount NixOS modules | | `flake.nix` / `flake.lock` | Inputs, checks, host entrypoints | @@ -298,6 +299,24 @@ Stalwart binaries. ## 6. Version validation +### tzdata 2026e `now.ck` (2026-11-04) + +Locked nixpkgs is `github:NixOS/nixpkgs/nixos-26.05` rev +`7dcdd87373d72e7a2173e4db02e20d3d7f495cfd`. That rev builds tzdata +2026e with `doCheck = true`. On or after 2026-11-01 the `now.ck` test +fails (`make: *** [Makefile:1006: now.ck] Error 1`) because +America/Phoenix and America/Vancouver are identical from now on. The +same check also names America/Mexico_City with America/Edmonton, and +America/Jamaica with America/Winnipeg. That is a date-dependent check, +not a gcc failure and not an x86-64-v3 failure. The failing step is +`awk -f checknow.awk zonenow.tab` (make target `now.ck`). `nix/overlays.nix` +copies [NixOS/nixpkgs pull 582069](https://github.com/NixOS/nixpkgs/pull/582069) +(accessed: 2026-11-04) until this lock contains that change. The overlay +sets `doCheck = false` with the same IANA thread comment as the pull. +The flake applies that file beside +`surmountOverlay` for `mkPkgs` and for `nixosConfigurations.mail-vps`. +`gcc.arch` and x86-64-v3 stay as they are. `surmount-remote` stays. + When touching package pins: 1. Check upstream latest stable/tag for that component. diff --git a/docs/research/version-audit.md b/docs/research/version-audit.md index a212fe8..95d3ad4 100644 --- a/docs/research/version-audit.md +++ b/docs/research/version-audit.md @@ -356,7 +356,7 @@ Do not treat host rustc 1.95.0 as the product pin. The 2026-10-03 lock | Item | Pin / state | Gap notes | |------|-------------|-----------| -| `nix/overlays.nix` | empty | no extra pins | +| `nix/overlays.nix` | tzdata 2026e `doCheck = false` | Same attribute change as NixOS/nixpkgs pull 582069. Date-dependent `now.ck` (America/Phoenix and America/Vancouver identical from now on, on or after 2026-11-01). Not a gcc or x86-64-v3 change. | | management-ui product version | 0.1.0 | Surmount-owned | | rust-overlay flake input | commented out | not active | | modules / hosts | no independent fetchurl pins | versions come from flake packages + nixpkgs | diff --git a/flake.lock b/flake.lock index 7cb9037..4ef939d 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "advisory-db": { "flake": false, "locked": { - "lastModified": 1793619944, - "narHash": "sha256-Ry4BewR3YDlxkbRQN9/z7BX4guU9RkdRhXNy6ci+Fds=", + "lastModified": 1793738068, + "narHash": "sha256-81n00zcuQvNNQpoqiYZqKvSGBcGR8M0LX4v5YFxcEE4=", "owner": "rustsec", "repo": "advisory-db", - "rev": "263a4d951a0867d4bddc29dddd6d59427c78f9a7", + "rev": "965bf9ad083b58e6340d65829d85217b18be9290", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 60eb5e9..2e3efc8 100644 --- a/flake.nix +++ b/flake.nix @@ -36,6 +36,8 @@ # nix/packages/stalwart-mail.nix and modules/stalwart-service.nix. # Arti engine is NOT taken from this channel. See # nix/packages/arti-onion-service.nix (Surmount-owned 2.7.0 source build). + # Builder-login Lean is NOT pkgs.lean4 from this channel (4.29.1). + # See nix/packages/lean4.nix (official release v4.35.0). nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; # rustc/cargo for crane packages and the Arti source build. Not host @@ -125,6 +127,10 @@ inherit system; }; + # Extra pins live in nix/overlays.nix (tzdata doCheck). Not a second + # overlay home. surmountOverlay remains the Surmount package set. + surmountPins = import ./nix/overlays.nix; + # rustPlatform whose rustc meets Arti MSRV; C deps still come from host pkgs. mkArtiRustPlatform = system: @@ -169,6 +175,8 @@ surmount-secrets-install = self.packages.${system}.surmount-secrets-install; surmount-secrets-prompt = self.packages.${system}.surmount-secrets-prompt; surmount-rekey = self.packages.${system}.surmount-rekey; + # Official Lean 4.35.0 linux x86_64. Not channel pkgs.lean4. + surmount-lean4 = self.packages.${system}.surmount-lean4; # Flake-input packages. Upstream crane src omits .cargo/config.toml # (laptop cargo still uses Menhera). Do not wrap src again here. splora = self.packages.${system}.splora; @@ -181,7 +189,10 @@ system: import nixpkgs { inherit system; - overlays = [ (surmountOverlay system) ]; + overlays = [ + surmountPins + (surmountOverlay system) + ]; }; # Shared crane args for management-ui checks (test/clippy/fmt). @@ -315,7 +326,10 @@ ( { ... }: { - nixpkgs.overlays = [ (surmountOverlay "x86_64-linux") ]; + nixpkgs.overlays = [ + surmountPins + (surmountOverlay "x86_64-linux") + ]; } ) ] @@ -442,6 +456,8 @@ # Mutinynet-only bitcoind. Does not replace pkgs.bitcoind. # testnet3 and testnet4 stay on the stock 32.1 package. bitcoind-mutinynet = pkgs.callPackage ./nix/packages/bitcoind-mutinynet.nix { }; + # Official Lean v4.35.0 linux x86_64 release. Not pkgs.lean4. + surmount-lean4 = pkgs.callPackage ./nix/packages/lean4.nix { }; # Ookla Speedtest CLI with --accept-license --accept-gdpr. # Unfree predicate is only ookla-speedtest. nix run .#speedtest speedtest = diff --git a/modules/options.nix b/modules/options.nix index 49206ce..c5d04a2 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -1297,6 +1297,10 @@ in caps for ssh-ng. Trusted ssh-ng forwards rustc to system nix-daemon, so MemoryMax and Nice land on nix-daemon.service as well as the nixbuilder user slice. + When enabled, also installs cc, C++, make, and Lean 4.35.0 + onto that user's login PATH (users.users..packages). + Lean is nix/packages/lean4.nix (tag v4.35.0), not channel + pkgs.lean4. That profile is not the nix-daemon sandbox PATH. Default false. Enable from private host-local. Does not set Nice= or MemoryMax on mail or other critical units. ''; diff --git a/modules/remote-builder.nix b/modules/remote-builder.nix index 8c89c57..c60c760 100644 --- a/modules/remote-builder.nix +++ b/modules/remote-builder.nix @@ -37,6 +37,63 @@ let export SURMOUNT_BUILDER_DISK_GUARD_PATH=${lib.escapeShellArg rb.diskGuardPath} export SURMOUNT_BUILDER_SLICE=${sliceName}.slice ''; + # nixbuilder login profile only. Not nix-daemon Environment and not the + # sandbox PATH (derivations keep their own stdenv). pkgs.gcc is wrapCC + # (bin/cc, bin/c++). clang-unwrapped, not pkgs.clang, because the wrapped + # clang also ships cc. Lean is nix/packages/lean4.nix (v4.35.0), + # not pkgs.lean4 from this nixpkgs (4.29.1) and not an elan pin. + gccBin = pkgs.gcc; + clangBin = pkgs.llvmPackages.clang-unwrapped; + binutilsBin = pkgs.binutils; + lldBin = pkgs.lld; + makeBin = pkgs.gnumake; + leanBin = pkgs.callPackage ../nix/packages/lean4.nix { }; + builderDevtools = pkgs.runCommand "surmount-builder-devtools" { } '' + set -eu + mkdir -p "$out/bin" + test -e ${gccBin}/bin/cc + test -e ${gccBin}/bin/c++ + test -e ${gccBin}/bin/gcc + test -e ${gccBin}/bin/g++ + test -e ${clangBin}/bin/clang + test -e ${binutilsBin}/bin/ld + test -e ${makeBin}/bin/make + test -e ${leanBin}/bin/lean + test -e ${leanBin}/bin/lake + test -e ${leanBin}/bin/leanc + test -e ${leanBin}/bin/leanmake + ln -s ${gccBin}/bin/cc "$out/bin/cc" + ln -s ${gccBin}/bin/c++ "$out/bin/c++" + ln -s ${gccBin}/bin/gcc "$out/bin/gcc" + ln -s ${gccBin}/bin/g++ "$out/bin/g++" + ln -s ${clangBin}/bin/clang "$out/bin/clang" + if [ -e ${clangBin}/bin/clang++ ]; then + ln -s ${clangBin}/bin/clang++ "$out/bin/clang++" + else + # argv[0] clang++ is C++ mode when the unwrapped tree has no symlink. + ln -s ${clangBin}/bin/clang "$out/bin/clang++" + fi + ln -s ${binutilsBin}/bin/ld "$out/bin/ld" + if [ -e ${lldBin}/bin/lld ]; then + ln -s ${lldBin}/bin/lld "$out/bin/lld" + fi + if [ -e ${lldBin}/bin/ld.lld ]; then + ln -s ${lldBin}/bin/ld.lld "$out/bin/ld.lld" + elif [ -e ${lldBin}/bin/lld ]; then + ln -s ${lldBin}/bin/lld "$out/bin/ld.lld" + else + echo "pkgs.lld has neither bin/lld nor bin/ld.lld" >&2 + exit 1 + fi + ln -s ${makeBin}/bin/make "$out/bin/make" + # Wrappers, not symlinks. leanmake uses dirname of its own path + # to find share/lean/lean.mk. A symlink would point that at this + # devtools tree. exec of the pinned file keeps that path. + for tool in lean lake leanc leanmake; do + printf '#!%s\nexec %s "$@"\n' '${pkgs.runtimeShell}' "${leanBin}/bin/$tool" > "$out/bin/$tool" + chmod +x "$out/bin/$tool" + done + ''; in { config = mkIf (cfg.enable && rb.enable) { @@ -72,6 +129,11 @@ in createHome = true; hashedPassword = "!"; linger = true; + # Activation links this to /etc/profiles/per-user//bin, which + # is already on the nixbuilder ssh PATH. A switch rebuilds that + # profile from this list. Not the nix-daemon unit PATH and not + # the sandbox. + packages = [ builderDevtools ]; }; # Laptop ssh-ng SSHes as this user. Do not add nixbuilder to wheel. diff --git a/nix/overlays.nix b/nix/overlays.nix index 09d8563..afbac53 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -10,8 +10,15 @@ # pkgs.arti). Surmount-owned Arti 2.5.1 source build + onion-service-service; # not a feature-only override of channel 1.4.2. See # nix/packages/arti-onion-service.nix. +# +# tzdata 2026e: now.ck is date-dependent. On or after 2026-11-01 the check +# fails because America/Phoenix and America/Vancouver are identical from +# now on (America/Edmonton and America/Winnipeg are the same class). This +# is not a gcc or x86-64-v3 failure. Same attribute change as +# NixOS/nixpkgs pull 582069. gcc.arch and x86-64-v3 stay as they are. -_final: _prev: { - # Intentionally empty. Prefer flake packages + nixosModule specialArgs - # over a heavy overlay unless many packages need sharing. +_final: prev: { + tzdata = prev.tzdata.overrideAttrs (_old: { + doCheck = false; # https://lists.iana.org/hyperkitty/list/tz@iana.org/thread/CBEK3HXYBOXGD34ZFDSF64QIDT7ZWGBB/ + }); } diff --git a/nix/packages/lean4.nix b/nix/packages/lean4.nix new file mode 100644 index 0000000..460b806 --- /dev/null +++ b/nix/packages/lean4.nix @@ -0,0 +1,85 @@ +# Pinned Lean toolchain for the nixbuilder login profile. +# nixos-26.05 pkgs.lean4 is 4.29.1. Operator directed Lean 4.35 on +# 2026-11-04. This file is the official v4.35.0 linux x86_64 release, +# not a floating nixpkgs unstable fetch and not an elan toolchain. +# +# Asset: lean-4.35.0-linux.tar.zst +# https://github.com/leanprover/lean4/releases/tag/v4.35.0 +# (published 2026-10-12T18:13:12Z, commit eb6cafb). +# +# Binaries keep their $ORIGIN runpaths into lib/ and lib/lean. +# Only bin/ interpreters are retargeted, because the release uses +# /lib64/ld-linux-x86-64.so.2, which NixOS does not provide. +# lib/ stays as upstream shipped it (leanc --sysroot). + +{ + lib, + stdenv, + fetchurl, + patchelf, + zstd, +}: + +let + version = "4.35.0"; +in +stdenv.mkDerivation { + pname = "lean4"; + inherit version; + + src = + if stdenv.hostPlatform.system != "x86_64-linux" then + throw "lean4 ${version}: official linux x86_64 tarball only (got ${stdenv.hostPlatform.system})" + else + fetchurl { + url = "https://github.com/leanprover/lean4/releases/download/v${version}/lean-${version}-linux.tar.zst"; + hash = "sha256-colZekRUMkbJInuMrlAXZ+CVzhf21Ei5X8So3Jcmj20="; + }; + + nativeBuildInputs = [ + patchelf + zstd + ]; + + sourceRoot = "lean-${version}-linux"; + + unpackPhase = '' + runHook preUnpack + tar -I zstd -xf "$src" + runHook postUnpack + ''; + + dontConfigure = true; + dontBuild = true; + # Do not strip, and do not let fixup rewrite $ORIGIN runpaths. + dontStrip = true; + dontPatchELF = true; + + installPhase = '' + runHook preInstall + mkdir -p "$out" + cp -a bin include lib share src LICENSE LICENSES "$out/" + dyn="$(cat "${stdenv.cc}/nix-support/dynamic-linker")" + find "$out/bin" -type f -print0 | while IFS= read -r -d "" f; do + if patchelf --print-interpreter "$f" >/dev/null 2>&1; then + patchelf --set-interpreter "$dyn" "$f" + fi + done + test -x "$out/bin/lean" + test -x "$out/bin/lake" + test -x "$out/bin/leanc" + test -x "$out/bin/leanmake" + test -f "$out/share/lean/lean.mk" + runHook postInstall + ''; + + meta = { + description = "Lean 4.35.0 official linux x86_64 release (not nixpkgs lean4)"; + homepage = "https://github.com/leanprover/lean4"; + changelog = "https://github.com/leanprover/lean4/releases/tag/v${version}"; + license = lib.licenses.asl20; + mainProgram = "lean"; + platforms = [ "x86_64-linux" ]; + sourceProvenance = [ lib.sourceTypes.binaryNativeCode ]; + }; +} diff --git a/tests/remote-builder-contract.nix b/tests/remote-builder-contract.nix index 7f8eceb..b768da5 100644 --- a/tests/remote-builder-contract.nix +++ b/tests/remote-builder-contract.nix @@ -190,6 +190,22 @@ let assert !(mail ? Nice); assert !(sshd ? Nice); "t39o-builder-is-preferred-oom-victim-ok"; + + # Login profile only. The nix-daemon unit path must not gain this + # package: sandbox builds use the derivation stdenv, not this PATH. + t39q-builder-user-packages-devtools = + let + e = evalRB { remoteBuilder.enable = true; }; + off = evalRB { remoteBuilder.enable = false; }; + names = map (p: p.name or "") (e.config.users.users.nixbuilder.packages or [ ]); + daemonPaths = e.config.systemd.services.nix-daemon.path or [ ]; + daemonNames = map (p: p.name or "") daemonPaths; + in + assert failedAssertions e == [ ]; + assert lib.any (lib.hasInfix "surmount-builder-devtools") names; + assert !(lib.any (lib.hasInfix "surmount-builder-devtools") daemonNames); + assert !(off.config.users.users ? nixbuilder); + "t39q-builder-user-packages-devtools-ok"; in { ok = [ @@ -203,5 +219,6 @@ in t39m-extra-system-features-advertises-surmount-remote t39n-disabled-does-not-require-surmount-remote t39o-builder-is-preferred-oom-victim + t39q-builder-user-packages-devtools ]; } From 2581c128165d477692ee1f4fe2ba148ea617fa1a Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Thu, 5 Nov 2026 00:53:00 -0700 Subject: [PATCH 14/22] wip --- crates/surmount-deploy-host/src/lib.rs | 1 + crates/surmount-deploy-host/src/nix_conf.rs | 777 ++++++++++++++++++++ crates/surmount-deploy-host/src/run.rs | 48 +- docs/COMPACTION-PIN.md | 17 +- docs/OPS.md | 34 +- docs/TEST-AND-DEPLOY.md | 19 +- modules/mail-host-cpu.nix | 3 + 7 files changed, 877 insertions(+), 22 deletions(-) create mode 100644 crates/surmount-deploy-host/src/nix_conf.rs diff --git a/crates/surmount-deploy-host/src/lib.rs b/crates/surmount-deploy-host/src/lib.rs index 0eda724..8eb019b 100644 --- a/crates/surmount-deploy-host/src/lib.rs +++ b/crates/surmount-deploy-host/src/lib.rs @@ -5,6 +5,7 @@ mod args; mod error; mod keys; +mod nix_conf; mod paths; mod run; mod smoke; diff --git a/crates/surmount-deploy-host/src/nix_conf.rs b/crates/surmount-deploy-host/src/nix_conf.rs new file mode 100644 index 0000000..42b3c8d --- /dev/null +++ b/crates/surmount-deploy-host/src/nix_conf.rs @@ -0,0 +1,777 @@ +//! Pure nix.conf transform for the mail-host gccarch bootstrap. +//! +//! The running daemon does not see `nix.settings.extra-system-features` +//! until a switch activates a new nix.conf. The system build needs +//! `gccarch-x86-64-v3` on the daemon that is already running. This module +//! decides the conf text. The remote driver in `gccarch_bootstrap_remote` +//! applies it on the target before `nixos-rebuild`. + +/// Feature the v3 stdenv requires. Nix 2.34 folds `extra-system-features` +/// into `system-features` on `nix config show`. +pub(crate) const GCCARCH_X86_64_V3: &str = "gccarch-x86-64-v3"; + +/// Remote-builder feature. The bootstrap must not drop it. +pub(crate) const SURMOUNT_REMOTE: &str = "surmount-remote"; + +/// Copy target. Never write the store nix.conf in place. A bind of this +/// path does not survive reboot. +pub(crate) const RUN_NIX_CONF: &str = "/run/surmount-nix.conf"; + +/// What to do with the daemon conf before the system build. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum GccarchConfDecision { + /// `system-features` already lists [`GCCARCH_X86_64_V3`]. + /// Do not rewrite the conf. Do not restart the daemon. + LeaveUnchanged, + /// Write this text to [`RUN_NIX_CONF`], bind-mount it over the store + /// nix.conf, and restart nix-daemon. + Rewrite(String), +} + +impl GccarchConfDecision { + pub(crate) fn rewrites_conf(&self) -> bool { + matches!(self, Self::Rewrite(_)) + } + + /// Restart only follows a rewrite. [`Self::LeaveUnchanged`] does not. + pub(crate) fn restarts_daemon(&self) -> bool { + matches!(self, Self::Rewrite(_)) + } +} + +/// Decide the conf text from the file the daemon is using. +/// +/// `system-features` is last-assignment-wins, same as Nix. A commented +/// line does not count. `extra-system-features` is not that key. +/// When the last `system-features` line lacks the token, every +/// `extra-system-features` line gains `surmount-remote` and +/// `gccarch-x86-64-v3` without dropping tokens already on the line. +/// The `system-features` line text is copied unchanged. +pub(crate) fn bootstrap_gccarch_conf(conf: &str) -> GccarchConfDecision { + let lines = conf_lines(conf); + if last_system_features_value(&lines).is_some_and(|value| value_has_arch(&value)) { + return GccarchConfDecision::LeaveUnchanged; + } + let mut out = String::new(); + let mut saw_extra = false; + for line in &lines { + if assignment_key(line) == Some("extra-system-features") { + saw_extra = true; + out.push_str(&rewrite_extra_line(line)); + out.push('\n'); + } else { + out.push_str(line); + out.push('\n'); + } + } + if !saw_extra { + out.push_str("extra-system-features = "); + out.push_str(SURMOUNT_REMOTE); + out.push(' '); + out.push_str(GCCARCH_X86_64_V3); + out.push('\n'); + } + GccarchConfDecision::Rewrite(out) +} + +fn conf_lines(conf: &str) -> Vec { + if conf.is_empty() { + return Vec::new(); + } + let mut lines: Vec = conf.split('\n').map(str::to_string).collect(); + if conf.ends_with('\n') { + lines.pop(); + } + lines +} + +fn trim_ascii_ws(s: &str) -> &str { + s.trim_matches([' ', '\t']) +} + +fn assignment_key(line: &str) -> Option<&str> { + let rest = trim_ascii_ws(line); + if rest.is_empty() || rest.starts_with('#') { + return None; + } + let eq = rest.find('=')?; + let key = trim_ascii_ws(&rest[..eq]); + if key.is_empty() + || !key + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return None; + } + Some(key) +} + +fn last_system_features_value(lines: &[String]) -> Option { + let mut last = None; + for line in lines { + if assignment_key(line) == Some("system-features") + && let Some(eq) = line.find('=') + { + last = Some(line[eq + 1..].to_string()); + } + } + last +} + +fn ascii_ws_tokens(value: &str) -> Vec { + let mut trimmed = trim_ascii_ws(value); + if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { + trimmed = &trimmed[1..trimmed.len() - 1]; + } + if trimmed.is_empty() { + return Vec::new(); + } + trimmed + .split(|c| c == ' ' || c == '\t' || c == '\n') + .filter(|tok| !tok.is_empty()) + .map(str::to_string) + .collect() +} + +fn value_has_arch(value: &str) -> bool { + ascii_ws_tokens(value) + .iter() + .any(|tok| tok == GCCARCH_X86_64_V3) +} + +fn ensure_extra_tokens(value: &str) -> String { + let mut toks = ascii_ws_tokens(value); + if !toks.iter().any(|tok| tok == SURMOUNT_REMOTE) { + toks.push(SURMOUNT_REMOTE.to_string()); + } + if !toks.iter().any(|tok| tok == GCCARCH_X86_64_V3) { + toks.push(GCCARCH_X86_64_V3.to_string()); + } + toks.join(" ") +} + +fn rewrite_extra_line(line: &str) -> String { + let eq = line + .find('=') + .expect("extra-system-features assignment has '='"); + let prefix = &line[..eq]; + let tokens = ensure_extra_tokens(&line[eq + 1..]); + format!("{prefix}= {tokens}") +} + +const SHELL_FUNCTIONS: &str = r#" +deploy_host_trim() { + val=$1 + tab=$(printf '\t') + while :; do + case "$val" in + ' '*) val=${val# } ;; + "$tab"*) val=${val#"$tab"} ;; + *) break ;; + esac + done + while :; do + case "$val" in + *' ') val=${val% } ;; + *"$tab") val=${val%"$tab"} ;; + *) break ;; + esac + done + printf '%s' "$val" +} + +deploy_host_assignment_key() { + rest=$(deploy_host_trim "$1") + case "$rest" in + ''|\#*) printf '\n'; return ;; + esac + case "$rest" in + *=*) ;; + *) printf '\n'; return ;; + esac + key=$(deploy_host_trim "${rest%%=*}") + case "$key" in + *[!A-Za-z0-9_-]*|'') printf '\n'; return ;; + esac + printf '%s\n' "$key" +} + +deploy_host_list_has_arch() { + oldifs=$IFS + IFS=$(printf ' \t\nX') + IFS=${IFS%X} + set -f + found=0 + for tok in $1; do + if [ "$tok" = "$deploy_host_gccarch_token" ]; then + found=1 + break + fi + done + set +f + IFS=$oldifs + [ "$found" -eq 1 ] +} + +deploy_host_value_has_arch() { + val=$(deploy_host_trim "$1") + case "$val" in + \"*\") + val=${val#\"} + val=${val%\"} + ;; + esac + deploy_host_list_has_arch "$val" +} + +deploy_host_blob_has_token() { + blob=$(printf '%s' "$1" | sed 's/[][",]/ /g') + deploy_host_list_has_arch "$blob" +} + +deploy_host_file_system_features_has_token() { + lastval= + found=0 + while IFS= read -r line || [ -n "$line" ]; do + k=$(deploy_host_assignment_key "$line") + if [ "$k" = system-features ]; then + found=1 + lastval=${line#*=} + fi + done < "$1" + if [ "$found" -eq 1 ]; then + deploy_host_value_has_arch "$lastval" + else + return 1 + fi +} + +deploy_host_ensure_extra_tokens() { + val=$(deploy_host_trim "$1") + case "$val" in + \"*\") + val=${val#\"} + val=${val%\"} + ;; + esac + has_remote=0 + has_arch=0 + out= + oldifs=$IFS + IFS=$(printf ' \t\nX') + IFS=${IFS%X} + set -f + for tok in $val; do + if [ -z "$tok" ]; then + continue + fi + if [ "$tok" = surmount-remote ]; then + has_remote=1 + fi + if [ "$tok" = "$deploy_host_gccarch_token" ]; then + has_arch=1 + fi + if [ -z "$out" ]; then + out=$tok + else + out="$out $tok" + fi + done + set +f + IFS=$oldifs + if [ "$has_remote" -eq 0 ]; then + if [ -z "$out" ]; then + out=surmount-remote + else + out="$out surmount-remote" + fi + fi + if [ "$has_arch" -eq 0 ]; then + if [ -z "$out" ]; then + out=$deploy_host_gccarch_token + else + out="$out $deploy_host_gccarch_token" + fi + fi + printf '%s' "$out" +} + +deploy_host_rewrite_extra_line() { + line=$1 + prefix=${line%%=*} + val=${line#*=} + tokens=$(deploy_host_ensure_extra_tokens "$val") + printf '%s= %s' "$prefix" "$tokens" +} + +deploy_host_rewrite_conf() { + src=$1 + dest=$2 + tmp=$(mktemp "${dest}.XXXXXX") || return 1 + found_extra=0 + while IFS= read -r line || [ -n "$line" ]; do + k=$(deploy_host_assignment_key "$line") + if [ "$k" = extra-system-features ]; then + found_extra=1 + printf '%s\n' "$(deploy_host_rewrite_extra_line "$line")" >> "$tmp" + else + printf '%s\n' "$line" >> "$tmp" + fi + done < "$src" + if [ "$found_extra" -eq 0 ]; then + printf '%s\n' "extra-system-features = surmount-remote $deploy_host_gccarch_token" >> "$tmp" + fi + if ! cat "$tmp" > "$dest"; then + rm -f "$tmp" + return 1 + fi + rm -f "$tmp" + chmod 644 "$dest" || true + return 0 +} + +deploy_host_bind_already() { + if ! command -v findmnt >/dev/null 2>&1; then + return 1 + fi + findmnt -n -o SOURCE -T "$1" 2>/dev/null | grep -F -- "$deploy_host_run_nix_conf" >/dev/null 2>&1 +} +"#; + +const SHELL_DRIVER: &str = r#" +# Bootstrap gccarch before the system build. +# Read the conf the daemon is using. Do not write that store file in place. +# Write /run/surmount-nix.conf and bind-mount it over the store path. +# The /run bind does not survive reboot. A successful switch puts the +# feature in the generation nix.conf. +# If the daemon already advertises the feature, do not rewrite and do not restart. +deploy_host_daemon_features=$(nix config show system-features 2>/dev/null || true) +if deploy_host_blob_has_token "$deploy_host_daemon_features"; then + : +else + store_conf=$(readlink -f /etc/nix/nix.conf) || { + echo "deploy-host: cannot resolve /etc/nix/nix.conf; gccarch-x86-64-v3 still absent; not starting the system build" >&2 + exit 1 + } + if [ ! -f "$store_conf" ]; then + echo "deploy-host: nix.conf the daemon reads is missing; gccarch-x86-64-v3 still absent; not starting the system build" >&2 + exit 1 + fi + if deploy_host_file_system_features_has_token "$store_conf"; then + echo "deploy-host: nix.conf system-features already lists gccarch-x86-64-v3 but the daemon does not advertise it; not rewriting and not restarting; not starting the system build" >&2 + exit 1 + fi + if ! deploy_host_rewrite_conf "$store_conf" "$deploy_host_run_nix_conf"; then + echo "deploy-host: failed to write /run/surmount-nix.conf; gccarch-x86-64-v3 still absent; not starting the system build" >&2 + exit 1 + fi + if [ "$store_conf" != "$deploy_host_run_nix_conf" ]; then + mount_err=$(mktemp /run/surmount-nix-mount.XXXXXX) || { + echo "deploy-host: cannot record the bind-mount error; gccarch-x86-64-v3 still absent; not starting the system build" >&2 + exit 1 + } + if mount --bind "$deploy_host_run_nix_conf" "$store_conf" 2>"$mount_err"; then + rm -f "$mount_err" + elif grep -E -q 'already mounted|already a mount point' "$mount_err"; then + cat "$mount_err" >&2 + rm -f "$mount_err" + elif deploy_host_bind_already "$store_conf"; then + rm -f "$mount_err" + else + echo "deploy-host: bind-mount of nix.conf failed; gccarch-x86-64-v3 still absent; not starting the system build" >&2 + cat "$mount_err" >&2 + rm -f "$mount_err" + exit 1 + fi + fi + if ! systemctl restart nix-daemon; then + echo "deploy-host: nix-daemon restart failed; gccarch-x86-64-v3 still absent; not starting the system build" >&2 + exit 1 + fi + deploy_host_daemon_features=$(nix config show system-features 2>/dev/null || true) + if ! deploy_host_blob_has_token "$deploy_host_daemon_features"; then + echo "deploy-host: gccarch-x86-64-v3 still absent from nix config show system-features; not starting the system build" >&2 + printf '%s\n' "deploy-host: nix config show system-features printed:" >&2 + printf '%s\n' "$deploy_host_daemon_features" >&2 + exit 1 + fi +fi +"#; + +/// Shell that runs on the target as root, before the system build. +/// `nix config show extra-system-features` is not used: Nix 2.34.9 has +/// no such key. The show output for `system-features` already includes +/// extra tokens. +pub(crate) fn gccarch_bootstrap_remote() -> String { + format!( + "set +e\ndeploy_host_gccarch_token={GCCARCH_X86_64_V3}\ndeploy_host_run_nix_conf={RUN_NIX_CONF}\n{SHELL_FUNCTIONS}\n{SHELL_DRIVER}" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + use std::process::Command; + use std::time::{SystemTime, UNIX_EPOCH}; + + const SYSTEM_FEATURES_LINE: &str = "system-features = nixos-test benchmark big-parallel kvm\n"; + + fn live_daemon_conf() -> &'static str { + "extra-system-features = surmount-remote\nsystem-features = nixos-test benchmark big-parallel kvm\n" + } + + fn system_feature_lines(conf: &str) -> Vec { + conf_lines(conf) + .into_iter() + .filter(|line| assignment_key(line) == Some("system-features")) + .collect() + } + + #[test] + fn conf_that_lacks_gccarch_is_not_left_unchanged() { + let input = live_daemon_conf(); + let decision = bootstrap_gccarch_conf(input); + assert!( + decision.rewrites_conf(), + "a conf that lacks {GCCARCH_X86_64_V3} must not be left unchanged" + ); + assert!( + decision.restarts_daemon(), + "a missing token is a rewrite, and a rewrite restarts the daemon" + ); + match decision { + GccarchConfDecision::LeaveUnchanged => { + panic!("a conf that lacks {GCCARCH_X86_64_V3} must not be left unchanged") + } + GccarchConfDecision::Rewrite(out) => { + assert_ne!( + out, input, + "a conf that lacks {GCCARCH_X86_64_V3} must not be left unchanged" + ); + assert!( + out.contains(GCCARCH_X86_64_V3), + "rewritten conf dropped {GCCARCH_X86_64_V3}: {out}" + ); + } + } + } + + #[test] + fn extra_line_gains_gccarch_and_system_features_line_is_unchanged() { + let input = live_daemon_conf(); + let GccarchConfDecision::Rewrite(out) = bootstrap_gccarch_conf(input) else { + panic!("missing token must rewrite"); + }; + assert!( + out.contains("extra-system-features = surmount-remote gccarch-x86-64-v3\n"), + "{out}" + ); + assert!(out.contains(SYSTEM_FEATURES_LINE), "{out}"); + assert_eq!( + system_feature_lines(input), + system_feature_lines(&out), + "system-features line must stay byte-for-byte" + ); + assert!( + !out.contains("system-features = nixos-test benchmark big-parallel kvm gccarch"), + "system-features line must not gain the token: {out}" + ); + } + + #[test] + fn system_features_line_with_gccarch_does_not_rewrite_or_restart() { + let input = "\ +extra-system-features = surmount-remote\n\ +system-features = nixos-test benchmark big-parallel kvm gccarch-x86-64-v3\n"; + let decision = bootstrap_gccarch_conf(input); + assert_eq!(decision, GccarchConfDecision::LeaveUnchanged); + assert!(!decision.rewrites_conf()); + assert!(!decision.restarts_daemon()); + } + + #[test] + fn extra_line_keeps_other_tokens_and_does_not_duplicate() { + let input = "\ +extra-system-features = surmount-remote other-token\n\ +system-features = nixos-test benchmark big-parallel kvm\n"; + let GccarchConfDecision::Rewrite(out) = bootstrap_gccarch_conf(input) else { + panic!("must rewrite"); + }; + assert!( + out.contains("extra-system-features = surmount-remote other-token gccarch-x86-64-v3\n"), + "{out}" + ); + assert_eq!(system_feature_lines(input), system_feature_lines(&out)); + + let already = "\ +extra-system-features = gccarch-x86-64-v3 surmount-remote extra\n\ +system-features = nixos-test benchmark big-parallel kvm\n"; + let GccarchConfDecision::Rewrite(out) = bootstrap_gccarch_conf(already) else { + panic!("system-features still lacks the token, so this is still a rewrite"); + }; + assert!( + out.contains("extra-system-features = gccarch-x86-64-v3 surmount-remote extra\n"), + "{out}" + ); + assert_eq!(out.matches("gccarch-x86-64-v3").count(), 1, "{out}"); + } + + #[test] + fn substring_and_comment_do_not_count_as_the_token() { + let substring = "\ +extra-system-features = surmount-remote\n\ +system-features = nixos-test gccarch-x86-64-v30 kvm\n"; + assert!(bootstrap_gccarch_conf(substring).rewrites_conf()); + + let comment = "\ +# system-features = nixos-test benchmark big-parallel kvm gccarch-x86-64-v3\n\ +# extra-system-features = surmount-remote\n\ +system-features = nixos-test benchmark big-parallel kvm\n"; + let GccarchConfDecision::Rewrite(out) = bootstrap_gccarch_conf(comment) else { + panic!("a comment must not suppress the rewrite"); + }; + assert!(out.contains( + "# system-features = nixos-test benchmark big-parallel kvm gccarch-x86-64-v3\n" + )); + assert!(out.contains("# extra-system-features = surmount-remote\n")); + assert!( + out.contains("extra-system-features = surmount-remote gccarch-x86-64-v3\n"), + "{out}" + ); + assert_eq!(system_feature_lines(comment), system_feature_lines(&out)); + } + + #[test] + fn last_system_features_line_wins() { + let earlier_only = "\ +system-features = gccarch-x86-64-v3\n\ +system-features = nixos-test benchmark big-parallel kvm\n\ +extra-system-features = surmount-remote\n"; + assert!(bootstrap_gccarch_conf(earlier_only).rewrites_conf()); + + let later = "\ +system-features = nixos-test benchmark big-parallel kvm\n\ +extra-system-features = surmount-remote\n\ +system-features = nixos-test benchmark big-parallel kvm gccarch-x86-64-v3\n"; + let decision = bootstrap_gccarch_conf(later); + assert!(!decision.rewrites_conf()); + assert!(!decision.restarts_daemon()); + } + + #[test] + fn missing_extra_line_is_appended_without_touching_system_features() { + let input = "max-jobs = 8\nsystem-features = nixos-test benchmark big-parallel kvm\nsandbox = true\n"; + let GccarchConfDecision::Rewrite(out) = bootstrap_gccarch_conf(input) else { + panic!("must rewrite"); + }; + assert!(out.contains("max-jobs = 8\n"), "{out}"); + assert!(out.contains("sandbox = true\n"), "{out}"); + assert!(out.contains(SYSTEM_FEATURES_LINE), "{out}"); + assert!( + out.contains("extra-system-features = surmount-remote gccarch-x86-64-v3\n"), + "{out}" + ); + assert_eq!(system_feature_lines(input), system_feature_lines(&out)); + } + + #[test] + fn extra_system_features_key_is_not_system_features() { + let input = "extra-system-features = gccarch-x86-64-v3\n"; + let decision = bootstrap_gccarch_conf(input); + assert!( + decision.rewrites_conf(), + "the token on extra-system-features is not the system-features line" + ); + let GccarchConfDecision::Rewrite(out) = decision else { + panic!("must rewrite"); + }; + assert!( + out.contains("extra-system-features = gccarch-x86-64-v3 surmount-remote\n") + || out.contains("extra-system-features = gccarch-x86-64-v3 surmount-remote"), + "{out}" + ); + assert!(out.contains(SURMOUNT_REMOTE), "{out}"); + } + + fn shell_decision(conf: &str) -> Result { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|err| err.to_string())? + .as_nanos(); + let dir = + std::env::temp_dir().join(format!("surmount-gccarch-{}-{nanos}", std::process::id())); + fs::create_dir_all(&dir).map_err(|err| err.to_string())?; + let result = shell_decision_in(&dir, conf); + let _ = fs::remove_dir_all(&dir); + result + } + + fn shell_decision_in(dir: &Path, conf: &str) -> Result { + let src = dir.join("nix.conf"); + let dest = dir.join("out.conf"); + fs::write(&src, conf).map_err(|err| err.to_string())?; + let script = format!( + "deploy_host_gccarch_token={GCCARCH_X86_64_V3}\n\ + deploy_host_run_nix_conf={RUN_NIX_CONF}\n\ + {SHELL_FUNCTIONS}\n\ + if deploy_host_file_system_features_has_token \"$1\"; then\n\ + echo LEAVE\n\ + else\n\ + deploy_host_rewrite_conf \"$1\" \"$2\" || exit 2\n\ + echo REWRITE\n\ + fi\n" + ); + let out = Command::new("sh") + .arg("-c") + .arg(&script) + .arg("sh") + .arg(&src) + .arg(&dest) + .output() + .map_err(|err| format!("sh failed to start: {err}"))?; + let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); + let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); + if !out.status.success() { + return Err(format!( + "sh exit {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}", + out.status.code() + )); + } + if stdout.trim() == "LEAVE" { + return Ok(ShellDecision::Leave); + } + if stdout.trim() == "REWRITE" { + let body = fs::read_to_string(&dest).map_err(|err| err.to_string())?; + return Ok(ShellDecision::Rewrite(body)); + } + Err(format!( + "unexpected shell stdout:\n{stdout}\nstderr:\n{stderr}" + )) + } + + enum ShellDecision { + Leave, + Rewrite(String), + } + + #[test] + fn shell_transform_matches_rust_decision() { + let fixtures = [ + live_daemon_conf(), + "", + "extra-system-features = surmount-remote other-token\nsystem-features = nixos-test benchmark big-parallel kvm\n", + "extra-system-features=surmount-remote\nsystem-features = nixos-test benchmark big-parallel kvm\n", + "extra-system-features = surmount-remote\nsystem-features = nixos-test benchmark big-parallel kvm gccarch-x86-64-v3\n", + "max-jobs = 8\nsystem-features = nixos-test benchmark big-parallel kvm\nsandbox = true\n", + "# system-features = gccarch-x86-64-v3\nsystem-features = nixos-test benchmark big-parallel kvm\n", + "extra-system-features = gccarch-x86-64-v3 surmount-remote extra\nsystem-features = nixos-test gccarch-x86-64-v30\n", + "system-features = gccarch-x86-64-v3\nsystem-features = nixos-test benchmark big-parallel kvm\nextra-system-features = surmount-remote\n", + "system-features = nixos-test benchmark big-parallel kvm\nextra-system-features = surmount-remote\nsystem-features = nixos-test benchmark big-parallel kvm gccarch-x86-64-v3\n", + " extra-system-features = surmount-remote\nsystem-features = nixos-test benchmark big-parallel kvm\n", + "extra-system-features = surmount-remote\nextra-system-features = other\nsystem-features = nixos-test benchmark big-parallel kvm\n", + "extra-system-features = \"surmount-remote\"\nsystem-features = nixos-test benchmark big-parallel kvm\n", + "\n", + "system-features = nixos-test benchmark big-parallel kvm", + ]; + for (idx, fixture) in fixtures.iter().enumerate() { + let rust = bootstrap_gccarch_conf(fixture); + let shell = shell_decision(fixture).unwrap_or_else(|err| { + panic!("fixture {idx} shell failed: {err}\ninput:\n{fixture}") + }); + match (rust, shell) { + (GccarchConfDecision::LeaveUnchanged, ShellDecision::Leave) => {} + (GccarchConfDecision::Rewrite(expected), ShellDecision::Rewrite(got)) => { + assert_eq!( + got, expected, + "fixture {idx} shell drifted from the rust transform\ninput:\n{fixture}" + ); + } + (rust, _) => { + panic!( + "fixture {idx} shell decision does not match rust: {rust:?}\ninput:\n{fixture}" + ) + } + } + } + } + + #[test] + fn shell_blob_token_is_exact() { + let script = format!( + "deploy_host_gccarch_token={GCCARCH_X86_64_V3}\n\ + deploy_host_run_nix_conf={RUN_NIX_CONF}\n\ + {SHELL_FUNCTIONS}\n\ + if deploy_host_blob_has_token \"$1\"; then echo YES; else echo NO; fi\n" + ); + let cases = [ + ( + "benchmark big-parallel kvm nixos-test surmount-remote", + "NO", + ), + ( + "benchmark big-parallel kvm nixos-test surmount-remote gccarch-x86-64-v3", + "YES", + ), + ("[ \"benchmark\" \"gccarch-x86-64-v3\" ]", "YES"), + ("gccarch-x86-64-v30", "NO"), + ("gccarch-x86-64", "NO"), + ]; + for (blob, want) in cases { + let out = Command::new("sh") + .arg("-c") + .arg(&script) + .arg("sh") + .arg(blob) + .output() + .expect("sh"); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!( + out.status.success(), + "blob {blob:?} stderr {}", + String::from_utf8_lossy(&out.stderr) + ); + assert_eq!(stdout.trim(), want, "blob {blob:?} stdout {stdout}"); + } + } + + #[test] + fn remote_driver_restarts_only_when_the_daemon_omits_the_feature() { + let script = gccarch_bootstrap_remote(); + assert!(script.contains(GCCARCH_X86_64_V3)); + assert!(script.contains(SURMOUNT_REMOTE)); + assert!(script.contains(RUN_NIX_CONF)); + assert!(script.contains("nix config show system-features")); + assert!( + !script.contains("nix config show extra-system-features"), + "Nix 2.34.9 has no extra-system-features show key" + ); + assert!(!script.contains("> \"$store_conf\"")); + assert!( + script + .contains("deploy_host_rewrite_conf \"$store_conf\" \"$deploy_host_run_nix_conf\"") + ); + assert!(script.contains("mount --bind")); + assert!(script.contains("systemctl restart nix-daemon")); + assert!(script.contains( + "gccarch-x86-64-v3 still absent from nix config show system-features; not starting the system build" + )); + let check = script + .find("if deploy_host_blob_has_token \"$deploy_host_daemon_features\"") + .expect("daemon check"); + let else_at = script[check..].find("\nelse\n").expect("else branch") + check; + let restart = script + .find("systemctl restart nix-daemon") + .expect("restart"); + assert!( + restart > else_at, + "restart must sit in the branch that runs only when the feature is missing" + ); + assert!( + !script[..else_at].contains("systemctl restart"), + "already-advertised feature must not restart nix-daemon" + ); + assert_eq!(script.matches("systemctl restart nix-daemon").count(), 1); + } +} diff --git a/crates/surmount-deploy-host/src/run.rs b/crates/surmount-deploy-host/src/run.rs index 3f77cde..6c4a5f1 100644 --- a/crates/surmount-deploy-host/src/run.rs +++ b/crates/surmount-deploy-host/src/run.rs @@ -314,6 +314,9 @@ fn run_deploy(opts: DeployOpts) -> Result<(), ToolError> { println!( "deploy-host: dry-run: rebuild uses path: flake so host-local (rsync'd, not in public git) is visible to Nix (git flake would omit untracked/gitignored paths)" ); + println!( + "deploy-host: dry-run: before the system build, bootstrap gccarch-x86-64-v3 when nix config show system-features omits it (write /run/surmount-nix.conf, bind-mount over the store nix.conf, restart nix-daemon only if the feature was missing). Dry-run does not apply that bootstrap. A /run bind does not survive reboot." + ); println!( "deploy-host: dry-run: ssh -- {} {}", shell_quote(&opts.target), @@ -530,13 +533,17 @@ fn ssh_status_label(code: Option) -> String { } /// One remote switch. Prints `synthetic-built-generation:` after the rebuild. -/// A new profile link (`system-N-link`) is the generation the switch built. -/// The same link name as before the switch is `unchanged`, not a generation. -/// The fake ssh matches `*nixos-rebuild*` before `*readlink*` and does not run -/// this script; it prints the same generation line itself. +/// Before that build, bootstrap `gccarch-x86-64-v3` on the running daemon +/// when `nix config show` system-features omits it. The bootstrap writes +/// `/run/surmount-nix.conf` and bind-mounts it. That bind does not survive +/// reboot. A new profile link (`system-N-link`) is the generation the switch +/// built. The same link name as before the switch is `unchanged`, not a +/// generation. The fake ssh matches `*nixos-rebuild*` before `*readlink*` +/// and does not run this script; it prints the same generation line itself. fn switch_remote_command(flake_ref: &str) -> String { let flake_q = shell_quote(flake_ref); - [ + let bootstrap = crate::nix_conf::gccarch_bootstrap_remote(); + let switch = [ "before=$(readlink /nix/var/nix/profiles/system 2>/dev/null || true)", // Live nix.conf still has max-jobs 8 until this generation // switches. 32 matches surmount.remoteBuilder.maxJobs. cores 0 @@ -549,7 +556,8 @@ fn switch_remote_command(flake_ref: &str) -> String { "if [ -n \"$after\" ] && [ \"$after\" != \"$before\" ]; then printf '%s\\n' \"synthetic-built-generation: $after\"; else printf '%s\\n' \"synthetic-built-generation: unchanged\"; fi", "exit \"$rc\"", ] - .join("; ") + .join("; "); + format!("{bootstrap}\n{switch}") } /// Later readlink. The fake ssh prints the booted generation number for any @@ -961,5 +969,33 @@ mod tests { assert!(cmd.contains("path:/tmp/repo#mail-vps")); assert!(cmd.contains("synthetic-built-generation:")); assert!(!cmd.contains("post-switch-smoke")); + let rebuild_at = cmd + .find("nixos-rebuild switch") + .expect("system build command"); + let bootstrap = &cmd[..rebuild_at]; + assert!( + bootstrap.contains("/run/surmount-nix.conf"), + "bootstrap must run before the system build" + ); + assert!( + bootstrap.contains("gccarch-x86-64-v3"), + "bootstrap must name the missing feature" + ); + assert!( + !bootstrap.contains("nixos-rebuild"), + "bootstrap text must not add a second nixos-rebuild line for the fake ssh log" + ); + } + + #[test] + fn switch_bootstraps_gccarch_before_the_system_build() { + let cmd = switch_remote_command("path:/tmp/repo#mail-vps"); + let rebuild_at = cmd.find("nixos-rebuild switch").expect("rebuild"); + assert!(cmd.find("/run/surmount-nix.conf").unwrap() < rebuild_at); + assert!(cmd.find("mount --bind").unwrap() < rebuild_at); + assert!(cmd.find("systemctl restart nix-daemon").unwrap() < rebuild_at); + assert!(cmd.find("nix config show system-features").unwrap() < rebuild_at); + assert!(!cmd.contains("nix config show extra-system-features")); + assert!(cmd.contains("not starting the system build")); } } diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index 6d220a5..14af93c 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -4,10 +4,19 @@ Read this first, then follow links into child docs. Do not invent product scope from memory alone. -**Last updated:** 2026-11-04 (surmount-1 nix-daemon must list -gccarch-x86-64-v3 before `just deploy` can build the x86-64-v3 -stdenv; the evaluated mail-vps config already requests it, and a -`/run` bind of nix.conf does not survive reboot). Prior 2026-11-03 +**Last updated:** 2026-11-04 (`surmount-deploy-host` bootstraps +gccarch-x86-64-v3 on the target before the system build when the +running nix-daemon omits that feature. The tool writes +`/run/surmount-nix.conf`, bind-mounts it over the store nix.conf, and +restarts nix-daemon only when the feature was missing. The `/run` bind +does not survive reboot. A successful switch is what puts the feature +in the generation's nix.conf. An operator or an agent does not SSH a +bind before `just deploy`. The evaluated mail-vps config still +requests the feature for the next generation). Prior 2026-11-04 +(surmount-1 nix-daemon must list gccarch-x86-64-v3 before `just +deploy` can build the x86-64-v3 stdenv; the evaluated mail-vps config +already requests it, and a `/run` bind of nix.conf does not survive +reboot). Prior 2026-11-03 (number every parallel list in operator chat; horizon is this laptop; surmount-1 is the mail host and the remote Nix builder; the private overlay directory is not a machine). diff --git a/docs/OPS.md b/docs/OPS.md index 7273431..5cbcba4 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -875,13 +875,26 @@ activates the new `nix.conf`. The switch builds the v3 stdenv first. A cache miss (zlib, gcc, glibc, and the rest of that chain) requires `gccarch-x86-64-v3` on the daemon that is already running. A `NIX_CONFIG` drop-in does not make that build check pass. The conf -the daemon reads is the store file behind `/etc/nix/nix.conf`. As -root, put `extra-system-features = surmount-remote gccarch-x86-64-v3` -in a copy of that file and bind-mount the copy over the store path, -then restart `nix-daemon`. Keep `benchmark`, `big-parallel`, `kvm`, -`nixos-test`, and `surmount-remote`. A bind under `/run` does not -survive reboot. `just deploy-dry-run` only prints planned commands -and does not build, so a green dry-run does not show this feature. +the daemon reads is the store file behind `/etc/nix/nix.conf`. +`surmount-deploy-host` bootstraps that feature on the target before +the system build. When `nix config show` system-features omits +`gccarch-x86-64-v3`, the tool reads that conf, writes +`extra-system-features` so the line contains `surmount-remote` and +`gccarch-x86-64-v3` (other tokens on that line stay), leaves the +`system-features` line unchanged (`benchmark`, `big-parallel`, `kvm`, +and `nixos-test` stay on that line; `surmount-remote` stays on the +extra line), copies the result to +`/run/surmount-nix.conf`, and bind-mounts that file over the store +path. It does not write the store file in place. It restarts +`nix-daemon` only when the feature was missing. If the daemon already +advertises the feature, it does not rewrite the conf and does not +restart the daemon. If the feature is still absent after that +restart, the tool stops and does not start the system build. The +`/run` bind does not survive reboot. A successful switch is what puts +the feature in the generation's `nix.conf`. An operator or an agent +does not SSH a bind before `just deploy`. `just deploy-dry-run` only +prints planned commands and does not build, so a green dry-run does +not apply this bootstrap. The nixpkgs cc-wrapper reads `targetPlatform.gcc.arch` and writes `-march=x86-64-v3` into @@ -1049,8 +1062,11 @@ missing system features. `extra-` keeps NixOS auto-detected `/etc/nix/nix.conf` picks up the extra feature; then restart is implied by the switch. That order works for `surmount-remote` only when the daemon can already build the closure. `gccarch-x86-64-v3` -is required to build the v3 stdenv, so it has to be on the running -daemon before that switch. See Mail-host compiler CPU above. +is required to build the v3 stdenv, so `surmount-deploy-host` +bootstraps it on the running daemon before that system build. See +Mail-host compiler CPU above. The `/run` bind does not survive +reboot. A successful switch is what puts the feature in the +generation's `nix.conf`. Do not advertise a fake high slot count. Speed factor is not a substitute for `MemoryMax`. Disk refuse at `diskGuardPercent` (default 95). Enable from private host-local. Host-local may set a diff --git a/docs/TEST-AND-DEPLOY.md b/docs/TEST-AND-DEPLOY.md index ac99541..1964dec 100644 --- a/docs/TEST-AND-DEPLOY.md +++ b/docs/TEST-AND-DEPLOY.md @@ -326,8 +326,11 @@ What the dry-run prints (it must not rebuild the guest): `crates/target/`, secrets patterns) - rsync of private host-local to `root@surmount-1:/root/surmount-server/host-local/` -- `nixos-rebuild switch --flake path:/root/surmount-server#mail-vps` - as a planned remote command +- planned remote command that bootstraps `gccarch-x86-64-v3` when + the daemon's system-features omit it, then + `nixos-rebuild switch --flake path:/root/surmount-server#mail-vps`. + Dry-run prints that plan and does not apply the bootstrap, does not + bind-mount, and does not restart nix-daemon - post-switch smoke as a planned remote command Success is a printed plan and exit 0. It is not a new generation. @@ -367,7 +370,17 @@ Explicit equivalent (`just deploy-host` has no default target): just deploy-host -- --target root@surmount-1 --host-local /home/hunter/.local/share/surmount/host-local ``` -That rsyncs, then runs `nixos-rebuild switch` on the guest. A generation +That rsyncs, then on the guest bootstraps `gccarch-x86-64-v3` when +`nix config show` system-features omits it, then runs `nixos-rebuild +switch`. The bootstrap reads the nix.conf the daemon is using, writes +`/run/surmount-nix.conf` (it does not write the store file in place), +bind-mounts that file over the store path, and restarts nix-daemon +only when the feature was missing. If the feature is still absent, the +tool stops and does not start the system build. If the daemon already +advertises the feature, it does not rewrite the conf and does not +restart the daemon. The `/run` bind does not survive reboot. A +successful switch is what puts the feature in the generation's +nix.conf. Do not SSH a separate bind before `just deploy`. A generation that compiles Splora can sit on one `building ...drv` line for a long time. Check guest rustc before killing it. diff --git a/modules/mail-host-cpu.nix b/modules/mail-host-cpu.nix index 686d0d8..f7ed330 100644 --- a/modules/mail-host-cpu.nix +++ b/modules/mail-host-cpu.nix @@ -206,6 +206,9 @@ in # The v3 stdenv requires gccarch-x86-64-v3. extra-system-features is # a freeform list and concatenates, so this does not drop # surmount-remote or the daemon's system-features defaults. + # This list is the next generation. It does not change the daemon + # that builds that generation. surmount-deploy-host bootstraps the + # running daemon before the system build. nix.settings.extra-system-features = [ "gccarch-${march}" ]; nixpkgs.overlays = lib.mkAfter [ mailHostCpuOverlay ]; From 998b35a8a7aadef5ef035741b0570499dfabcdb1 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Thu, 5 Nov 2026 01:16:13 -0700 Subject: [PATCH 15/22] fixes --- README.md | 20 ++++++++----- flake.lock | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++---- flake.nix | 52 +++++++++++++++++++++++----------- 3 files changed, 126 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a1efbd3..d149de1 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,16 @@ nix build .#management-ui required (Stalwart chip stays down until something answers `SURMOUNT_STALWART_URL`). Ctrl-C stops it. Override any `SURMOUNT_*` env before invoking. -**nixfmt (Nix formatter):** the flake already ships `nixfmt-rfc-style` as -`packages.nixfmt`, `formatter`, and in `devShells.default`. Prefer the flake -over a distro package so CI and laptop match. +**nixfmt (Nix formatter):** the flake ships the Rust formatter +[nixfmt-rs](https://github.com/Mic92/nixfmt-rs) (accessed: 2026-11-05), +pinned at `c22b2c9ef0ad3b66f97ebd6296fbba298aa9e039` (default-branch HEAD +read 2026-11-05), as `packages.nixfmt`, `formatter`, and in +`devShells.default`. The binary is still named `nixfmt`, and `nixfmt +--check` is the check flag. This is not Haskell `pkgs.nixfmt` and not +`nixfmt-rfc-style` (on nixos-26.05 that alias is the Haskell package). +The nixfmt-rs author claims byte-identical output with Haskell nixfmt +1.2.0, not with 1.5.0. Prefer the flake over a distro package so CI and +the laptop match. ```bash # one-shot (no install) @@ -152,10 +159,9 @@ On Arch, you do **not** need a pacman/AUR package for day-to-day work if you use the flake above. If you still want a system package for other repos: ```bash -# AUR (names vary; check with paru/yay search) -paru -S nixfmt # or nixfmt-bin / nixfmt-git, depending on AUR -# or always-nix from nixpkgs (same RFC style as this flake): -nix profile install nixpkgs#nixfmt-rfc-style +# AUR names vary (nixfmt, nixfmt-bin, nixfmt-git). This flake does not +# use them. Do not install nixpkgs#nixfmt-rfc-style for this repo: that +# name is the Haskell package. ``` **Host quality bar:** `just check` runs format check (no write), clippy with diff --git a/flake.lock b/flake.lock index 4ef939d..aa2f1c0 100644 --- a/flake.lock +++ b/flake.lock @@ -63,18 +63,38 @@ "type": "github" } }, + "nixfmt-rs": { + "inputs": { + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1792014159, + "narHash": "sha256-6+Wf6C28VrpbT6Jx7qtP4QfNJlwmYCLy5j+VfTkbnXg=", + "owner": "Mic92", + "repo": "nixfmt-rs", + "rev": "c22b2c9ef0ad3b66f97ebd6296fbba298aa9e039", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "nixfmt-rs", + "rev": "c22b2c9ef0ad3b66f97ebd6296fbba298aa9e039", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1793633738, - "narHash": "sha256-tItrb9M8eYA6WhFgmT9tpGKueri3+wRuVA952o/2xSU=", + "lastModified": 1791048980, + "narHash": "sha256-KgItSKML8Xvte0B7/uGnBDsYzSnnKHcOaiUbgWBXLXw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7dcdd87373d72e7a2173e4db02e20d3d7f495cfd", + "rev": "a7868a727837f3c09cee2ce0ca671c76b1589fed", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-26.05", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -96,6 +116,38 @@ } }, "nixpkgs_2": { + "locked": { + "lastModified": 1790149054, + "narHash": "sha256-FfjXg6mAzKQBCY+7V9keGWOXG4DV2Eym/qA987lFl8A=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "8825bebf6324e0579d012936eff73379af284b6d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1793633738, + "narHash": "sha256-tItrb9M8eYA6WhFgmT9tpGKueri3+wRuVA952o/2xSU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7dcdd87373d72e7a2173e4db02e20d3d7f495cfd", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { "locked": { "lastModified": 1789785513, "narHash": "sha256-B44WL6h0XoLjJ41bUPJk0X5SDinLCII//6EcBLXKiJ0=", @@ -115,7 +167,8 @@ "inputs": { "advisory-db": "advisory-db", "crane": "crane", - "nixpkgs": "nixpkgs", + "nixfmt-rs": "nixfmt-rs", + "nixpkgs": "nixpkgs_3", "nixpkgs-rust": "nixpkgs-rust", "sops-nix": "sops-nix", "splora": "splora", @@ -168,7 +221,7 @@ "inputs": { "asset-registry": "asset-registry", "crane": "crane_2", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs_4", "rust-overlay": "rust-overlay" }, "locked": { @@ -201,6 +254,24 @@ "repo": "site", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1791490544, + "narHash": "sha256-qScE7Wz6Zg7eg8ItUpnkmKgst8weWschGe0E8jM4rj4=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "4cfe8d7065cc8ca91c0a50361b6dd74920eeb1f5", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 2e3efc8..63fc47e 100644 --- a/flake.nix +++ b/flake.nix @@ -70,6 +70,15 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + # Rust nixfmt (Mic92/nixfmt-rs), not Haskell pkgs.nixfmt. + # Default-branch HEAD c22b2c9ef0ad3b66f97ebd6296fbba298aa9e039, + # read 2026-11-05 (commit date 2026-10-14). Do not float main. + # Their published byte match is Haskell nixfmt 1.2.0, not this + # channel's pkgs.nixfmt 1.5.0. Do not set inputs.nixpkgs.follows: + # their flake pins its own nixpkgs, so this repo's gcc.arch overlay + # cannot reach the formatter build. + nixfmt-rs.url = "github:Mic92/nixfmt-rs/c22b2c9ef0ad3b66f97ebd6296fbba298aa9e039"; + # Public static site for apex/www. Fetch the git tree (HTML/CSS/JS). # Operator bumps: just publish (nix flake update surmount-site). surmount-site = { @@ -97,6 +106,7 @@ nixpkgs-rust, crane, sops-nix, + nixfmt-rs, surmount-site, advisory-db, splora, @@ -446,10 +456,14 @@ splora-liquid = sploraInput.packages.${system}.splora-liquid or (throw "splora flake input has no packages.${system}.splora-liquid (fail-closed)"); - # RFC-style nixfmt (same as formatter / checks.ci / devShell). - # Use: nix run .#nixfmt -- file.nix or nix shell .#nixfmt -c nixfmt … - # nixos-26.05: nixfmt-rfc-style is an alias of pkgs.nixfmt (prefer the latter). - nixfmt = pkgs.nixfmt; + # Rust nixfmt-rs (Mic92). Not Haskell pkgs.nixfmt and not + # nixfmt-rfc-style (that alias is the channel Haskell package). + # Upstream attribute is packages.${system}.default. Binary name + # is nixfmt. Author claims byte-identical output with Haskell + # nixfmt 1.2.0, not with nixos-26.05 pkgs.nixfmt 1.5.0. + # Pin c22b2c9ef0ad3b66f97ebd6296fbba298aa9e039, read 2026-11-05. + # Use: nix run .#nixfmt -- file.nix or nix shell .#nixfmt -c nixfmt ... + nixfmt = nixfmt-rs.packages.${system}.default; # Flake-pinned just for GHA (`nix shell .#just -c just ci`) and local # bootstrap without a host just install. just = pkgs.just; @@ -981,7 +995,10 @@ nix-fmt-check = pkgs.runCommand "nix-fmt-check" { - nativeBuildInputs = [ pkgs.nixfmt ]; + nativeBuildInputs = [ + # Rust nixfmt-rs. Not pkgs.nixfmt (Haskell, compiles GHC). + nixfmt-rs.packages.${system}.default + ]; } '' set -euo pipefail @@ -1116,18 +1133,19 @@ in { default = pkgs.mkShell { - packages = with pkgs; [ + packages = [ # Match crane toolchain (nix/rust-toolchain.nix, nixpkgs-rust 1.99.1). (import ./nix/rust-toolchain.nix { pkgs = mkPkgsRust system; }) - rust-analyzer - pkg-config - nixfmt - just - sops - age - ssh-to-age - git - jq + pkgs.rust-analyzer + pkgs.pkg-config + # Rust nixfmt-rs. Not pkgs.nixfmt (Haskell, compiles GHC). + nixfmt-rs.packages.${system}.default + pkgs.just + pkgs.sops + pkgs.age + pkgs.ssh-to-age + pkgs.git + pkgs.jq ]; RUST_LOG = "info,surmount_management_ui=debug"; shellHook = '' @@ -1154,6 +1172,8 @@ } ); - formatter = forAllSystems (system: (mkPkgs system).nixfmt); + # Rust nixfmt-rs. Not (mkPkgs system).nixfmt: that attr is Haskell + # pkgs.nixfmt, and the mail-host gcc.arch overlay must not build it. + formatter = forAllSystems (system: nixfmt-rs.packages.${system}.default); }; } From 39e08da8a48515c70e06eb4a37bc6d9d09feece2 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Thu, 5 Nov 2026 13:26:28 -0700 Subject: [PATCH 16/22] fixes --- crates/management-ui/src/http3.rs | 1 + crates/management-ui/src/proxy_vaultwarden.rs | 567 +++++++++++++++--- crates/management-ui/src/tls.rs | 1 + docs/COMPACTION-PIN.md | 7 +- docs/EDGE_AND_TLS.md | 43 +- docs/operator-direction.md | 4 +- docs/packages-and-forks.md | 21 +- docs/research/version-audit.md | 51 +- flake.lock | 8 +- flake.nix | 28 +- hosts/mail-vps/configuration.nix | 6 +- modules/arti-hidden-service.nix | 6 +- modules/hardening.nix | 64 +- modules/mail-host-rust-cpu.nix | 12 +- modules/management-ui.nix | 23 +- modules/options.nix | 4 +- nix/overlays.nix | 83 ++- nix/packages/arti-onion-service.nix | 57 +- nix/packages/splora-web.nix | 33 + tests/arti-onion-package.nix | 18 +- tests/fail2ban-permanent-eval.nix | 137 ++--- tests/mail-host-rust-cpu.nix | 3 +- tests/module-eval.nix | 19 +- 23 files changed, 880 insertions(+), 316 deletions(-) create mode 100644 nix/packages/splora-web.nix diff --git a/crates/management-ui/src/http3.rs b/crates/management-ui/src/http3.rs index dbb3681..9da7f92 100644 --- a/crates/management-ui/src/http3.rs +++ b/crates/management-ui/src/http3.rs @@ -371,6 +371,7 @@ mod tests { queue_path: DEFAULT_QUEUE_PATH.into(), body_limit_bytes: DEFAULT_BODY_LIMIT_BYTES, portal_host: None, + web_root: PathBuf::new(), }; let http3_cfg = Http3Config { enable: true, diff --git a/crates/management-ui/src/proxy_vaultwarden.rs b/crates/management-ui/src/proxy_vaultwarden.rs index 5430103..5384a16 100644 --- a/crates/management-ui/src/proxy_vaultwarden.rs +++ b/crates/management-ui/src/proxy_vaultwarden.rs @@ -1011,7 +1011,8 @@ mod tests { /// `/run/splora/.http.sock`. The queue is a separate socket and /// path. NIP-98 stays in splora. This edge does not add API keys. Not nginx. /// Not the Electrum newline socket (fail-closed if that socket is configured). -/// Not an explorer: no block, transaction, or address pages. +/// Block, transaction, and address paths under a network prefix are the Trunk +/// shell, not indexer REST. Unprefixed `/block`, `/tx`, and `/address` stay unmatched. pub(crate) mod splora { use std::collections::BTreeMap; @@ -1086,6 +1087,10 @@ pub(crate) mod splora { /// Public portal Host (one DNS name). Not an indexer. Env /// `SURMOUNT_SPLORA_PORTAL_HOST`. None when the proxy is off or unset. pub portal_host: Option, + /// Trunk `dist` directory (the `splora-web` package output). Env + /// `SURMOUNT_SPLORA_WEB_ROOT`. Empty when unset. A missing directory + /// does not invent HTML. + pub web_root: PathBuf, } impl Default for SploraProxyConfig { @@ -1097,6 +1102,7 @@ pub(crate) mod splora { queue_path: DEFAULT_QUEUE_PATH.to_string(), body_limit_bytes: DEFAULT_BODY_LIMIT_BYTES, portal_host: None, + web_root: PathBuf::new(), } } } @@ -1156,6 +1162,11 @@ pub(crate) mod splora { ); } + let web_root = match get("SURMOUNT_SPLORA_WEB_ROOT") { + Some(v) if !v.trim().is_empty() => PathBuf::from(v.trim()), + _ => PathBuf::new(), + }; + let portal_host = parse_portal_host(get("SURMOUNT_SPLORA_PORTAL_HOST"), enable)?; if let Some(ref portal) = portal_host { if instances @@ -1176,6 +1187,7 @@ pub(crate) mod splora { queue_path, body_limit_bytes: DEFAULT_BODY_LIMIT_BYTES, portal_host, + web_root, }) } @@ -1268,12 +1280,21 @@ pub(crate) mod splora { } /// Where one public path goes. No redirect arm: `/signet` and `/mutinynet` - /// are both proxies to the mutinynet socket. + /// are both the mutinynet indexer. Screen routes and the prefix root are + /// the Trunk `index.html` shell. `/api` stays the indexer socket. #[derive(Debug, Clone, PartialEq, Eq)] pub enum SploraPublicRoute { Index, Queue, NotFound, + /// Trunk CSR shell, or a file under the web root. Not proxied. + /// Not mainnet. `rel` is the path inside the Trunk dist + /// (`""` is `index.html`). + Frontend { + instance: &'static str, + prefix: &'static str, + rel: String, + }, Proxy { socket_name: &'static str, backend_path: String, @@ -1284,15 +1305,16 @@ pub(crate) mod splora { /// Path routing for the portal Host. /// - /// Network prefix is stripped first. The query string is kept. A - /// remainder of `/api` or `/api/...` keeps the Splora REST rule: drop a - /// leading `/api`, except exact `/api/v1/ws`, which stays and is the - /// only upgrade. Any other remainder (`/block/...`, `/tx/...`, - /// `/address/...`, `/`, and later UI paths) is forwarded to that network - /// socket. This repo does not render those pages. Unprefixed `/block`, - /// `/tx`, and `/address` are not routes here. `/testnet3` is not - /// `/testnet`. The queue path is not a network prefix. `/api` with no - /// network prefix is mainnet, and is not proxied when mainnet is absent. + /// Network prefix is stripped first. The query string is kept on indexer + /// REST. A remainder of `/api` or `/api/...` keeps the Splora REST rule: + /// drop a leading `/api`, except exact `/api/v1/ws`, which stays and is + /// the only upgrade. Any other remainder is the Trunk shell (SPA + /// fallback) unless that relative path is a file in the web root. That + /// does not include `/docs/api/...`, which is a screen, not indexer REST. + /// Unprefixed `/block`, `/tx`, and `/address` are not routes here. + /// `/testnet3` is not `/testnet`. The queue path is not a network prefix. + /// `/api` with no network prefix is mainnet REST, not a frontend, + /// and is not proxied when mainnet is absent. pub fn route_splora_public( path: &str, query: Option<&str>, @@ -1311,22 +1333,22 @@ pub(crate) mod splora { _ => None, }; if let Some(rest) = strip_named_prefix(path, "/testnet4") { - return proxy_after_network("testnet4", rest, kept, enabled); + return route_after_network("testnet4", "/testnet4", rest, kept, enabled); } if let Some(rest) = strip_named_prefix(path, "/testnet") { - return proxy_after_network("testnet3", rest, kept, enabled); + return route_after_network("testnet3", "/testnet", rest, kept, enabled); } if let Some(rest) = strip_named_prefix(path, "/signet") { - return proxy_after_network("mutinynet", rest, kept, enabled); + return route_after_network("mutinynet", "/signet", rest, kept, enabled); } if let Some(rest) = strip_named_prefix(path, "/mutinynet") { - return proxy_after_network("mutinynet", rest, kept, enabled); + return route_after_network("mutinynet", "/mutinynet", rest, kept, enabled); } if let Some(rest) = strip_named_prefix(path, "/liquid") { - return proxy_after_network("liquid", rest, kept, enabled); + return route_after_network("liquid", "/liquid", rest, kept, enabled); } if path == "/api" || path.starts_with("/api/") { - return proxy_after_network("mainnet", path, kept, enabled); + return route_after_network("mainnet", "", path, kept, enabled); } SploraPublicRoute::NotFound } @@ -1343,8 +1365,9 @@ pub(crate) mod splora { } } - fn proxy_after_network( + fn route_after_network( socket_name: &'static str, + public_prefix: &'static str, after_network: &str, query: Option, enabled: &[&str], @@ -1353,28 +1376,107 @@ pub(crate) mod splora { return SploraPublicRoute::NotFound; } let rest_is_api = after_network == "/api" || after_network.starts_with("/api/"); - let backend_path = if rest_is_api { - backend_path_for_indexer(after_network) - } else { - forward_ui_remainder(after_network) - }; - SploraPublicRoute::Proxy { - socket_name, - backend_path, - query, - websocket: rest_is_api && after_network == INDEXER_WS_PATH, + if rest_is_api { + return SploraPublicRoute::Proxy { + socket_name, + backend_path: backend_path_for_indexer(after_network), + query, + websocket: after_network == INDEXER_WS_PATH, + }; + } + // Mainnet `/api` is the only unprefixed indexer path. Do not mount + // the Trunk app on it. + if socket_name == "mainnet" { + return SploraPublicRoute::NotFound; + } + SploraPublicRoute::Frontend { + instance: socket_name, + prefix: public_prefix, + rel: trunk_rel(after_network), } } - /// Remainder after the network prefix, when it is not REST. - /// Empty is the prefix itself (`/testnet4`). Not HTML from this repo. - fn forward_ui_remainder(after_network: &str) -> String { - if after_network.is_empty() || after_network == "/" { - "/".to_string() - } else if after_network.starts_with('/') { - after_network.to_string() - } else { - format!("/{after_network}") + /// Path inside the Trunk dist. Empty means `index.html`. + fn trunk_rel(after_network: &str) -> String { + after_network.trim_start_matches('/').to_string() + } + + /// Trunk dist file, or `index.html` for a client route. A missing + /// web root does not invent HTML. + fn frontend_response(method: &Method, web_root: &std::path::Path, rel: &str) -> Response { + if *method != Method::GET && *method != Method::HEAD { + return (StatusCode::METHOD_NOT_ALLOWED, "GET only").into_response(); + } + if web_root.as_os_str().is_empty() || !web_root.is_dir() { + return (StatusCode::NOT_FOUND, "not found").into_response(); + } + if !rel.is_empty() { + let Some(path) = web_root_join(web_root, rel) else { + return (StatusCode::NOT_FOUND, "not found").into_response(); + }; + if path.is_file() { + return web_file_response(&path); + } + if trunk_rel_is_asset(rel) { + return (StatusCode::NOT_FOUND, "not found").into_response(); + } + } + let index = web_root.join("index.html"); + if index.is_file() { + return web_file_response(&index); + } + (StatusCode::NOT_FOUND, "not found").into_response() + } + + fn web_root_join(root: &std::path::Path, rel: &str) -> Option { + if rel.contains('\0') { + return None; + } + let mut out = root.to_path_buf(); + for seg in rel.split('/') { + if seg.is_empty() || seg == "." || seg == ".." { + return None; + } + out.push(seg); + } + Some(out) + } + + /// A dotted final segment is a Trunk asset. A missing asset is 404. + /// A path with no extension is a client route and falls back to the shell. + fn trunk_rel_is_asset(rel: &str) -> bool { + let name = rel.rsplit('/').next().unwrap_or(rel); + match name.rsplit_once('.') { + Some((stem, ext)) => !stem.is_empty() && !ext.is_empty(), + None => false, + } + } + + fn web_file_response(path: &std::path::Path) -> Response { + match std::fs::read(path) { + Ok(bytes) => ( + StatusCode::OK, + [(header::CONTENT_TYPE, web_content_type(path))], + bytes, + ) + .into_response(), + Err(_) => (StatusCode::NOT_FOUND, "not found").into_response(), + } + } + + fn web_content_type(path: &std::path::Path) -> &'static str { + match path.extension().and_then(|ext| ext.to_str()) { + Some("html") => "text/html; charset=utf-8", + Some("js") | Some("mjs") => "text/javascript; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("wasm") => "application/wasm", + Some("json") | Some("map") => "application/json", + Some("svg") => "image/svg+xml", + Some("png") => "image/png", + Some("ico") => "image/x-icon", + Some("woff2") => "font/woff2", + Some("txt") => "text/plain; charset=utf-8", + _ => "application/octet-stream", } } @@ -1581,6 +1683,9 @@ pub(crate) mod splora { SploraPublicRoute::Queue => proxy_queue(state, request).await, SploraPublicRoute::Index => portal_response(cfg, request.method(), &path), SploraPublicRoute::NotFound => (StatusCode::NOT_FOUND, "not found").into_response(), + SploraPublicRoute::Frontend { rel, .. } => { + frontend_response(request.method(), &cfg.web_root, &rel) + } SploraPublicRoute::Proxy { socket_name, backend_path, @@ -2104,6 +2209,18 @@ pub(crate) mod splora { let cfg = SploraProxyConfig::from_env_map(|_| None).unwrap(); assert!(!cfg.enable); assert!(cfg.instances.is_empty()); + assert!(cfg.web_root.as_os_str().is_empty()); + } + + #[test] + fn web_root_comes_from_env() { + let cfg = SploraProxyConfig::from_env_map(|k| match k { + "SURMOUNT_SPLORA_WEB_ROOT" => Some("/nix/store/splora-web".into()), + _ => None, + }) + .unwrap(); + assert_eq!(cfg.web_root, PathBuf::from("/nix/store/splora-web")); + assert!(!cfg.enable); } #[test] @@ -2170,6 +2287,7 @@ pub(crate) mod splora { queue_path: DEFAULT_QUEUE_PATH.into(), body_limit_bytes: DEFAULT_BODY_LIMIT_BYTES, portal_host: Some(portal.to_string()), + web_root: PathBuf::new(), } } @@ -2202,6 +2320,157 @@ pub(crate) mod splora { } } + fn expect_shell(route: SploraPublicRoute, instance: &str, prefix: &str, rel: &str) { + match route { + SploraPublicRoute::Frontend { + instance: got_instance, + prefix: got_prefix, + rel: got_rel, + } => { + assert_eq!(got_instance, instance); + assert_eq!(got_prefix, prefix); + assert_eq!(got_rel, rel); + } + other => { + panic!("expected trunk shell {instance} {prefix} rel={rel}, got {other:?}") + } + } + } + + /// Named contract: client paths under a public prefix are the Trunk + /// shell, not a screen list and not indexer REST. `/testnet` is + /// testnet3. `/signet` and `/mutinynet` are mutinynet. `/api` is + /// not a frontend. `/testnet3` is not `/testnet`. + #[test] + fn splora_trunk_shell_routes_follow_public_prefixes() { + let rows = [ + ("/testnet", "testnet3", "/testnet", "dashboard"), + ("/testnet/", "testnet3", "/testnet", "dashboard"), + ("/testnet/blocks", "testnet3", "/testnet", "blocks"), + ("/testnet4/block/abc", "testnet4", "/testnet4", "block"), + ( + "/testnet4/block-height/abc", + "testnet4", + "/testnet4", + "block-height", + ), + ("/liquid/asset/abc", "liquid", "/liquid", "asset"), + ("/liquid/assets", "liquid", "/liquid", "assets"), + ( + "/liquid/assets/asset/abc", + "liquid", + "/liquid", + "assets-asset", + ), + ( + "/liquid/assets/registry", + "liquid", + "/liquid", + "assets-registry", + ), + ( + "/liquid/assets/registry/abc", + "liquid", + "/liquid", + "assets-registry-entry", + ), + ( + "/signet/scripthash/abc", + "mutinynet", + "/signet", + "scripthash", + ), + ( + "/mutinynet/widget/wallet", + "mutinynet", + "/mutinynet", + "widget-wallet", + ), + ("/testnet/tx/push", "testnet3", "/testnet", "tx-push"), + ("/testnet/pushtx", "testnet3", "/testnet", "pushtx"), + ("/testnet/tx/test", "testnet3", "/testnet", "tx-test"), + ("/testnet4/tx/abc", "testnet4", "/testnet4", "tx"), + ("/testnet/address/abc", "testnet3", "/testnet", "address"), + ("/testnet4/mempool", "testnet4", "/testnet4", "mempool"), + ("/signet/calculator", "mutinynet", "/signet", "calculator"), + ("/mutinynet/fiat", "mutinynet", "/mutinynet", "fiat"), + ("/liquid/statistics", "liquid", "/liquid", "statistics"), + ("/liquid/mining", "liquid", "/liquid", "mining"), + ("/liquid/lightning", "liquid", "/liquid", "lightning"), + ("/testnet4/graphs", "testnet4", "/testnet4", "graphs"), + ("/testnet4/hashrate", "testnet4", "/testnet4", "hashrate"), + ( + "/testnet4/price-chart", + "testnet4", + "/testnet4", + "price-chart", + ), + ( + "/testnet/terms-of-service", + "testnet3", + "/testnet", + "terms-of-service", + ), + ( + "/testnet/privacy-policy", + "testnet3", + "/testnet", + "privacy-policy", + ), + ( + "/testnet/trademark-policy", + "testnet3", + "/testnet", + "trademark-policy", + ), + ("/testnet/docs", "testnet3", "/testnet", "docs"), + ("/testnet/docs/faq", "testnet3", "/testnet", "docs-faq"), + ( + "/testnet/docs/api/rest", + "testnet3", + "/testnet", + "docs-api-rest", + ), + ( + "/testnet/docs/api/websocket", + "testnet3", + "/testnet", + "docs-api-type", + ), + ]; + for (path, instance, prefix, _) in rows { + let rel = path + .strip_prefix(prefix) + .unwrap_or("") + .trim_start_matches('/'); + expect_shell(route_four(path, None), instance, prefix, rel); + } + expect_shell( + route_four("/testnet4/not-a-screen", None), + "testnet4", + "/testnet4", + "not-a-screen", + ); + expect_shell( + route_four("/testnet4/pkg.js", None), + "testnet4", + "/testnet4", + "pkg.js", + ); + assert!(matches!( + route_four("/api/blocks", None), + SploraPublicRoute::NotFound + )); + assert!(matches!( + route_four("/testnet3/", None), + SploraPublicRoute::NotFound + )); + assert!(matches!( + route_splora_public("/testnet/", None, DEFAULT_QUEUE_PATH, &["liquid"]), + SploraPublicRoute::NotFound + )); + } + /// Named contract: each public prefix selects that network socket. /// REST drops `/api` (Splora http_front). Mainnet is absent here. #[test] @@ -2329,52 +2598,119 @@ pub(crate) mod splora { ); } - /// Named contract: block, tx, and address paths are forwarded to the - /// network socket. This repo does not render that HTML. Unprefixed - /// `/block`, `/tx`, and `/address` stay unmatched so another app on - /// this router keeps those paths on other hosts. + /// Trunk `index.html` that `trunk build` serves. The rust link stays + /// so the wasm app can mount. This is not a screen list. + const TRUNK_INDEX_HTML: &str = r#" + + + + Splora + + + + + +"#; + + /// Named contract: screen routes and each prefix root return the Trunk + /// shell from `SURMOUNT_SPLORA_WEB_ROOT`. A file that exists in that + /// directory is served as a file. Indexer REST stays a socket proxy. + /// Unprefixed `/block`, `/tx`, and `/address` stay unmatched. + /// `/api` is not a frontend. A missing web root does not invent HTML. #[tokio::test] - async fn splora_ui_paths_forward_without_html() { - expect_proxy( + async fn splora_leptos_frontend_mounted_on_each_indexer() { + let missing = frontend_response( + &Method::GET, + std::path::Path::new("/tmp/splora-web-root-missing-not-a-dir"), + "", + ); + assert_eq!(missing.status(), StatusCode::NOT_FOUND); + let missing_body = axum::body::to_bytes(missing.into_body(), 1024) + .await + .unwrap(); + assert_eq!(missing_body.as_ref(), b"not found"); + + expect_shell( route_four("/testnet4/tx/abc", Some("x=1")), "testnet4", - "/tx/abc", - Some("x=1"), - false, + "/testnet4", + "tx/abc", ); assert!(!matches!( route_four("/testnet4/tx/abc", Some("x=1")), - SploraPublicRoute::Index + SploraPublicRoute::Index | SploraPublicRoute::Proxy { .. } )); - expect_proxy( + expect_shell( route_four("/testnet4/block/abc", None), "testnet4", - "/block/abc", - None, - false, + "/testnet4", + "block/abc", ); - expect_proxy( + expect_shell( route_four("/testnet/address/abc", None), "testnet3", - "/address/abc", - None, - false, + "/testnet", + "address/abc", ); - expect_proxy( + expect_shell( route_four("/liquid/block/abc", None), "liquid", - "/block/abc", + "/liquid", + "block/abc", + ); + expect_shell( + route_four("/signet/tx/abc", None), + "mutinynet", + "/signet", + "tx/abc", + ); + expect_shell( + route_four("/mutinynet/tx/abc", None), + "mutinynet", + "/mutinynet", + "tx/abc", + ); + expect_shell(route_four("/testnet/", None), "testnet3", "/testnet", ""); + expect_shell(route_four("/testnet4/", None), "testnet4", "/testnet4", ""); + expect_shell(route_four("/signet/", None), "mutinynet", "/signet", ""); + expect_shell( + route_four("/mutinynet/", None), + "mutinynet", + "/mutinynet", + "", + ); + expect_shell(route_four("/liquid/", None), "liquid", "/liquid", ""); + expect_shell( + route_four("/testnet/docs/api/rest", None), + "testnet3", + "/testnet", + "docs/api/rest", + ); + expect_proxy( + route_four("/testnet/api/tx/x", None), + "testnet3", + "/tx/x", None, false, ); - let signet = route_four("/signet/tx/abc", None); - let mutiny = route_four("/mutinynet/tx/abc", None); - expect_proxy(signet.clone(), "mutinynet", "/tx/abc", None, false); - expect_proxy(mutiny.clone(), "mutinynet", "/tx/abc", None, false); - assert_eq!(signet, mutiny); - assert!(!matches!(signet, SploraPublicRoute::NotFound)); - expect_proxy(route_four("/testnet4", None), "testnet4", "/", None, false); - expect_proxy(route_four("/testnet4/", None), "testnet4", "/", None, false); + assert!(matches!( + route_four("/api/", None), + SploraPublicRoute::NotFound + )); + assert!(matches!( + route_four("/testnet3/", None), + SploraPublicRoute::NotFound + )); + expect_shell( + route_four("/testnet4/not-a-screen", None), + "testnet4", + "/testnet4", + "not-a-screen", + ); for path in [ "/block/abc", @@ -2398,6 +2734,10 @@ pub(crate) mod splora { } let sock = tmp_sock("splora-ui"); + let web = std::env::temp_dir().join(format!("splora-web-shell-{}", std::process::id())); + std::fs::create_dir_all(&web).unwrap(); + std::fs::write(web.join("index.html"), TRUNK_INDEX_HTML).unwrap(); + std::fs::write(web.join("pkg.js"), "console.log(\"splora\");\n").unwrap(); let seen = Arc::new(Mutex::new(Vec::<(String, String)>::new())); let seen_c = seen.clone(); let mock = Router::new().fallback(any(move |req: AxumRequest| { @@ -2444,6 +2784,7 @@ pub(crate) mod splora { queue_path: DEFAULT_QUEUE_PATH.into(), body_limit_bytes: DEFAULT_BODY_LIMIT_BYTES, portal_host: Some("splora.surmount.systems".into()), + web_root: web.clone(), }; let state = test_state_for(cfg, false); let app = Router::new() @@ -2474,15 +2815,97 @@ pub(crate) mod splora { .and_then(|v| v.to_str().ok()) .unwrap_or(""); assert!( - !tx_type.contains("text/html"), - "UI forward must not be local HTML, content-type={tx_type}" + tx_type.contains("text/html"), + "Trunk shell must be HTML, content-type={tx_type}" ); let tx_body = tx.text().await.unwrap(); - assert_eq!(tx_body, "upstream-bytes"); - assert!(!tx_body.contains("=1.88 | | Host nixpkgs channel | **nixos-26.05** lock | Engine is **not** from channel; channel lag is separate | | Lean (builder login) | **4.35.0** official linux x86_64 release (`nix/packages/lean4.nix`, tag `v4.35.0`, published 2026-10-12) | Operator directed Lean 4.35 on 2026-11-04. Not nixpkgs `pkgs.lean4` 4.29.1. Not an elan pin. The live login PATH is unchanged until a switch. `just deploy` was not run for this pin. | @@ -270,7 +271,7 @@ be reachable via **Arti onion / hidden services**. That reachability is | **Still true** | Own stack; direct clearnet to VPS; no required CF hop | | **Secrets** | HS identity keys and startup material = **deploy secrets** on host (**never in git**). Human inventory (onion address notes, recovery checklist) = **Vaultwarden**. Note: Vaultwarden does **not** implement Bitwarden **Secrets Manager** API today. | | **Relay / egress** | Separate open choices (ToS, bandwidth). Required bar is **service reachability via onion HS**, not "must run a public exit relay." | -| **Code today** | **Management-publish module:** `modules/arti-hidden-service.nix` generates real `arti.toml` (`[onion_services]` + `proxy_ports`; backend derives from managementUi when unset; `state_dir` = host HS identity path). `startDaemon` default false; complete lean path does not need `acceptIncompleteOnionConfig` (no effect). **Surmount package:** `pkgs.artiOnionService` / `packages.*.arti-onion-service` is a **Surmount-owned source build of Arti 2.7.0** (GitLab `arti-v2.7.0`) with `onion-service-service` (distinct from stock `pkgs.arti` 1.4.2 client-default; often channel-lagged); rustc from flake input `nixpkgs-rust` for MSRV 1.91+. Module prefers it when `package` is null and treats `passthru.surmountOnionServiceCapable` as the capable claim. Stock path still fail-closed. **Local cleartext full API shipped** for https+Arti: `SURMOUNT_LOCAL_CLEARTEXT_LISTEN` / auto loopback when UI is https and Arti enable (onion rproxy targets cleartext HTTP or UDS; no TLS-on-onion). **Onion-Location + Alt-Svc shipped (2026-08-17; every public Host 2026-08-20)** on mapped HTTPS 2xx/3xx (apex, www, services, extra static Hosts, MTA-STS; one v3 per public HTTP Host; Onion-Location is that Host's onion root plus path; process-start load; no hot-reload; optional `SURMOUNT_ONION_*` enable/disable/map env; dump on `GET /api/v1/system` `onion_discovery`). **Live host (2026-08-17):** unit `surmount-arti-hidden-service` active; durable HS dir; hostname file present; headers proven on HTTPS 307/200. **unit active != onion published** without live Tor Browser / onion-fetch verify. No private keys in tree. Residual: live Tor verify, operator offline HS backup, public-module HOME/`port_info`, Q-ARTI-* (do not invent answers). Do **not** claim B3 fully closed. Validation SoT: `just e2e` / `just check` / `checks.*.ci`. | +| **Code today** | **Management-publish module:** `modules/arti-hidden-service.nix` generates real `arti.toml` (`[onion_services]` + `proxy_ports`; backend derives from managementUi when unset; `state_dir` = host HS identity path). `startDaemon` default false; complete lean path does not need `acceptIncompleteOnionConfig` (no effect). **Surmount package:** `pkgs.artiOnionService` / `packages.*.arti-onion-service` is a **Surmount-owned source build of Arti 2.8.0** (GitLab `arti-v2.8.0`) with `onion-service-service`. `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY= rather than reuse nixpkgs-rust Arti 2.7.0 `cargoDeps` (distinct from stock `pkgs.arti` 1.4.2 client-default; often channel-lagged); rustc from flake input `nixpkgs-rust` for MSRV 1.91+. Module prefers it when `package` is null and treats `passthru.surmountOnionServiceCapable` as the capable claim. Stock path still fail-closed. **Local cleartext full API shipped** for https+Arti: `SURMOUNT_LOCAL_CLEARTEXT_LISTEN` / auto loopback when UI is https and Arti enable (onion rproxy targets cleartext HTTP or UDS; no TLS-on-onion). **Onion-Location + Alt-Svc shipped (2026-08-17; every public Host 2026-08-20)** on mapped HTTPS 2xx/3xx (apex, www, services, extra static Hosts, MTA-STS; one v3 per public HTTP Host; Onion-Location is that Host's onion root plus path; process-start load; no hot-reload; optional `SURMOUNT_ONION_*` enable/disable/map env; dump on `GET /api/v1/system` `onion_discovery`). **Live host (2026-08-17):** unit `surmount-arti-hidden-service` active; durable HS dir; hostname file present; headers proven on HTTPS 307/200. **unit active != onion published** without live Tor Browser / onion-fetch verify. No private keys in tree. Residual: live Tor verify, operator offline HS backup, public-module HOME/`port_info`, Q-ARTI-* (do not invent answers). Do **not** claim B3 fully closed. Validation SoT: `just e2e` / `just check` / `checks.*.ci`. | Research (update status to required): [research/arti-and-secrets-manager.md](research/arti-and-secrets-manager.md). @@ -443,7 +444,7 @@ Honest gaps. Do not claim these are shipping code: | Gap | Reality today | |-----|----------------| -| **Arti live Tor publish** | Module generates **management-publish** `arti.toml` (onion + rproxy, no keys in tree); Surmount `artiOnionService` is owned **2.7.0** source + `onion-service-service` (not nixpkgs 1.4.2 lag); stock path fail-closed. **Local cleartext full API for https+Arti auto-path shipped** (loopback `SURMOUNT_LOCAL_CLEARTEXT_LISTEN` + Arti backend target; lean onion backend is cleartext HTTP or UDS). **Onion status product-real (2026-08-10):** Arti enable derives hostname file + HS state dir env; structured `configured` / `hostname_missing` / `not_provisioned` on SSR + `GET /api/v1/system`; residual names `surmount.artiHiddenService` (not local demo). Lab `SURMOUNT_ONION_URL` only. **Onion-Location + Alt-Svc shipped (2026-08-17; every public Host 2026-08-20)** on mapped HTTPS 2xx/3xx (apex, www, services, extra static Hosts, MTA-STS; one v3 per public HTTP Host; Onion-Location is that Host's onion root plus path; process-start load; no hot-reload; optional `SURMOUNT_ONION_LOCATION_ENABLED` / `SURMOUNT_ONION_ALT_SVC_ENABLED` / `SURMOUNT_ONION_*_DISABLED_HOSTS` / `SURMOUNT_ONION_MAP_FILE`; dump on `GET /api/v1/system` `onion_discovery`). **Live host (2026-08-17, private host-local):** unit `surmount-arti-hidden-service` active; durable HS dir (not `/run`); hostname file present (v3 onion; do not paste the address in this public tree); headers proven on HTTPS 307/200. Host-local `HOME=/var/lib/surmount/arti`; `surmount-ui` in group `surmount-arti`; keystore 0700. Still residual: **live Tor Browser / onion-fetch verify**, operator offline backup of HS identity, public-module HOME/`port_info`. Do **not** claim onion published from unit active or header presence alone; local temp-key green != operator backup. Do **not** claim B3 fully closed. Do not invent Q-ARTI-2/3 answers | +| **Arti live Tor publish** | Module generates **management-publish** `arti.toml` (onion + rproxy, no keys in tree); Surmount `artiOnionService` is owned **2.8.0** source + `onion-service-service` (`cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=, not nixpkgs 1.4.2 lag); stock path fail-closed. **Local cleartext full API for https+Arti auto-path shipped** (loopback `SURMOUNT_LOCAL_CLEARTEXT_LISTEN` + Arti backend target; lean onion backend is cleartext HTTP or UDS). **Onion status product-real (2026-08-10):** Arti enable derives hostname file + HS state dir env; structured `configured` / `hostname_missing` / `not_provisioned` on SSR + `GET /api/v1/system`; residual names `surmount.artiHiddenService` (not local demo). Lab `SURMOUNT_ONION_URL` only. **Onion-Location + Alt-Svc shipped (2026-08-17; every public Host 2026-08-20)** on mapped HTTPS 2xx/3xx (apex, www, services, extra static Hosts, MTA-STS; one v3 per public HTTP Host; Onion-Location is that Host's onion root plus path; process-start load; no hot-reload; optional `SURMOUNT_ONION_LOCATION_ENABLED` / `SURMOUNT_ONION_ALT_SVC_ENABLED` / `SURMOUNT_ONION_*_DISABLED_HOSTS` / `SURMOUNT_ONION_MAP_FILE`; dump on `GET /api/v1/system` `onion_discovery`). **Live host (2026-08-17, private host-local):** unit `surmount-arti-hidden-service` active; durable HS dir (not `/run`); hostname file present (v3 onion; do not paste the address in this public tree); headers proven on HTTPS 307/200. Host-local `HOME=/var/lib/surmount/arti`; `surmount-ui` in group `surmount-arti`; keystore 0700. Still residual: **live Tor Browser / onion-fetch verify**, operator offline backup of HS identity, public-module HOME/`port_info`. Do **not** claim onion published from unit active or header presence alone; local temp-key green != operator backup. Do **not** claim B3 fully closed. Do not invent Q-ARTI-2/3 answers | | **Axum HTTPS public cutover / nginx delete** | In-process rustls TLS 1.3 acceptor wired (host PEMs, fail-closed key mode). **`surmount.web.enable` default false** (nginx not product edge). Dual-run escape: `web.enable = true`. Module/nginx code still in tree. **:80 redirect-only bind wired** when `redirectHttpToHttps` + listen (dual-run mutex at eval; no cleartext API on :80). **Operator 2026-08-02:** production port 80 is **free** for that redirect-only path. **HTTP-01 on surmount-1** renews the Let's Encrypt production certificate that host presents (timer `surmount-cert-renew`). horizon does not have to stay online for that renewal. Public module `acme.enable` stays false. **In-process ACME scaffold (2026-08-10; A1/A2):** `instant-acme` + DNS-01 inside management-ui, **default off**; static PEMs still first-class; hermetic `mock` + **external-hook** DNS-01 challenge adapter (operator-owned executable; not a commercial DNS brand); early-renew default 30 days. **Live host (2026-08-11 free-443; 2026-08-12 mta-sts + policy):** public :443 is **surmount-manage** not Stalwart; `https://services.surmount.systems/health` **200** without `-k`. Cert is **Let's Encrypt production** (issuer YE1; browser-trusted). Intended production leaf is **20** certificate hostnames on one PEM (`with_single_cert`). Live leaf as of **2026-09-07** (CT, YE2) is still **18** names: six extra static zones apex+www (`baxterartworks.com`, `btcfur.com`, `exophiles.org`, `iantuckerstudios.com`, `nostrfurs.com`, `yiffa.app`) plus `surmount.systems`, `www.surmount.systems`, `mail.surmount.systems`, `services.surmount.systems`, `mta-sts.surmount.systems`, plus `mail.cryptoquick.com`. Missing: `cryptoquick.com` and `www.cryptoquick.com`. That hostname mismatch is why cryptoquick HTTPS fails verify. Extra-vhost HTTPS for those six is **live**. Validating A for cryptoquick apex/www succeeds (AD true). Leftover parent DS key tag 2368 is gone. The old wait-for-SERVFAIL gate is closed as a live A-lookup gate. SHA-1 parent DS digest type 1 remains standing DNSSEC quality debt in operator-facts Monday leftover; it is not the HTTPS cause. After the tree lists the two missing names, laptop `--issue` is operator-gated (Namecheap custody). `--live` does **not** detect missing certificate hostnames. Esplora Hosts stay off this leaf. Mail **is** on the production leaf (IMAP/SMTPS 993/465 YE1). Issued by **laptop DNS-01** (Namecheap; ClientIp = laptop egress; SettleSeconds=120). Host ACME stays **off**. Durable PEMs under `/var/lib/surmount/secrets/tls/`. Named renew: `just laptop-renew-cert` (`--directory` required; do **not** re-issue just to add a name already on the leaf). Apex/www public page is packaged **SurmountSystems/site** (live 2026-08-19 tip `1c84696`; not UNDER CONSTRUCTION). Private host-local `mtaStsMode=testing`; public `https://mta-sts./.well-known/mta-sts.txt` **200** `mode: testing` (stay testing while DNS MX is eforward; do not enforce). Reports: `../.agents/reports/impl-live-le-production-laptop.md`, `../.agents/reports/impl-earn-trust-wave.md`. **Post-reboot proof PASS (2026-08-13).** Later 2026-08-18 hard cycle: health/apex **200**, IMAP Let's Encrypt including **mail**. Agents never reboot. Residual: firewall across switch, MemoryDenyWriteExecute host proof (`just e2e-host`). Day-one order: [OPS.md](OPS.md). Do **not** claim live onion or "nginx deleted from repo" without further host proof. Do **not** invent MX / DMARC / VW. Agent language: say **certificate hostnames**, not bare **SAN** (collides with storage SAN). Dual-run unused checklist: [OPS.md](OPS.md); **do not delete `modules/web.nix` without operator OK** | | **Merciless ban + whitelist** | First path + helper scaffold: Rust ban decide + memory/file; optional kernel firewall sync via **socket-activated** `surmount-nft-ban-helper@` (Unix socket `SURMOUNT_BAN_NFT_HELPER_SOCK`; CAP_NET_ADMIN on helper unit only) or unsupported direct `nftExec`; UI keeps NNP and **no** CAP_NET_ADMIN (child setcap spawn is not the product path). DryRun does not mutate sets; Enforce apply-then-durable. Hermetic mock/tests under `just e2e`. **`remove_ban` / lab unban shipped** (helper `remove_ban` / CLI `remove-ban`; delete-element; absent element = ok). **Auth-failure BanCandidate + matrix shipped** (`decide_ban_signal` / `BanGuard::signal_unauthorized` + request-context hook; whitelist immune; Off/DryRun/Enforce). Session exchange fail and bad presented NIP-98 may signal once; missing cookie does not; 404/501 still do not auto-ban. Table: [SECURITY.md](SECURITY.md). **Not** live host bans / full Q-ACL-1..6 surface answers. Live drop: `just e2e-host` + sets `surmount-ban4`/`surmount-ban6`. fail2ban sshd still transitional sketch | | **Leptos SSR admin/webmail** | **Multi-page console + UI depth (2026-08-01; directory live 2026-08-07):** SSR `/`, `/domains`, `/accounts`, `/system`, `/mail`, `/login` (DOGE only; no skeleton). Inventory cards, onion chip, system definition table, mail probe card. Accounts honest empty by default; `/domains` config union (primary names including derived www and mta-sts when first-class, `static_vhosts` keys, extra mail hostnames; `source` stays `config`; not Namecheap API; not Stalwart directory). **Directory trait + hermetic mock + live Stalwart client shipped** (`AppState.directory`; `unavailable` default; labeled `mock` never default-on; `stalwart` = management JMAP `x:Account/query`+`get` via Bearer token, explicit `SURMOUNT_DIRECTORY=stalwart` + host token only; list fail-closed empty). **JMAP thin 501 boundary locked (2026-08-07):** `POST /api/v1/jmap` honest 501 (`jmap_proxy_not_implemented`); mail SSR residual copy; no `/webmail` product route; hermetic anchors. Directory research: [research/stalwart-directory-api.md](research/stalwart-directory-api.md). **Account create/update API mutations shipped (2026-08-07)** (`POST/PATCH /api/v1/accounts`; auth gate + CSRF; mock + `x:Account/set` wire-mock). **Create mailbox form shipped (2026-08-14)** on `/mail` (Administrator; optional npub; User vs Administrator; map at `/var/lib/surmount/console/accounts.json` or `SURMOUNT_CONSOLE_ACCOUNTS`). **Attach npub shipped (2026-08-20):** Administrator `/mail` Grant console login pastes bech32 `npub1...`, session-bound CSRF, binds an existing mailbox so that person can log into the services portal (AuthMode nostr). Garbage/nsec refused. Directory listing not required. `/accounts` still has no create form. **Structured request logging shipped.** Still residual: hydrate islands only if clear SSR gap (parked), full Q-AUTH-1, JMAP proxy + webmail **beyond** 501, **MX still parked**, **host cutover**. Living crane rustc **1.99.0** from `nixpkgs-rust` (not host `rustPackages_1_95`); Leptos MSRV floor is >=1.88 | diff --git a/docs/EDGE_AND_TLS.md b/docs/EDGE_AND_TLS.md index fcc875b..e4c87a0 100644 --- a/docs/EDGE_AND_TLS.md +++ b/docs/EDGE_AND_TLS.md @@ -346,23 +346,28 @@ when those indexer units run. One public Host: `splora.surmount.systems`. The sample proxy map does not list `esplora.surmount.systems`, `testnet3.esplora.surmount.systems`, `testnet4.esplora.surmount.systems`, `mutinynet.esplora.surmount.systems`, -or `liquid.esplora.surmount.systems`. The Rust explorer UI belongs to -Splora, not this repo. Do not add block, transaction, or address pages. - -The network prefix is stripped before the Unix socket. The query string -is kept. `/{network}/api/v1/ws` (mainnet: `/api/v1/ws`) is the WebSocket +or `liquid.esplora.surmount.systems`. The Leptos explorer route table belongs to Splora +`frontend/web/src/views.rs` (`App`). This Axum process serves that table +as SSR HTML on `/testnet/` (testnet3), `/testnet4/`, `/signet/`, +`/mutinynet/`, and `/liquid/`. It does not serve a mainnet frontend on +`/api/`. The Splora crate mounts `App` with `leptos::mount::mount_to_body` +on wasm32 only. This server does not start a second HTTP server, and it +does not build that wasm crate. + +The network prefix is stripped before the Unix socket for indexer REST. +The query string is kept. `/{network}/api/v1/ws` (mainnet: `/api/v1/ws`) is the WebSocket upgrade. `POST /splora/queue` stays the queue and is not a network prefix. `GET /` on that host is a short same-host path list (`/api/`, `/testnet/`, `/testnet4/`, `/signet/`, `/mutinynet/`, `/liquid/`), not an explorer. | Public path on `splora.surmount.systems` | Socket | Notes | |------------------------------------------|--------|-------| -| `/api` and `/api/...` | mainnet | 404 while the mainnet indexer is off. | -| `/testnet` and `/testnet/...` | testnet3 | Public path is `/testnet`, not `/testnet3`. | -| `/testnet4` and `/testnet4/...` | testnet4 | | -| `/signet` and `/signet/...` | mutinynet | No redirect between `/signet` and `/mutinynet`. | -| `/mutinynet` and `/mutinynet/...` | the same mutinynet socket | No redirect between them. | -| `/liquid` and `/liquid/...` | liquid | | +| `/api` and `/api/...` | mainnet | 404 while the mainnet indexer is off. Not a Leptos frontend. | +| `/testnet` and `/testnet/...` | testnet3 | REST `/api` uses the socket. Screen routes are Leptos HTML. Not `/testnet3`. | +| `/testnet4` and `/testnet4/...` | testnet4 | REST `/api` uses the socket. Screen routes are Leptos HTML. | +| `/signet` and `/signet/...` | mutinynet | Same mutinynet frontend as `/mutinynet`. No redirect. | +| `/mutinynet` and `/mutinynet/...` | the same mutinynet socket | REST `/api` uses the socket. Screen routes are Leptos HTML. No redirect. | +| `/liquid` and `/liquid/...` | liquid | REST `/api` uses the socket. Screen routes are Leptos HTML. | Mempool / Esplora REST is the indexer process on a Unix socket. It does **not** require extra public Hosts, certificate names, or HTTP/3. Unix @@ -379,13 +384,17 @@ to each Splora Unix socket is still HTTP/1.1. A client that arrived on HTTP/3 does not make the indexer speak HTTP/3. Splora does not listen on UDP. -Under a network prefix, a remainder that is not the REST API is forwarded -to that network Unix socket. This edge does not answer it with 404 and -does not render it. A REST remainder still drops a leading `/api`, except +Under a network prefix, a remainder of `/api` or `/api/...` is still +forwarded to that network Unix socket. A remainder that matches the +Splora Leptos `App` router is HTML from this Axum process and is not +forwarded. Any other remainder is 404. `/docs/api/...` is a screen, not +indexer REST. A REST remainder still drops a leading `/api`, except exact `/api/v1/ws`, which stays and is the WebSocket upgrade. `/api` on -mainnet is 404 only while the mainnet indexer is off. Public -`/testnet3/...` is not a prefix. The Electrum newline socket is not -proxied (fail-closed if configured). +mainnet is 404 only while the mainnet indexer is off, and it is not a +Leptos frontend. Public `/testnet3/...` is not a prefix. `/signet` and +`/mutinynet` both serve the mutinynet frontend. There is no redirect +between them. The Electrum newline socket is not proxied (fail-closed +if configured). **Live in browsers (2026-08-20):** Namecheap NS, exclusive A matching this host, HTTPS **200**, the Let's Encrypt certificate this host presents covers apex+www, packaged diff --git a/docs/operator-direction.md b/docs/operator-direction.md index c9219b9..c980187 100644 --- a/docs/operator-direction.md +++ b/docs/operator-direction.md @@ -43,7 +43,7 @@ from horizon onto surmount-1. 3. Splora mainnet stays off. The other Splora instances already enabled in the private overlay stay as they are. 4. Host renewal on surmount-1 is on. HTTP-01 on surmount-1 refreshes the Let's Encrypt production certificate that host already presents. horizon does not have to stay online for that renewal. Namecheap API credentials stay off surmount-1. No Namecheap ClientIp change. Closing horizon does not stop surmount-1 from serving the PEMs already on disk. 5. Restic backups stay off for now. -6. fail2ban for sshd stays on. The management UI nftables ban helper stays off. +6. sshguard watches sshd. A ban is permanent after 3 failures. fail2ban is not installed. The management UI nftables ban helper stays off. 7. Say horizon or surmount-1. The private overlay directory is not a machine name. 8. `just deploy` keeps surmount-1 as the remote Nix builder. The private overlay on horizon already sets `surmount.remoteBuilder.enable = true`. The public module default of false applies only when that file is absent. @@ -183,7 +183,7 @@ Research: [research/pqconnect-and-pqc.md](research/pqconnect-and-pqc.md) | Ban | Any unauthorized access of any kind -> **immediate blacklist** on the box (precise signal classes still open; see research) | | Whitelist | Explicit whitelist IPs: never blacklisted; may bypass blacklist | | Hygiene | Track **last-used** on whitelist entries | -| Implementation lean | Prefer nftables + custom Rust long-term; fail2ban is Python transitional sketch only | +| Implementation lean | Prefer nftables + custom Rust long-term. sshguard is the sshd ban, permanent after 3 failures. fail2ban is not installed. | Research: [research/access-control-fail2ban.md](research/access-control-fail2ban.md). Module pointer: `modules/hardening.nix` header. diff --git a/docs/packages-and-forks.md b/docs/packages-and-forks.md index 1e5026a..58233da 100644 --- a/docs/packages-and-forks.md +++ b/docs/packages-and-forks.md @@ -46,7 +46,7 @@ package bumps in this repo. | `nix/packages/stalwart-spam-filter.nix` | Spam rules FODs | | `nix/packages/management-ui.nix` | Surmount Axum UI (crane) | | `nix/packages/surmount-public-site.nix` | Apex/www static site from flake input `github:SurmountSystems/site` (no NPM; operator bumps rev) | -| `nix/packages/arti-onion-service.nix` | Surmount-owned Arti **2.7.0** source build + `onion-service-service` (HS publish); distinct from stock `pkgs.arti`. Kept equal to nixpkgs-rust `pkgs.arti.version` | +| `nix/packages/arti-onion-service.nix` | Surmount-owned Arti **2.8.0** source build + `onion-service-service` (HS publish); distinct from stock `pkgs.arti`. `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. Does not reuse nixpkgs-rust `pkgs.arti` **2.7.0** `cargoDeps` | | `nix/packages/surmount-private-data.nix` | Private-data pattern scanner (crane). `nix run .#surmount-private-data`. Pre-commit + `just check-private-data` / `just private-data`. | | `nix/packages/surmount-host-logs.nix` | Host journal status/follow (crane). `nix run .#surmount-host-logs`. `just host-logs`. Journald stays source of truth. | | `nix/packages/surmount-shc.nix` | SHC customer user-api client (crane; rDNS PTR + tickets). `nix run .#surmount-shc`. `just rdns-shc`. No python3. | @@ -66,7 +66,7 @@ package bumps in this repo. | `nix/packages/surmount-secrets-install.nix` | Domain A to Domain B install + Vaultwarden export-to-staging (crane). `nix run .#secrets-install-host` / `.#secrets-export-bw-to-staging`. | | `nix/packages/surmount-secrets-prompt.nix` | Domain A no-echo intake (crane). `nix run .#surmount-secrets-prompt`. `just secrets-prompt`. | | `nix/packages/surmount-rekey.nix` | SHC backup re-key paste (X25519 age identity + gpg wrap, not the age crate). `nix run .#surmount-rekey`. `just rekey`. Prints `age1...` for type PGP. Never prints the age secret. | -| `nix/overlays.nix` | Extra pins (flake `surmountOverlay` is primary). tzdata 2026e sets `doCheck = false` (same change as NixOS/nixpkgs pull 582069). | +| `nix/overlays.nix` | Extra pins (flake `surmountOverlay` is primary). tzdata 2026e sets `doCheck = false` (same change as NixOS/nixpkgs pull 582069). Nix package pin is **2.35.4** (commit `21286d605815196c3fff1960550ab08700fc4e6f`) on the nixos-26.05 2.35.3 recipe. Channel `pkgs.nix` was stable **2.34.9**. NixOS stays **nixos-26.05** because 26.11 is not released. Release notes still read 2026.11/??. Branch-off is scheduled 2026-11-16 to 2026-11-23. Release is scheduled 2026-11-23 to 2026-11-30. See [NixOS 26.11 release schedule](https://github.com/NixOS/nixpkgs/issues/567567) (accessed: 2026-11-05). | | `modules/stalwart-service.nix` | 0.16+ `config.json` service (disables both stock paths: `services/mail/stalwart-mail.nix` and `services/mail/stalwart.nix`; option `services.stalwart`; unit stays `stalwart-mail.service`) | | `modules/*.nix` | Surmount NixOS modules | | `flake.nix` / `flake.lock` | Inputs, checks, host entrypoints | @@ -134,19 +134,18 @@ thin just alias. **Arti note:** stock nixpkgs `pkgs.arti` often lags (client-default, not HS-capable). Surmount owns the Arti pin at -`nix/packages/arti-onion-service.nix` (`2.7.0` from GitLab `arti-v2.7.0`). -That version is this lock's nixpkgs-rust `pkgs.arti.version` (rev -`9ecc0a672e9ef2ca4eec8e87dbee653607a7d332`, rustc 1.99.1). The package enables -cargo feature `onion-service-service` and is exposed as +`nix/packages/arti-onion-service.nix` (`2.8.0` from GitLab `arti-v2.8.0`). +nixpkgs-rust `pkgs.arti.version` on rev +`9ecc0a672e9ef2ca4eec8e87dbee653607a7d332` is still **2.7.0** (rustc 1.99.1). +The package does not use that `cargoDeps`. `cargoHash` for 2.8.0 is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. +The package enables cargo feature `onion-service-service` and is exposed as `pkgs.artiOnionService` / `packages.*.arti-onion-service` (it does **not** replace `pkgs.arti`). rustc comes from flake input `nixpkgs-rust` when Arti MSRV exceeds the host -channel pin. Vendor crates use matching `nixpkgs-rust` `arti.cargoDeps` -with a version assert (crates.io `fetch-cargo-vendor` 403 workaround); bump -`nixpkgs-rust` until `pkgs.arti.version` matches, or restore plain `cargoHash` -when crates.io vendor works again. Module `arti-hidden-service.nix` prefers +channel pin. Module `arti-hidden-service.nix` prefers the Surmount package when `package` is null. Cargo build is heavy and is -**not** in `checks.*.ci`; eval-only version/feature/passthru contract is. +**not** in `checks.*.ci`; the eval contract asserts version 2.8.0 and does +not embed a fake hash. Live Tor publish remains residual (operator HS keys + network verify). No official multi-arch Arti release binaries, so this is source build not binary FOD. diff --git a/docs/research/version-audit.md b/docs/research/version-audit.md index 95d3ad4..56467ad 100644 --- a/docs/research/version-audit.md +++ b/docs/research/version-audit.md @@ -24,11 +24,13 @@ blob `bf8ac6194a0ef4befa6bb3204caea46800b20a31` is the same file as prior pin `93b590222` and as prior nixpkgs-rust lock `c46e6c9a`. An update does not replace that package. `doCheck` stays `stdenv.hostPlatform.isLinux`. This pass did not build sqlite. -**Arti HS package refresh:** 2026-10-17 (UTC). Surmount `arti-onion-service` -pin is **2.7.0** (GitLab tag `arti-v2.7.0`, 2026-10-01). It matches this -nixpkgs-rust snapshot's `pkgs.arti.version` **2.7.0**. MSRV is 1.92. -High-severity TROVE-2026-047 (half-open streams) is fixed in this tag. -`onion_services`, `proxy_ports`, and `socks_listen` stay the module keys. +**Arti HS package refresh:** 2026-11-05 (UTC). Surmount `arti-onion-service` +pin is **2.8.0** (GitLab tag `arti-v2.8.0`, changelog date 3 November 2026). +`cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. It does not reuse this +nixpkgs-rust snapshot's `pkgs.arti` **2.7.0** `cargoDeps`. The arti crate +`rust-version` in that tag is 1.92. The 2026-10-17 pin was **2.7.0** +(GitLab `arti-v2.7.0`). `onion_services`, `proxy_ports`, and `socks_listen` +stay the module keys. **Prior full package audit:** 2026-07-31 (UTC). Stalwart FOD / cargo / upstream "latest" cells below stay labeled as that audit day unless revalidated. @@ -72,7 +74,8 @@ date in that row** until revalidated. | WebUI FOD | **2.0.4** (re-check 2026-11-03: at latest, published 2026-11-02T11:19:17Z). Prior 2026-10-17 cell was 1.0.11. | | spam-filter FOD | **3.0.4** (re-check 2026-11-03: at latest, published 2026-11-01T15:50:40Z). Rules archive only. Prior 2026-10-17 cell was 3.0.2, which still shipped `spam-filter.toml`. | | vandelay FOD | **1.0.14** (re-check 2026-11-03: at latest, published 2026-11-01T16:43:01Z). Prior 2026-10-17 cell was 1.0.12. | -| Arti (Surmount HS package) | Surmount-owned source build **2.7.0** (GitLab `arti-v2.7.0`; `arti-onion-service.nix`; checked 2026-10-17). nixpkgs-rust `pkgs.arti.version` is **2.7.0**. Stock host `pkgs.arti` is channel-lagged client-default (not the HS path). | +| Arti (Surmount HS package) | Surmount-owned source build **2.8.0** (GitLab `arti-v2.8.0`; `arti-onion-service.nix`; checked 2026-11-05). `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. nixpkgs-rust `pkgs.arti.version` is still **2.7.0** and is not `cargoDeps`. Stock host `pkgs.arti` is channel-lagged client-default (not the HS path). | +| Nix package | **2.35.4** from `nix/overlays.nix` on the nixos-26.05 `nixVersions.nix_2_35` recipe (that recipe was **2.35.3**). Channel `pkgs.nix` was `nixVersions.stable` **2.34.9**. Source commit `21286d605815196c3fff1960550ab08700fc4e6f`. No cargoHash in that recipe. NixOS stays **nixos-26.05** because 26.11 is not released. Release notes still read 2026.11/??. Branch-off is scheduled 2026-11-16 to 2026-11-23. Release is scheduled 2026-11-23 to 2026-11-30. See [NixOS 26.11 release schedule](https://github.com/NixOS/nixpkgs/issues/567567) (accessed: 2026-11-05). | | crane / sops-nix flake locks | Crane is `bd63bf1` (2026-10-11), one internal flake.lock commit after `47b6b27`, newer than release v0.24.0. sops-nix is `44931708` (2026-10-15), still master. Re-checked 2026-10-17. | | management-ui Cargo.lock | 2026-10-17: reqwest ceiling is **0.13**, tower-http ceiling is **0.7**, rustls is **0.23.46** (git patch; menhera 10d still lists 0.23.45), nostr stays **0.45.5**, rcgen ceiling is **0.14**. | | RocksDB | Embedded in upstream Stalwart lock (`10.4.2` via `librocksdb-sys`); Surmount does **not** use system `pkgs.rocksdb` for the mail binary | @@ -293,9 +296,9 @@ optional `blobSize` / `bufferSize`; it does not pin a system rocksdb package. ## 5b. Arti (HS publish package) Packaging mode: **Surmount-owned source build** of Arti from Tor -Project GitLab (`fetchFromGitLab` tag `arti-v2.7.0`), with cargo feature -`onion-service-service`. The package version is **2.7.0**. It is not 2.5.1 -and not 2.6.0. +Project GitLab (`fetchFromGitLab` tag `arti-v2.8.0`), with cargo feature +`onion-service-service`. The package version is **2.8.0**. It is not 2.5.1, +not 2.6.0, and not 2.7.0. `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. Distinct attribute `pkgs.artiOnionService` / `packages.*.arti-onion-service`. Does **not** replace stock `pkgs.arti`. @@ -304,16 +307,16 @@ is a hermetic cargo source build, not a binary FOD. | Item | Value | Source | |------|-------|--------| -| Surmount package version | **2.7.0** | `nix/packages/arti-onion-service.nix` (checked 2026-10-17). | -| Source | GitLab `tpo/core/arti` tag **`arti-v2.7.0`** | package `src` | +| Surmount package version | **2.8.0** | `nix/packages/arti-onion-service.nix` (checked 2026-11-05). | +| Source | GitLab `tpo/core/arti` tag **`arti-v2.8.0`** | package `src` | | Cargo feature | **`onion-service-service`** (lean; not nixpkgs `full`) | package `buildFeatures` | | Capability passthru | `surmountOnionServiceCapable = true` | package `passthru` | | Toolchain | rustc from flake input **`nixpkgs-rust`** (pinned rev 9ecc0a67, rustc **1.99.1**; Arti MSRV **1.92**) | `flake.nix` `mkArtiRustPlatform` | -| Vendor | `cargoDeps` from matching nixpkgs-rust `arti` (crates.io 403 workaround) | package `artiUnstable.cargoDeps` | +| Vendor | `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. Does not use nixpkgs-rust 2.7.0 `cargoDeps` | package `cargoHash` | | Stock nixpkgs `pkgs.arti` | channel-lagged client-default (COMPACTION-PIN: often **1.4.2**); not used for HS path | host channel, not this pin | -| Upstream GitLab tag | **`arti-v2.7.0`** (2026-10-01) | this package | -| nixpkgs-rust `pkgs.arti.version` | **2.7.0** | lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332` (`nix eval --raw` printed `2.7.0`, 2026-10-18). Prior lock `c46e6c9a` was also 2.7.0. | -| Gap vs upstream tag | **closed** | pin equals `arti-v2.7.0` so `artiUnstable.version == version` stays true | +| Upstream GitLab tag | **`arti-v2.8.0`** (changelog date 3 November 2026) | this package | +| nixpkgs-rust `pkgs.arti.version` | **2.7.0** | lock `9ecc0a672e9ef2ca4eec8e87dbee653607a7d332` (`nix eval --raw` printed `2.7.0`, 2026-10-18). Prior lock `c46e6c9a` was also 2.7.0. Not the Surmount src. | +| Gap vs upstream tag | **src pinned.** `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY= | src tag is `arti-v2.8.0`. `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. Do not treat 2.7.0 `cargoDeps` as 2.8.0. | Honest limits: @@ -326,9 +329,10 @@ Honest limits: - **Build proof (2026-07-31):** `nix build .#arti-onion-service` green for then-current **2.5.0**; cargo features include `onion-service-service`. **2026-08-27** the pin was **2.5.1**. The 2026-10-10 pin was **2.6.0** - (GitLab `arti-v2.6.0`). Living pin checked 2026-10-17 is **2.7.0** + (GitLab `arti-v2.6.0`). The 2026-10-17 check was **2.7.0** (GitLab `arti-v2.7.0`), equal to this nixpkgs-rust `pkgs.arti.version`. - Cargo rebuild of the HS binary is not this note. + The living pin on 2026-11-05 is **2.8.0** (GitLab `arti-v2.8.0`). + `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. Cargo rebuild of the HS binary is not this note. ## 5c. rustc (management-ui vs host vs stable) @@ -356,12 +360,12 @@ Do not treat host rustc 1.95.0 as the product pin. The 2026-10-03 lock | Item | Pin / state | Gap notes | |------|-------------|-----------| -| `nix/overlays.nix` | tzdata 2026e `doCheck = false` | Same attribute change as NixOS/nixpkgs pull 582069. Date-dependent `now.ck` (America/Phoenix and America/Vancouver identical from now on, on or after 2026-11-01). Not a gcc or x86-64-v3 change. | +| `nix/overlays.nix` | tzdata 2026e `doCheck = false`. Nix package **2.35.4** | tzdata: same attribute change as NixOS/nixpkgs pull 582069. Date-dependent `now.ck` (America/Phoenix and America/Vancouver identical from now on, on or after 2026-11-01). Not a gcc or x86-64-v3 change. Nix: commit `21286d605815196c3fff1960550ab08700fc4e6f` on the 2.35.3 recipe. Channel stable was **2.34.9**. NixOS stays nixos-26.05 because 26.11 is not released. Release notes still read 2026.11/??. Branch-off is scheduled 2026-11-16 to 2026-11-23. Release is scheduled 2026-11-23 to 2026-11-30. See [NixOS 26.11 release schedule](https://github.com/NixOS/nixpkgs/issues/567567) (accessed: 2026-11-05). | | management-ui product version | 0.1.0 | Surmount-owned | | rust-overlay flake input | commented out | not active | | modules / hosts | no independent fetchurl pins | versions come from flake packages + nixpkgs | | Stalwart packaging mode | `release-binary-fod` | source build deferred (vendor 403 / rustc) | -| arti-onion-service | Surmount-owned source **2.7.0** (GitLab `arti-v2.7.0`) + `onion-service-service` | equals nixpkgs-rust `pkgs.arti.version` 2.7.0 (section 5b). | +| arti-onion-service | Surmount-owned source **2.8.0** (GitLab `arti-v2.8.0`) + `onion-service-service` | `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. Does not equal nixpkgs-rust `pkgs.arti.version` 2.7.0 (section 5b). | | Stock Stalwart modules | dual `disabledModules` | `modules/stalwart-service.nix` | --- @@ -374,9 +378,10 @@ Priority order (proposed, not accepted): **1.0.14**, WebUI **2.0.4**, spam-filter **3.0.4** (rules archive only), vandelay **1.0.14**. The 2026-10-17 cells are not the living pins. Re-check releases before every packaging PR. -2. **Arti currency:** the Surmount package is **2.7.0** (GitLab - `arti-v2.7.0`), equal to nixpkgs-rust `pkgs.arti.version`. Live Tor - verify remains residual. Do not claim onion published from pin alone. +2. **Arti currency:** the Surmount package src is **2.8.0** (GitLab + `arti-v2.8.0`). `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. nixpkgs-rust + `pkgs.arti.version` is still **2.7.0** and is not the vendor tree. + Live Tor verify remains residual. Do not claim onion published from pin alone. 3. **OS channel:** **done** for 25.05 -> 26.05 (living host is 26.05). Further bumps only with release notes + measured need. May retire `nixpkgs-rust` if host channel rustc meets Arti MSRV without it. @@ -441,7 +446,7 @@ re-running. Keep reports short under `~/.agents/reports/`. | WebUI latest = 2.0.4 | GitHub tag `v2.0.4` published 2026-11-02T11:19:17Z (re-check 2026-11-03). The 2026-10-17 cell that said 1.0.11 is historical. | | spam-filter latest = 3.0.4 | GitHub tag `v3.0.4` published 2026-11-01T15:50:40Z (re-check 2026-11-03). Asset is `spam-filter-rules.json.gz` only. The 2026-10-17 cell that said 3.0.2 is historical. | | vandelay latest = 1.0.14 | GitHub tag `v1.0.14` published 2026-11-01T16:43:01Z (re-check 2026-11-03). The 2026-10-17 cell that said 1.0.12 is historical. | -| Surmount arti-onion-service = 2.7.0 | package expression, GitLab `arti-v2.7.0`, cargoDeps assert vs nixpkgs-rust `arti` 2.7.0 (checked 2026-10-17). | +| Surmount arti-onion-service = 2.8.0 | package expression, GitLab `arti-v2.8.0`, src hash from the 2026-11-05 prefetch. `cargoHash` is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. The 2026-10-17 cell that said 2.7.0 with nixpkgs-rust `cargoDeps` is historical. | | rustc pin = 1.99.1 | Upstream stable is 1.99.1 (accessed: 2026-10-17). nixpkgs-rust `9ecc0a67` is that rustc. Master still has 1.99.0. Prior lock `c46e6c9a` was rustc 1.99.0. The 2026-07-31 cell that said 1.97.1 is historical. | | Historical host 25.05 / crane 1.88 | 2026-07-31 audit day only (see footnotes) | | Current stable branch name 26.05 | channels.nixos.org + nixpkgs README Hydra links for release-26.05 (2026-07-30) | diff --git a/flake.lock b/flake.lock index aa2f1c0..7ac232b 100644 --- a/flake.lock +++ b/flake.lock @@ -225,17 +225,17 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1793252175, - "narHash": "sha256-pHOfDKsOrqeplMAKdsY//+xEupsGfJwxyYJhUyZWvDo=", + "lastModified": 1793877947, + "narHash": "sha256-PWVbTYsDmSI+03iWFPsc71OfW2bNs/JLKGbwmFByQ6M=", "owner": "SurmountSystems", "repo": "splora", - "rev": "dd71ab234fb416efbdaf85315efa806cabba514f", + "rev": "28eb3ea077eee02bd8a9c0cba63ee9a611b10e54", "type": "github" }, "original": { "owner": "SurmountSystems", - "ref": "surmount", "repo": "splora", + "rev": "28eb3ea077eee02bd8a9c0cba63ee9a611b10e54", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 63fc47e..e1f4645 100644 --- a/flake.nix +++ b/flake.nix @@ -35,7 +35,8 @@ # Stalwart engine is NOT taken from this channel. See # nix/packages/stalwart-mail.nix and modules/stalwart-service.nix. # Arti engine is NOT taken from this channel. See - # nix/packages/arti-onion-service.nix (Surmount-owned 2.7.0 source build). + # nix/packages/arti-onion-service.nix (Surmount-owned 2.8.0 source build; + # cargoHash is set to sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=). # Builder-login Lean is NOT pkgs.lean4 from this channel (4.29.1). # See nix/packages/lean4.nix (official release v4.35.0). nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; @@ -96,7 +97,7 @@ # Splora (Esplora-compatible indexer). Operator bumps with nix flake update splora. # NixOS module is nixosModules.splora. REST is one indexer instance plus # JSON-RPC and a cookie; a local bitcoind datadir is not required. - splora.url = "github:SurmountSystems/splora/surmount"; + splora.url = "github:SurmountSystems/splora/28eb3ea077eee02bd8a9c0cba63ee9a611b10e54"; }; outputs = @@ -137,9 +138,15 @@ inherit system; }; - # Extra pins live in nix/overlays.nix (tzdata doCheck). Not a second - # overlay home. surmountOverlay remains the Surmount package set. - surmountPins = import ./nix/overlays.nix; + # nix/overlays.nix exports .pins and .tzdata. Not a second overlay + # home. surmountOverlay remains the Surmount package set. + # .pins is the Nix 2.35.4 cli (not nix-everything). + # .tzdata sets tzdata 2026e doCheck = false (now.ck). The mail-host + # nixosSystem below imports .tzdata. mkPkgs does not, so + # checks.x86_64-linux.ci does not rebuild CPython for that skip. + surmountOverlayParts = import ./nix/overlays.nix; + surmountPins = surmountOverlayParts.pins; + surmountTzdata = surmountOverlayParts.tzdata; # rustPlatform whose rustc meets Arti MSRV; C deps still come from host pkgs. mkArtiRustPlatform = @@ -338,6 +345,7 @@ { nixpkgs.overlays = [ surmountPins + surmountTzdata (surmountOverlay "x86_64-linux") ]; } @@ -376,10 +384,13 @@ surmount-public-site = pkgs.callPackage ./nix/packages/surmount-public-site.nix { src = surmount-site; }; - # Surmount-owned Arti 2.7.0 + onion-service-service (HS publish). + # Surmount-owned Arti 2.8.0 + onion-service-service (HS publish). # Not a drop-in for pkgs.arti; heavy cargo build, not in checks.ci. - # rustPlatform + artiUnstable.cargoDeps from nixpkgs-rust (MSRV 1.91+; - # vendor handoff avoids crates.io 403). C deps from host pkgs. + # rustPlatform from nixpkgs-rust (MSRV 1.92). The Arti 2.8.0 + # cargoHash is set to + # sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. + # Do not use artiUnstable.cargoDeps (that tree is 2.7.0). + # C deps from host pkgs. arti-onion-service = pkgs.callPackage ./nix/packages/arti-onion-service.nix { rustPlatform = mkArtiRustPlatform system; artiUnstable = (mkPkgsRust system).arti; @@ -456,6 +467,7 @@ splora-liquid = sploraInput.packages.${system}.splora-liquid or (throw "splora flake input has no packages.${system}.splora-liquid (fail-closed)"); + splora-web = pkgs.callPackage ./nix/packages/splora-web.nix { inherit craneLib sploraInput; }; # Rust nixfmt-rs (Mic92). Not Haskell pkgs.nixfmt and not # nixfmt-rfc-style (that alias is the channel Haskell package). # Upstream attribute is packages.${system}.default. Binary name diff --git a/hosts/mail-vps/configuration.nix b/hosts/mail-vps/configuration.nix index e1ee3e5..ef1b470 100644 --- a/hosts/mail-vps/configuration.nix +++ b/hosts/mail-vps/configuration.nix @@ -66,7 +66,7 @@ # a production leaf that names mail., run # `just point-stalwart-mail-tls -- --live --restart` on the host. # Same durable PEMs as Axum. Not product browser :443. -# 14. Review fail2ban/ssh exposure; lock SSH to admin nets if possible. +# 14. Review sshguard bans and SSH exposure; lock SSH to admin nets if possible. # 15. Enable surmount.artiHiddenService when HS identity is on host (later). # With https UI, leave arti backendAddress null so the module auto- # binds loopback cleartext API for the onion reverse-proxy (or set @@ -337,7 +337,9 @@ curl dig tcpdump - git + # nixos-26.05 git defaults pythonSupport to true, which installs + # python3. The mail host still has git. The flag stays off. + (git.override { pythonSupport = false; }) scc fio sysstat diff --git a/modules/arti-hidden-service.nix b/modules/arti-hidden-service.nix index dbe95e1..5f8f18d 100644 --- a/modules/arti-hidden-service.nix +++ b/modules/arti-hidden-service.nix @@ -31,7 +31,7 @@ # auto-points at managementUi local cleartext (loopback API bind). Pointing # cleartext Arti at the primary https TCP without that path still warns. # -# Version assumption: arti.toml shape matches Surmount-owned Arti 2.7.0 +# Version assumption: arti.toml shape matches Surmount-owned Arti 2.8.0 # (nix/packages/arti-onion-service.nix; not stock nixpkgs 1.4.x lag). Keys: # [storage] cache_dir/state_dir, [onion_services.""] proxy_ports, # [proxy] socks_listen (not legacy socks_port). See upstream @@ -166,8 +166,8 @@ let # No private keys in this file. Onion service identity lives under # onionServiceStateDir on the host (deploy secrets; never in git). # - # Version assumption: Surmount-owned Arti 2.7.0 config shape - # (nix/packages/arti-onion-service.nix; GitLab arti-v2.7.0). Not stock + # Version assumption: Surmount-owned Arti 2.8.0 config shape + # (nix/packages/arti-onion-service.nix; GitLab arti-v2.8.0). Not stock # nixpkgs 1.4.x lag. Keys: socks_listen, onion_services, proxy_ports. # Command: arti proxy -c /etc/surmount/arti.toml # diff --git a/modules/hardening.nix b/modules/hardening.nix index cfdc5e8..3cc2b27 100644 --- a/modules/hardening.nix +++ b/modules/hardening.nix @@ -1,4 +1,4 @@ -# Security defaults: SSH hardening, fail2ban sketch, edge-friendly limits. +# Security defaults: SSH hardening, sshguard, edge-friendly limits. # nginx is transitional-to-delete; prefer limits that transfer to Axum-first edge. # Keep this boring and reversible; deep custom hardening belongs in host config. # @@ -7,12 +7,11 @@ # - Ban decision layer in management-ui (default enforcement off; lean private) # - Unauthorized access -> fast/immediate host blacklist (nftables lean; Q-ACL-1 open) # - Explicit whitelist IPs never banned; track last-used for hygiene -# - Long-term prefer Rust ban helper over Python fail2ban as product identity -# This module's fail2ban block is a LIGHT TRANSITIONAL SKETCH (sshd only), -# not final merciless policy. Design: +# - Long-term prefer Rust ban helper over a host Python service +# sshguard is the sshd ban: permanent after 3 failures. fail2ban is not +# installed. Design notes: # docs/research/access-control-fail2ban.md -# Do not expand aggressive mail jails here without that design + false-positive -# review. Stalwart owns mail anti-abuse first. +# Do not add mail watchers here. Stalwart owns mail anti-abuse first. # # SSH host keys: advertise and generate ed25519 only. NixOS 26.05 default # also offers RSA; this module stops that. Existing RSA files on disk are @@ -72,44 +71,25 @@ in }; }; - # fail2ban: sshd jail only. Permanent ban after 3 failures. - # bantime "-1" is fail2ban's never-unban value. The option type is - # a string. iptables-multiport inserts a rule with no kernel - # timeout. Restore keeps bips rows whose bantime is -1. - # Mail abuse stays with Stalwart. Do not add jails here. - services.fail2ban = { - enable = mkDefault true; - maxretry = 3; - bantime = "-1"; - # modules/networking.nix leaves networking.nftables.enable false, - # so the host firewall backend is iptables and this action is - # iptables-multiport (same package as networking.firewall). - # Native nftables blacklists ip_tables, so the action must - # switch or the ban never installs. - banaction = - if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"; - # SYSTEMD-JOURNAL is the native journal path. SYSLOG depends on - # /dev/log while journald ForwardToSyslog is no. INFO records - # ban, unban, and found lines. DEBUG is not set (it copies matched - # log lines). No mail jails. - daemonSettings.Definition = { - logtarget = "SYSTEMD-JOURNAL"; - loglevel = "INFO"; - }; - jails = { - sshd = { - settings = { - enabled = true; - port = "ssh"; - filter = "sshd"; - backend = "systemd"; - }; - }; - }; + # sshguard 2.5.1 (pinned nixos-26.05): an sshd auth failure scores + # 10. Block and blacklist both fire when cumulated danger >= the + # threshold, so 30 is 3 failures. blacklist pardontime 0 is never + # unblocked and is reloaded from blacklist_file. The nixpkgs module + # uses sshg-fw-ipset with the host iptables firewall, or + # sshg-fw-nft-sets when networking.nftables.enable is true, and + # partOf firewall.service. fail2ban is Python and stays off. + # Do not enable surmount.accessControl or the nft helper. + services.fail2ban.enable = false; + services.sshguard = { + enable = true; + services = [ "sshd" ]; + attack_threshold = 30; + blacklist_threshold = 30; + blacklist_file = "/var/lib/sshguard/blacklist.db"; }; - systemd.services.fail2ban.serviceConfig = { - SyslogIdentifier = "fail2ban"; + systemd.services.sshguard.serviceConfig = { + SyslogIdentifier = "sshguard"; LogRateLimitIntervalSec = "30s"; LogRateLimitBurst = 50000; }; diff --git a/modules/mail-host-rust-cpu.nix b/modules/mail-host-rust-cpu.nix index f99e7ba..3d33114 100644 --- a/modules/mail-host-rust-cpu.nix +++ b/modules/mail-host-rust-cpu.nix @@ -1,7 +1,9 @@ -# Mail-host Cargo builds: -C target-cpu=x86-64-v3 on every rustc Cargo -# runs, including dependency crates and proc-macros. Gated on -# surmount.mailHostCpu.enable (no second option). enable false installs -# nothing, so CI and mkPkgsBare stay generic. +# Mail-host Cargo builds: one rustc argv, -Ctarget-cpu=x86-64-v3, on +# every rustc Cargo runs, including dependency crates and proc-macros. +# Appended, not prepended. cargo-auditable treats its first argument as +# the rustc path. A split "-C" "target-cpu=..." in front made it exec +# "-C". Gated on surmount.mailHostCpu.enable (no second option). enable +# false installs nothing, so CI and mkPkgsBare stay generic. # # Crane buildDepsOnly is a separate derivation (cargoArtifacts). A flag # on the top-level package does not reach that derivation. Stamp both. @@ -49,7 +51,7 @@ let *target-cpu=*) need=0 ;; esac if [ "$need" -eq 1 ]; then - set -- -C "target-cpu=x86-64-v3" "$@" + set -- "$@" "-Ctarget-cpu=x86-64-v3" fi if [ -n "''${SURMOUNT_RUSTC_WRAPPER_NEXT-}" ]; then exec "''${SURMOUNT_RUSTC_WRAPPER_NEXT}" "$real" "$@" diff --git a/modules/management-ui.nix b/modules/management-ui.nix index 18dd010..8438b82 100644 --- a/modules/management-ui.nix +++ b/modules/management-ui.nix @@ -15,6 +15,9 @@ lib, pkgs, options, + # mail-vps passes the flake as specialArgs.self. Module eval without + # that argument leaves the web root unset. + self ? null, ... }: let @@ -374,6 +377,9 @@ let hs.onionServiceStateDir "${hs.stateDir}/published-hostnames" ] + ++ lib.optionals (sploraWebRoot != "") [ + sploraWebRoot + ] ); # EnvironmentFile expects KEY=value lines (e.g. SURMOUNT_SESSION_SECRET=...). @@ -481,7 +487,8 @@ let # Burst keeps http_request and tls_handshake_failed during a scan. LogRateLimitIntervalSec = "30s"; LogRateLimitBurst = 50000; - Environment = envList; + Environment = envList + ++ optional (sploraWebRoot != "") "SURMOUNT_SPLORA_WEB_ROOT=${sploraWebRoot}"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; @@ -560,6 +567,20 @@ let # Operator may still set apexPublicRoot to a host directory. packagedPublicSite = if pkgs ? surmount-public-site then "${pkgs.surmount-public-site}" else ""; + # Trunk client. Flake attribute packages..splora-web + # (nix/packages/splora-web.nix). Overlay name wins when present. + # Otherwise the mail host uses specialArgs.self. Not a store hash. + # Empty when neither is in scope, so module eval without the flake + # does not instantiate the Trunk build. + sploraWebPkg = + if pkgs ? splora-web then + pkgs.splora-web + else if self == null then + null + else + (self.packages.${pkgs.stdenv.hostPlatform.system} or { }).splora-web or null; + sploraWebRoot = if sploraWebPkg == null then "" else "${sploraWebPkg}"; + # Oneshot script the timer execs. builtins.readFile of this path is the # hermetic proof the unit names the management UI and the PEM paths. # No Namecheap client, no API token, no DNS hook. diff --git a/modules/options.nix b/modules/options.nix index c5d04a2..fab1d9f 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -1145,7 +1145,7 @@ in enable = mkOption { type = types.bool; default = true; - description = "Apply SSH hardening, fail2ban sketch, and sensible defaults."; + description = "Apply SSH hardening, sshguard, and sensible defaults."; }; allowPasswordAuth = mkOption { @@ -1204,7 +1204,7 @@ in default = true; description = '' Persistent journald paper trail for host services (mail, management-ui, - sshd, Arti, nft/fail2ban, nix-daemon, optional Vaultwarden, optional + sshd, Arti, sshguard, nix-daemon, optional Vaultwarden, optional Lake). Default true when surmount.enable. Completeness and start-of-OOM clues beat aggressive rate-limit vacuum. Size-capped so logs cannot fill the disk (SHC ticket 261 class). Operator-only; no public log dump. diff --git a/nix/overlays.nix b/nix/overlays.nix index afbac53..db5b04a 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -7,18 +7,87 @@ # Keep this file for any extra pins that should not live in the flake body. # # artiOnionService lives in flake surmountOverlay (distinct from stock -# pkgs.arti). Surmount-owned Arti 2.5.1 source build + onion-service-service; -# not a feature-only override of channel 1.4.2. See -# nix/packages/arti-onion-service.nix. +# pkgs.arti). The expression is nix/packages/arti-onion-service.nix +# (GitLab tag arti-v2.8.0). The Arti 2.8.0 cargoHash is set there +# (sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=). This file +# does not vendor Arti. Not a feature-only override of channel 1.4.2. +# +# This file exports two overlays: +# pins Nix 2.35.4 cli. Safe on checks (mkPkgs) and on the mail host. +# tzdata tzdata 2026e doCheck = false. Mail-host nixosSystem only. +# Not on mkPkgs, so checks.x86_64-linux.ci does not rebuild +# CPython just because now.ck is skipped. # # tzdata 2026e: now.ck is date-dependent. On or after 2026-11-01 the check # fails because America/Phoenix and America/Vancouver are identical from # now on (America/Edmonton and America/Winnipeg are the same class). This # is not a gcc or x86-64-v3 failure. Same attribute change as # NixOS/nixpkgs pull 582069. gcc.arch and x86-64-v3 stay as they are. +# +# Nix: nixos-26.05 pkgs.nix is nixVersions.stable (the 2.34.9 recipe). +# nixVersions.nix_2_35 is the 2.35.3 recipe: one fetchFromGitHub source +# and meson components, no cargoHash. 2.35.4 is that recipe plus the +# source of commit 21286d605815196c3fff1960550ab08700fc4e6f and version +# 2.35.4. The nixpkgs channel stays nixos-26.05. NixOS 26.11 is not +# released. +# +# The system package is nix-cli, not nix-everything. nix-everything's +# installPhase symlinks doc and man to nix-manual (the HTML manual; +# json-schema-for-humans). Its doCheck pulls nix-functional-tests, and +# those tests' nativeBuildInputs include mercurial (python3.13-pip). +# The mail host runs nix-daemon from nix-cli, including the systemd +# units that component installs. It does not run the HTML manual or +# the functional tests. nix-nswrapper is only the non-root daemon +# ExecStart; the default daemon user is root, and this host does not +# change that. + +let + # Mail-host package set only. flake nixosConfigurations.mail-vps + # appends this. mkPkgs (checks.x86_64-linux.ci, devShells) does not. + tzdataOverlay = _final: prev: { + tzdata = prev.tzdata.overrideAttrs (_old: { + doCheck = false; # https://lists.iana.org/hyperkitty/list/tz@iana.org/thread/CBEK3HXYBOXGD34ZFDSF64QIDT7ZWGBB/ + }); + }; + + pinsOverlay = final: prev: { + nixVersions = prev.nixVersions.extend ( + _vfinal: vprev: + let + # nix-prefetch-url --unpack printed + # 03h0j2bk97xgm15x02ccph4h1rwwci37mkdy6d1200jxzf51b2a4 + # builtins.convertHash { toHashFormat = "sri"; hashAlgo = "sha256"; } + # printed sha256-RIkVivtdAiBCM77NekZknOcACbyMCdBLqK+fNJeQAA4= + nixSrc2354 = prev.fetchFromGitHub { + owner = "NixOS"; + repo = "nix"; + rev = "21286d605815196c3fff1960550ab08700fc4e6f"; + hash = "sha256-RIkVivtdAiBCM77NekZknOcACbyMCdBLqK+fNJeQAA4="; + }; + components = vprev.nixComponents_2_35.overrideScope ( + _finalScope: _prevScope: { + version = "2.35.4"; + src = nixSrc2354; + patchedSrc = nixSrc2354; + } + ); + in + { + nixComponents_2_35 = components; + # nix-cli: binaries and systemd units. Not nix-everything + # (that package builds nix-manual and mercurial). + nix_2_35 = components.nix-cli; + latest = components.nix-cli; + } + ); -_final: prev: { - tzdata = prev.tzdata.overrideAttrs (_old: { - doCheck = false; # https://lists.iana.org/hyperkitty/list/tz@iana.org/thread/CBEK3HXYBOXGD34ZFDSF64QIDT7ZWGBB/ - }); + # Channel stable stays the 2.34.9 nix-everything attribute. + # Nothing in the mail-host module list selects it. The package + # the system runs is pkgs.nix, the 2.35.4 nix-cli above. + nix = final.nixVersions.nix_2_35; + }; +in +{ + pins = pinsOverlay; + tzdata = tzdataOverlay; } diff --git a/nix/packages/arti-onion-service.nix b/nix/packages/arti-onion-service.nix index 7d10ab9..212f550 100644 --- a/nix/packages/arti-onion-service.nix +++ b/nix/packages/arti-onion-service.nix @@ -2,14 +2,14 @@ # # Why not stock pkgs.arti from the host nixpkgs channel? # That channel packages Arti 1.4.2 (client-default features only). Upstream -# stable is far ahead (2.7.0, GitLab tag arti-v2.7.0). Greenfield Surmount +# stable is far ahead (2.8.0, GitLab tag arti-v2.8.0). Greenfield Surmount # pins the current engine here, same # spirit as Stalwart: channel lag is not a reason to ship old Tor for the # required HS surface. # # Why not only overrideAttrs on host pkgs.arti? # Feature-only overrides inherit the channel version (1.4.2). This expression -# owns the upstream tag (arti-v2.7.0), features, and Surmount capability +# owns the upstream tag (arti-v2.8.0), features, and Surmount capability # passthru. # # Why source build (not binary FOD like Stalwart)? @@ -20,16 +20,16 @@ # Vendor / crates.io: # fetch-cargo-vendor against crates.io returned HTTP 403 from this # environment (same class of failure that forced Stalwart to release-binary -# FODs). We still build *from GitLab source*, but take `cargoDeps` from the -# matching nixpkgs-rust `arti` package (same version / Cargo.lock) so the -# vendor tree is substitutable from cache.nixos.org. When crates.io vendor -# works again, cargoHash can replace the artiUnstable.cargoDeps handoff. +# FODs). nixpkgs-rust pkgs.arti is still 2.7.0. Do not set cargoDeps to +# that tree or to any other 2.7.0 vendor. cargoHash for arti-v2.8.0 is +# sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=. The missing-hash +# throw is gone. Do not pretend a 2.7.0 vendor tree matches this source. # # Toolchain: -# Arti 2.7.0 MSRV is 1.92. Older host channels had rustc < 1.91 and -# rustPackages_* tops out at 1.89. The flake passes a rustPlatform whose -# rustc/cargo meet MSRV (from the nixpkgs-rust input), without rebasing -# the whole host OS channel. +# crates/arti/Cargo.toml in the arti-v2.8.0 tree sets rust-version = "1.92". +# Older host channels had rustc < 1.91 and rustPackages_* tops out at 1.89. +# The flake passes a rustPlatform whose rustc/cargo meet MSRV (from the +# nixpkgs-rust input), without rebasing the whole host OS channel. # # Features: onion-service-service on the arti binary (HS publish / tor-hsrproxy). # Prefer this lean feature over nixpkgs-unstable `full` unless build needs @@ -44,15 +44,15 @@ # # Bump procedure: # 1. Set the single `version` binding below to "X.Y.Z" (GitLab tag -# arti-vX.Y.Z). Assert, drv version, and src tag all use that binding. -# 2. Bump flake input nixpkgs-rust until its pkgs.arti.version matches (for -# cargoDeps), or restore cargoHash once crates.io vendor works. +# arti-vX.Y.Z). The drv version and the src tag both use that binding. +# 2. Prefetch cargoHash for this tag. Do not reuse a 2.7.0 vendor tree. # 3. Refresh src hash (nix build until got: hash, or prefetch). # 4. Confirm MSRV still met by nixpkgs-rust rustc. # 5. Re-read upstream CHANGELOG / example config for TOML shape drift. # 6. Keep passthru.surmountOnionServiceCapable = true. # -# Modelled on nixpkgs pkgs/by-name/ar/arti/package.nix (2.7.0 shape: +# Modelled on nixpkgs pkgs/by-name/ar/arti/package.nix (2.7.0 shape). Source +# tag is now arti-v2.8.0. # buildAndTestSubdir, tokio-util postPatch, ARTI_FS_DISABLE_PERMISSION_CHECKS) # with Surmount pname, lean HS feature, and capability passthru. # @@ -70,19 +70,17 @@ openssl, zstd, versionCheckHook, - # Matching-version arti from nixpkgs-rust (provides substitutable cargoDeps). + # Still passed by the flake. Not a cargoDeps source for 2.8.0. artiUnstable, }: let - # Single source of truth for pin, cargoDeps assert, and GitLab tag. - version = "2.7.0"; + # Single source of truth for pin, drv version, and GitLab tag. + version = "2.8.0"; in assert lib.assertMsg ( artiUnstable != null -) "arti-onion-service: artiUnstable is required (cargoDeps handoff)"; -assert lib.assertMsg (artiUnstable.version == version) - "arti-onion-service: artiUnstable.version must be ${version} (got ${artiUnstable.version or "null"})"; +) "arti-onion-service: artiUnstable is passed by the flake; it is not cargoDeps for ${version}"; rustPlatform.buildRustPackage (finalAttrs: { pname = "arti-onion-service"; @@ -94,7 +92,12 @@ rustPlatform.buildRustPackage (finalAttrs: { owner = "core"; repo = "arti"; tag = "arti-v${finalAttrs.version}"; - hash = "sha256-28s2BnesadPGm1gQDxzIqSgGaUd8ZgoTpKK3WXBRU9Q="; + # nix-prefetch-url --unpack of the GitLab archive and of the + # fetchFromGitLab API URL both printed + # 0jkhr8d9z9m6sffslvahcqnzdvb61kw8hnlgsdm9xnn6lfb80w12 + # builtins.convertHash to sri printed + # sha256-InCAlqPG2p5q049aiPgMZu32LWZQbaqd06amnxrKcEo= + hash = "sha256-InCAlqPG2p5q049aiPgMZu32LWZQbaqd06amnxrKcEo="; }; # Working around a bug in cargo that appears with cargo-auditable, see @@ -108,9 +111,9 @@ rustPlatform.buildRustPackage (finalAttrs: { buildAndTestSubdir = "crates/arti"; - # Substitutable vendor tree from nixpkgs-rust arti (avoids crates.io 403). - # Same version lock as src tag above. - cargoDeps = artiUnstable.cargoDeps; + # nixpkgs-rust pkgs.arti is 2.7.0. Do not point cargoDeps at it. + # cargoHash is the fetchCargoVendor vendor-staging got hash for this tag. + cargoHash = "sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY="; nativeBuildInputs = lib.optionals stdenv.hostPlatform.isLinux [ pkg-config ]; @@ -126,7 +129,7 @@ rustPlatform.buildRustPackage (finalAttrs: { checkFeatures = [ "onion-service-service" ]; checkFlags = [ - # problematic test that hangs the build (same skip as nixpkgs 2.7.0) + # problematic test that hangs the build (same skip as nixpkgs 2.7.0; still skipped on 2.8.0) "--skip=reload_cfg::test::watch_single_file" ]; @@ -148,9 +151,9 @@ rustPlatform.buildRustPackage (finalAttrs: { onionServiceFeatures = [ "onion-service-service" ]; # Honest packaging provenance for audits. surmountArtiPackaging = { - mode = "gitlab-source-with-nixpkgs-rust-cargoDeps"; + mode = "gitlab-source-cargoHash-set"; upstreamTag = "arti-v${finalAttrs.version}"; - reason = "crates.io fetch-cargo-vendor 403; reuse matching nixpkgs-rust vendor"; + reason = "arti 2.8.0 cargoHash is sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=; the missing-hash throw is gone; nixpkgs-rust arti 2.7.0 cargoDeps is not this source"; }; }; diff --git a/nix/packages/splora-web.nix b/nix/packages/splora-web.nix new file mode 100644 index 0000000..d6a534e --- /dev/null +++ b/nix/packages/splora-web.nix @@ -0,0 +1,33 @@ +# Trunk CSR bundle of the pinned splora input, directory frontend/web. +# Trunk.toml sets target = index.html and dist = dist. +# Not wasm-pack. Not cargo-leptos. +# The mail-host management UI unit sets SURMOUNT_SPLORA_WEB_ROOT to this +# package output (flake packages..splora-web). The Axum edge +# serves $out. Do not copy dist by hand. +# wasm-bindgen in frontend/web/Cargo.lock is 0.2.128. The CLI must match. +{ + pkgs, + craneLib, + sploraInput, +}: + +let + wasmBindgenCli = + if pkgs ? wasm-bindgen-cli_0_2_128 then + pkgs.wasm-bindgen-cli_0_2_128 + else + pkgs.wasm-bindgen-cli; +in +craneLib.buildTrunkPackage { + pname = "splora-web"; + version = "0.1.0"; + src = sploraInput; + cargoExtraArgs = "--locked --manifest-path frontend/web/Cargo.toml"; + cargoVendorDir = craneLib.vendorCargoDeps { + cargoLock = "${sploraInput}/frontend/web/Cargo.lock"; + }; + trunkIndexPath = "frontend/web/index.html"; + doCheck = false; + wasm-bindgen-cli = wasmBindgenCli; + meta.description = "Splora Trunk client bundle from frontend/web. Not wasm-pack and not cargo-leptos."; +} diff --git a/tests/arti-onion-package.nix b/tests/arti-onion-package.nix index 8d13320..fbcee94 100644 --- a/tests/arti-onion-package.nix +++ b/tests/arti-onion-package.nix @@ -40,11 +40,11 @@ let ); # Named contract: Surmount owns current upstream Arti (not nixpkgs 1.4.2 lag). - # Exact pin is the package binding in arti-onion-service.nix (2.7.0). + # Exact pin is the package binding in arti-onion-service.nix (2.8.0). t0 = - assert pkg.version == "2.7.0"; + assert pkg.version == "2.8.0"; assert !(lib.hasPrefix "1.4" pkg.version); - "t0-version-2.7.0-ok"; + "t0-version-2.8.0-ok"; t1 = assert builtins.elem "onion-service-service" buildFeatures; @@ -73,11 +73,17 @@ let assert builtins.elem "onion-service-service" passthruFeatures; "t6-passthru-onion-service-features-ok"; - # Named contract: Surmount owns GitLab-source packaging mode (not a silent - # return to feature-only overrideAttrs of host nixpkgs 1.4.x). + # Named contract: Surmount owns GitLab-source packaging for arti 2.8.0 + # with this prefetched cargoHash. Reject the old missing-hash mode. + # Not a silent return to feature-only overrideAttrs of host nixpkgs 1.4.x. packaging = pkg.passthru.surmountArtiPackaging or { }; + expectedCargoHash = "sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY="; t7 = - assert packaging.mode or null == "gitlab-source-with-nixpkgs-rust-cargoDeps"; + assert (packaging.mode or null) != "gitlab-source-cargoHash-missing"; + assert (packaging.mode or null) == "gitlab-source-cargoHash-set"; + assert (pkg.cargoHash or null) == expectedCargoHash; + assert lib.hasInfix expectedCargoHash (packaging.reason or ""); + assert lib.hasInfix "missing-hash throw is gone" (packaging.reason or ""); assert packaging.upstreamTag or null == "arti-v${pkg.version}"; "t7-packaging-provenance-ok"; diff --git a/tests/fail2ban-permanent-eval.nix b/tests/fail2ban-permanent-eval.nix index 80aca65..31f02f0 100644 --- a/tests/fail2ban-permanent-eval.nix +++ b/tests/fail2ban-permanent-eval.nix @@ -1,8 +1,13 @@ -# Hermetic contract: sshd fail2ban ban is permanent after 3 failures, -# and the ban action is the one this host's firewall applies. +# Hermetic contract: sshd bans are permanent after 3 failures, the +# ban uses the host firewall, and fail2ban is not installed. +# sshguard 2.5.1 scores a normal sshd auth failure at 10. Block and +# blacklist both use >=, so attack_threshold 30 and +# blacklist_threshold 30 are 3 failures and a permanent blacklist. # Host firewall is modules/networking.nix: networking.firewall on, -# networking.nftables.enable left false, so the backend is iptables. -# Do not enable the management UI nft helper. Do not add jails. +# networking.nftables.enable left false, so the pinned nixpkgs module +# selects sshg-fw-ipset (iptables + ipset) and partOf firewall.service. +# Turning nftables on in this eval only switches that backend to +# sshg-fw-nft-sets. Do not enable the management UI nft helper. # Never embeds host SKUs, addresses, RAM, or guest disk sizes. let flake = builtins.getFlake (toString ./..); @@ -33,115 +38,99 @@ let ++ extraModules; }; failedAssertions = e: builtins.filter (a: !a.assertion) e.config.assertions; + pathNames = e: map (p: p.pname or p.name or "") e.config.systemd.services.sshguard.path; + execText = e: toString e.config.systemd.services.sshguard.serviceConfig.ExecStart; - # Product jail is sshd. DEFAULT is the fail2ban defaults section the - # NixOS module always writes (bantime, maxretry, banaction). It is - # not a second jail we added. - wantJails = [ - "DEFAULT" - "sshd" - ]; + checkPermanent = + e: + let + sg = e.config.services.sshguard; + in + assert lib.assertMsg (sg.enable == true) "sshguard must be enabled"; + assert lib.assertMsg (sg.services == [ "sshd" ]) "sshguard must watch sshd only"; + assert lib.assertMsg ( + sg.attack_threshold == 30 + ) "attack_threshold is ${toString sg.attack_threshold}, want 30 (3 sshd failures)"; + assert lib.assertMsg ( + sg.blacklist_threshold == 30 + ) "blacklist_threshold is ${toString sg.blacklist_threshold}, want 30 (permanent after 3)"; + assert lib.assertMsg ( + toString sg.blacklist_file == "/var/lib/sshguard/blacklist.db" + ) "blacklist file is ${toString sg.blacklist_file}"; + assert lib.assertMsg (e.config.services.fail2ban.enable == false) "fail2ban must be disabled"; + assert lib.assertMsg (lib.hasInfix "-a 30" (execText e)) "sshguard command must pass -a 30"; + assert lib.assertMsg (lib.hasInfix "-b 30:/var/lib/sshguard/blacklist.db" ( + execText e + )) "sshguard command must pass a permanent blacklist of 30"; + assert lib.assertMsg (builtins.elem "firewall.service" e.config.systemd.services.sshguard.partOf) + "sshguard must be part of the host firewall unit"; + assert lib.assertMsg (e.config.surmount.accessControl.nftHelper == false) "nftHelper must stay off"; + assert lib.assertMsg ( + e.config.surmount.accessControl.enable == false + ) "accessControl must stay off"; + true; checkHostFirewall = let e = evalHardening [ ]; - f2b = e.config.services.fail2ban; - defs = f2b.jails.DEFAULT.settings; - sshd = f2b.jails.sshd.settings; fw = e.config.networking.firewall; - jailNames = builtins.attrNames f2b.jails; + names = pathNames e; + preStart = e.config.systemd.services.sshguard.preStart or ""; in assert failedAssertions e == [ ]; - assert lib.assertMsg (f2b.maxretry == 3) "maxretry is ${toString f2b.maxretry}, want 3"; - assert lib.assertMsg (f2b.bantime == "-1") "bantime is ${f2b.bantime}, want -1 (permanent)"; - assert lib.assertMsg (defs.maxretry == 3) "DEFAULT maxretry is ${toString defs.maxretry}, want 3"; - assert lib.assertMsg ( - defs.bantime == "-1" - ) "DEFAULT bantime is ${defs.bantime}, want -1 (permanent)"; - assert lib.assertMsg (!(sshd ? maxretry)) "sshd jail must inherit maxretry, not override it"; - assert lib.assertMsg (!(sshd ? bantime)) "sshd jail must inherit bantime, not override it"; - assert lib.assertMsg (f2b.enable == true) "fail2ban must be enabled"; - assert lib.assertMsg (sshd.enabled == true) "sshd jail must be enabled"; - assert lib.assertMsg (sshd.filter == "sshd") "sshd jail filter must be sshd"; - assert lib.assertMsg (sshd.backend == "systemd") "sshd jail backend must be systemd"; - assert lib.assertMsg ( - jailNames == wantJails - ) "jails are ${builtins.concatStringsSep "," jailNames}, want DEFAULT,sshd"; + assert checkPermanent e; assert lib.assertMsg (fw.enable == true) "host firewall must be enabled"; assert lib.assertMsg ( e.config.networking.nftables.enable == false ) "host firewall must not turn on native nftables"; assert lib.assertMsg (fw.backend == "iptables") "firewall backend is ${fw.backend}, want iptables"; - assert lib.assertMsg ( - f2b.banaction == "iptables-multiport" - ) "banaction is ${f2b.banaction}, want iptables-multiport"; - assert lib.assertMsg ( - defs.banaction == "iptables-multiport" - ) "DEFAULT banaction is ${defs.banaction}, want iptables-multiport"; - assert lib.assertMsg (!(sshd ? action)) "sshd jail must not override action"; - assert lib.assertMsg (!(sshd ? banaction)) "sshd jail must not override banaction"; - assert lib.assertMsg ( - f2b.packageFirewall == fw.package - ) "fail2ban firewall package must be the host firewall package"; assert lib.assertMsg ( fw.package.pname == "iptables" ) "firewall package is ${fw.package.pname}, want iptables"; - assert lib.assertMsg (builtins.elem "firewall.service" e.config.systemd.services.fail2ban.partOf) - "fail2ban must be part of the host firewall unit"; - assert lib.assertMsg (f2b.bantime-increment.enable == false) "bantime increment must stay off"; + assert lib.assertMsg (builtins.elem "iptables" names) "sshguard path must include iptables"; + assert lib.assertMsg (builtins.elem "ipset" names) "sshguard path must include ipset"; assert lib.assertMsg ( - (f2b.daemonSettings.Definition.dbfile or "") == "/var/lib/fail2ban/fail2ban.sqlite3" - ) "permanent bans need the fail2ban sqlite db"; - assert lib.assertMsg (e.config.surmount.accessControl.nftHelper == false) "nftHelper must stay off"; + !(builtins.elem "nftables" names) + ) "iptables host must not put nftables on the sshguard path"; assert lib.assertMsg ( - e.config.surmount.accessControl.enable == false - ) "accessControl must stay off"; + lib.hasInfix "iptables" preStart + && lib.hasInfix "sshguard4" preStart + && lib.hasInfix "DROP" preStart + ) "sshguard must install a host-firewall DROP for sshguard4"; "host-firewall-permanent-sshd-ban-ok"; - # If native nftables is turned on, ip_tables is blacklisted. The ban - # action must switch to nftables-multiport or the ban never installs. - # This check forces that in the eval only. It does not enable nftHelper - # and it does not add a jail. + # Native nftables blacklists ip_tables. The pinned sshguard module + # then puts nftables on the unit path (sshg-fw-nft-sets). Thresholds + # stay 3 failures and permanent. This does not enable nftHelper. checkNftablesSwitch = let e = evalHardening [ { networking.nftables.enable = true; } ]; - f2b = e.config.services.fail2ban; fw = e.config.networking.firewall; + names = pathNames e; in assert failedAssertions e == [ ]; - assert lib.assertMsg ( - f2b.maxretry == 3 - ) "nftables-switch maxretry is ${toString f2b.maxretry}, want 3"; - assert lib.assertMsg (f2b.bantime == "-1") "nftables-switch bantime is ${f2b.bantime}, want -1"; + assert checkPermanent e; assert lib.assertMsg ( e.config.networking.nftables.enable == true ) "nftables switch eval must enable nftables"; assert lib.assertMsg ( fw.backend == "nftables" ) "nftables switch backend is ${fw.backend}, want nftables"; - assert lib.assertMsg ( - f2b.banaction == "nftables-multiport" - ) "nftables switch banaction is ${f2b.banaction}, want nftables-multiport"; - assert lib.assertMsg ( - f2b.jails.DEFAULT.settings.banaction == "nftables-multiport" - ) "nftables switch DEFAULT banaction drifted"; - assert lib.assertMsg (!(f2b.jails.sshd.settings ? action)) "sshd jail must not pin an action"; - assert lib.assertMsg ( - f2b.packageFirewall == fw.package - ) "nftables switch packageFirewall must match the firewall package"; assert lib.assertMsg ( fw.package.pname == "nftables" ) "nftables switch package is ${fw.package.pname}, want nftables"; - assert lib.assertMsg (builtins.elem "ip_tables" e.config.boot.blacklistedKernelModules) - "nftables firewall must blacklist ip_tables, so an iptables ban action cannot install"; + assert lib.assertMsg (builtins.elem "nftables" names) + "nftables host must put nftables on the sshguard path"; assert lib.assertMsg ( - e.config.surmount.accessControl.nftHelper == false - ) "nftHelper must stay off when nftables is on"; + !(builtins.elem "iptables" names) + ) "nftables host must not put iptables on the sshguard path"; + assert lib.assertMsg (builtins.elem "ip_tables" e.config.boot.blacklistedKernelModules) + "nftables firewall must blacklist ip_tables"; assert lib.assertMsg ( - builtins.attrNames f2b.jails == wantJails - ) "nftables switch must not add jails"; + e.config.services.sshguard.services == [ "sshd" ] + ) "nftables switch must not add services"; "nftables-switch-permanent-sshd-ban-ok"; in { diff --git a/tests/mail-host-rust-cpu.nix b/tests/mail-host-rust-cpu.nix index 4e586fa..8f4ede0 100644 --- a/tests/mail-host-rust-cpu.nix +++ b/tests/mail-host-rust-cpu.nix @@ -66,7 +66,8 @@ let goodHook = text: - lib.hasInfix "target-cpu=x86-64-v3" text + lib.hasInfix ''set -- "$@" "-Ctarget-cpu=x86-64-v3"'' text + && !(lib.hasInfix ''-C "target-cpu='' text) && lib.hasInfix "RUSTFLAGS" text && lib.hasInfix "CARGO_ENCODED_RUSTFLAGS" text; diff --git a/tests/module-eval.nix b/tests/module-eval.nix index b688865..0f75b6f 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -3605,7 +3605,6 @@ let scOf = name: e.config.systemd.services.${name}.serviceConfig; journal = e.config.services.journald.extraConfig or ""; ssh = e.config.services.openssh.settings; - f2b = e.config.services.fail2ban.daemonSettings.Definition; scram = scOf "surmount-scram"; sshdSc = scOf "sshd"; etSc = scOf "eternal-terminal"; @@ -3617,7 +3616,8 @@ let etNice = etSc.Nice or null; etMem = etSc.MemoryMax or null; tcpForward = ssh.AllowTcpForwarding or null; - jailNames = builtins.attrNames e.config.services.fail2ban.jails; + gitPkgs = builtins.filter (p: (p.pname or "") == "git") e.config.environment.systemPackages; + gitHasPython = p: builtins.any (d: (d.pname or "") == "python3") (p.buildInputs or [ ]); scramFlags = [ "NoNewPrivileges" "PrivateTmp" @@ -3676,7 +3676,7 @@ let assert requireJournalBurst "surmount-management-ui" (scOf "surmount-management-ui"); assert requireJournalBurst "surmount-scram" (scOf "surmount-scram"); assert requireJournalBurst "sshd" (scOf "sshd"); - assert requireJournalBurst "fail2ban" (scOf "fail2ban"); + assert requireJournalBurst "sshguard" (scOf "sshguard"); assert requireJournalBurst "eternal-terminal" (scOf "eternal-terminal"); assert requireJournalBurst "nix-daemon" (scOf "nix-daemon"); assert requireSyslog "nix-daemon" "nix-daemon" (scOf "nix-daemon"); @@ -3689,7 +3689,7 @@ let assert requireJournalStreams "surmount-management-ui" (scOf "surmount-management-ui"); assert requireSyslog "surmount-scram" "surmount-scram" (scOf "surmount-scram"); assert requireJournalStreams "surmount-scram" (scOf "surmount-scram"); - assert requireSyslog "fail2ban" "fail2ban" (scOf "fail2ban"); + assert requireSyslog "sshguard" "sshguard" (scOf "sshguard"); assert requireSyslog "eternal-terminal" "eternal-terminal" (scOf "eternal-terminal"); assert (scOf "surmount-scram").NoNewPrivileges == true; assert (scOf "surmount-scram").ProtectSystem == "strict"; @@ -3699,8 +3699,11 @@ let assert (toString ssh.LoginGraceTime) == "60"; assert (ssh.LogLevel or "") == "VERBOSE"; assert (ssh.AuthenticationMethods or "") == "publickey"; - assert f2b.logtarget == "SYSTEMD-JOURNAL"; - assert f2b.loglevel == "INFO"; + assert e.config.services.fail2ban.enable == false; + assert e.config.services.sshguard.enable == true; + assert e.config.services.sshguard.services == [ "sshd" ]; + assert e.config.services.sshguard.attack_threshold == 30; + assert e.config.services.sshguard.blacklist_threshold == 30; assert e.config.services.eternal-terminal.enable == true; assert e.config.services.eternal-terminal.verbosity == 2; assert e.config.services.eternal-terminal.silent == false; @@ -3726,8 +3729,8 @@ let assert etMem == null; assert tcpForward != "no"; assert tcpForward != false; - assert builtins.elem "sshd" jailNames; - assert builtins.all (n: !(lib.hasInfix "mail" (lib.toLower n))) jailNames; + assert gitPkgs != [ ]; + assert builtins.all (p: !(gitHasPython p)) gitPkgs; # Gates this sample does not turn on. assert e.config.surmount.vaultwarden.enable == false; assert e.config.surmount.lake.enable == false; From f6d4389e850717dc21e9a912b305236ede89d853 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Fri, 6 Nov 2026 12:30:16 -0700 Subject: [PATCH 17/22] fixes --- crates/surmount-deploy-host/src/args.rs | 15 +- crates/surmount-deploy-host/src/run.rs | 228 ++++++++++-------- .../tests/deploy_host_cli.rs | 184 +++++++++++--- docs/OPS.md | 13 +- docs/TEST-AND-DEPLOY.md | 32 ++- docs/deploy-host-local.md | 9 +- flake.nix | 3 + justfile | 6 +- nix/packages/splora-web.nix | 62 ++++- 9 files changed, 375 insertions(+), 177 deletions(-) diff --git a/crates/surmount-deploy-host/src/args.rs b/crates/surmount-deploy-host/src/args.rs index af9914e..e0a1912 100644 --- a/crates/surmount-deploy-host/src/args.rs +++ b/crates/surmount-deploy-host/src/args.rs @@ -18,7 +18,14 @@ Required: Must not start with '-' (option-shaped values rejected). Options: - --dry-run Print planned commands; do not rsync or rebuild. + --dry-run Rsync the public tree and the private host-local + overlay, bootstrap gccarch-x86-64-v3 the same way + as a real deploy, and run nixos-rebuild build + with the same path: flake ref and + --option max-jobs 32 --option cores 0. + Does not run nixos-rebuild switch, does not change + /nix/var/nix/profiles/system, does not run + post-switch smoke, and does not install secrets. --host-local DIR Path to host-local overlay (local machine or mount). Or env SURMOUNT_HOST_LOCAL_DIR. Required for lockout / layout checks unless --skip-host-local-check. @@ -76,10 +83,12 @@ Environment: Same as --secrets-from-secret-service SURMOUNT_SECRETS_HOST_ID Same as --secrets-host-id -After rebuild (success or non-zero), deploy-host always runs post-switch -smoke on the remote via SSH (surmount-deploy-host-post-switch-smoke): +After a real deploy (no --dry-run), success or non-zero, deploy-host +runs post-switch smoke on the remote via SSH +(surmount-deploy-host-post-switch-smoke): generation pointer, systemctl is-active sshd/stalwart-mail/ surmount-management-ui (starts UI if inactive), loopback /health. +Dry-run does not run that smoke and does not switch the running generation. "#; pub const ALLOWED_SECRETS_KINDS: &[&str] = &[ diff --git a/crates/surmount-deploy-host/src/run.rs b/crates/surmount-deploy-host/src/run.rs index 6c4a5f1..d5e0e3b 100644 --- a/crates/surmount-deploy-host/src/run.rs +++ b/crates/surmount-deploy-host/src/run.rs @@ -191,56 +191,22 @@ fn run_deploy(opts: DeployOpts) -> Result<(), ToolError> { } if !opts.skip_sync { - if opts.dry_run { - if let Some(id) = operator_ssh_identity(&opts.target) { - println!( - "deploy-host: dry-run: ssh as {} uses {} (plus keepalives). Override: SURMOUNT_DEPLOY_SSH_IDENTITY", - opts.target, - id.display() - ); - } - println!( - "deploy-host: dry-run: rsync public tree -> {}:{} / (excludes host-local, secrets patterns; not private-data admission control)", - opts.target, - shell_quote(&opts.remote_dir) - ); - let excl = RSYNC_EXCLUDES.join(" "); - println!( - "deploy-host: dry-run: rsync -az --delete {excl} {}/ {}:{} /", - shell_quote(&repo_root.to_string_lossy()), - opts.target, - shell_quote(&opts.remote_dir) - ); - } else { - ssh_run( - &opts.target, - &format!("mkdir -p {}", shell_quote(&opts.remote_dir)), - )?; - rsync_remote( - &["-az", "--delete"], - RSYNC_EXCLUDES, - &repo_root, - &opts.target, - &opts.remote_dir, - )?; - } + ssh_run( + &opts.target, + &format!("mkdir -p {}", shell_quote(&opts.remote_dir)), + )?; + rsync_remote( + &["-az", "--delete"], + RSYNC_EXCLUDES, + &repo_root, + &opts.target, + &opts.remote_dir, + )?; } if opts.host_local.is_some() && !opts.skip_host_local_check { let remote_hl = format!("{}/host-local", opts.remote_dir.trim_end_matches('/')); - if opts.dry_run { - println!( - "deploy-host: dry-run: ensure remote host-local at {}:{} (operator-placed; not from public git)", - opts.target, - shell_quote(&remote_hl) - ); - println!( - "deploy-host: dry-run: rsync {} private host-local -> {}:{}/ (never into tracked hosts/; default without --delete unless --host-local-delete)", - hl_rsync_flags.join(" "), - opts.target, - shell_quote(&remote_hl) - ); - } else if let Some(hl) = opts.host_local.as_ref() { + if let Some(hl) = opts.host_local.as_ref() { ssh_run( &opts.target, &format!("mkdir -p {}", shell_quote(&remote_hl)), @@ -294,6 +260,24 @@ fn run_deploy(opts: DeployOpts) -> Result<(), ToolError> { } let rebuild_flake_ref = format!("path:{}#{}", opts.remote_dir, opts.flake_attr); + if opts.dry_run { + // Build only. Do not switch, do not touch the system profile, + // and do not run post-switch smoke. + let build_remote_cmd = build_remote_command(&rebuild_flake_ref); + let (finish, _build_stdout) = ssh_captured(&opts.target, &build_remote_cmd); + let build_rc = build_outcome(finish)?; + if build_rc != 0 { + return Err(ToolError::fail(format!( + "nixos-rebuild build failed (exit {build_rc}); the running generation was not switched" + ))); + } + println!( + "deploy-host: nixos-rebuild build finished; the running generation was not switched" + ); + let _ = io::stdout().flush(); + return Ok(()); + } + let rebuild_remote_cmd = switch_remote_command(&rebuild_flake_ref); let smoke_name = smoke_remote_name(); let smoke_remote_cmd = format!( @@ -301,72 +285,37 @@ fn run_deploy(opts: DeployOpts) -> Result<(), ToolError> { smoke = smoke_name ); - let mut rebuild_rc = 0i32; - let mut smoke_rc = 0i32; - let mut built_generation: Option = None; - if opts.dry_run { - println!( - "deploy-host: dry-run: ssh keepalives ServerAliveInterval=30 ServerAliveCountMax=10 TCPKeepAlive=yes" - ); - println!( - "deploy-host: dry-run: a dropped connection retries before nixos-rebuild; a switch SSH drop is not retried" - ); - println!( - "deploy-host: dry-run: rebuild uses path: flake so host-local (rsync'd, not in public git) is visible to Nix (git flake would omit untracked/gitignored paths)" - ); - println!( - "deploy-host: dry-run: before the system build, bootstrap gccarch-x86-64-v3 when nix config show system-features omits it (write /run/surmount-nix.conf, bind-mount over the store nix.conf, restart nix-daemon only if the feature was missing). Dry-run does not apply that bootstrap. A /run bind does not survive reboot." - ); - println!( - "deploy-host: dry-run: ssh -- {} {}", - shell_quote(&opts.target), - shell_quote(&rebuild_remote_cmd) - ); - println!( - "deploy-host: dry-run: post-switch smoke runs after a rebuild that finished (including a non-zero rebuild exit): ssh -- {} {}", - shell_quote(&opts.target), - shell_quote(&smoke_remote_cmd) - ); - println!( - "deploy-host: dry-run: smoke checks generation + systemctl is-active sshd stalwart-mail surmount-management-ui (start UI if inactive) + loopback /health" - ); - } else { - // One switch attempt. A drop after this starts may have changed the host. - // Capture stdout so the built-generation line can be read, and copy - // both streams through so a non-zero rebuild's own error stays visible. - let (finish, rebuild_stdout) = ssh_captured(&opts.target, &rebuild_remote_cmd); - built_generation = parse_built_generation(&rebuild_stdout); - if let RemoteFinish::Connection { code } = &finish { - eprintln!( - "deploy-host: nixos-rebuild exit={}", - ssh_status_label(*code) - ); - } - rebuild_rc = switch_outcome(finish)?; - eprintln!("deploy-host: nixos-rebuild exit={rebuild_rc}"); + // One switch attempt. A drop after this starts may have changed the host. + // Capture stdout so the built-generation line can be read, and copy + // both streams through so a non-zero rebuild's own error stays visible. + let (finish, rebuild_stdout) = ssh_captured(&opts.target, &rebuild_remote_cmd); + let built_generation = parse_built_generation(&rebuild_stdout); + if let RemoteFinish::Connection { code } = &finish { eprintln!( - "deploy-host: running post-switch smoke on {} (after a finished rebuild)", - opts.target + "deploy-host: nixos-rebuild exit={}", + ssh_status_label(*code) ); - smoke_rc = match retry_remote(&format!("post-switch smoke ssh -- {}", opts.target), || { - ssh_finish(&opts.target, &smoke_remote_cmd) - }) { - Ok(code) => code, - Err(err) => { - return Err(ToolError::fail(format!( - "{err}; not starting a second switch" - ))); - } - }; - eprintln!("deploy-host: post-switch smoke exit={smoke_rc}"); } + let rebuild_rc = switch_outcome(finish)?; + eprintln!("deploy-host: nixos-rebuild exit={rebuild_rc}"); + eprintln!( + "deploy-host: running post-switch smoke on {} (after a finished rebuild)", + opts.target + ); + let smoke_rc = match retry_remote(&format!("post-switch smoke ssh -- {}", opts.target), || { + ssh_finish(&opts.target, &smoke_remote_cmd) + }) { + Ok(code) => code, + Err(err) => { + return Err(ToolError::fail(format!( + "{err}; not starting a second switch" + ))); + } + }; + eprintln!("deploy-host: post-switch smoke exit={smoke_rc}"); print!("{POST_SMOKE_TAIL}"); let _ = io::stdout().flush(); - - if opts.dry_run { - return Ok(()); - } if rebuild_rc != 0 && smoke_rc != 0 { return Err(ToolError::fail(format!( "nixos-rebuild failed (exit {rebuild_rc}) and post-switch smoke failed (exit {smoke_rc})" @@ -532,6 +481,18 @@ fn ssh_status_label(code: Option) -> String { } } +/// One remote build. Same gccarch bootstrap as `switch_remote_command`, +/// then `nixos-rebuild build`. Does not switch and does not read or +/// write `/nix/var/nix/profiles/system`. `nixos-rebuild build` leaves a +/// `result` symlink in the remote working directory. +fn build_remote_command(flake_ref: &str) -> String { + let flake_q = shell_quote(flake_ref); + let bootstrap = crate::nix_conf::gccarch_bootstrap_remote(); + let build = + format!("nixos-rebuild build --flake {flake_q} --option max-jobs 32 --option cores 0"); + format!("{bootstrap}\n{build}") +} + /// One remote switch. Prints `synthetic-built-generation:` after the rebuild. /// Before that build, bootstrap `gccarch-x86-64-v3` on the running daemon /// when `nix config show` system-features omits it. The bootstrap writes @@ -722,6 +683,22 @@ where ))) } +/// The build is one attempt. It must not fall through into a switch. +fn build_outcome(finish: RemoteFinish) -> Result { + match finish { + RemoteFinish::Exited(code) => Ok(code), + RemoteFinish::Connection { code } => { + let shown = ssh_status_label(code); + Err(ToolError::fail(format!( + "connection failed during nixos-rebuild build (ssh status {shown}); not switching the running generation" + ))) + } + RemoteFinish::Local(msg) => Err(ToolError::fail(format!( + "connection failed during nixos-rebuild build ({msg}); not switching the running generation" + ))), + } +} + /// The switch is one attempt. Never call this in a retry loop. fn switch_outcome(finish: RemoteFinish) -> Result { match finish { @@ -964,9 +941,11 @@ mod tests { assert!(PROFILE_GENERATION_REMOTE.contains("readlink")); let cmd = switch_remote_command("path:/tmp/repo#mail-vps"); assert!(cmd.contains("nixos-rebuild switch --flake")); + assert!(!cmd.contains("nixos-rebuild build")); assert!(cmd.contains("--option max-jobs 32")); assert!(cmd.contains("--option cores 0")); assert!(cmd.contains("path:/tmp/repo#mail-vps")); + assert!(cmd.contains("/nix/var/nix/profiles/system")); assert!(cmd.contains("synthetic-built-generation:")); assert!(!cmd.contains("post-switch-smoke")); let rebuild_at = cmd @@ -998,4 +977,41 @@ mod tests { assert!(!cmd.contains("nix config show extra-system-features")); assert!(cmd.contains("not starting the system build")); } + + #[test] + fn dry_run_command_is_build_and_does_not_switch_or_touch_profile() { + let cmd = build_remote_command("path:/tmp/repo#mail-vps"); + assert!( + cmd.contains( + "nixos-rebuild build --flake path:/tmp/repo#mail-vps --option max-jobs 32 --option cores 0" + ), + "{cmd}" + ); + assert!(!cmd.contains("nixos-rebuild switch"), "{cmd}"); + assert!(!cmd.contains("/nix/var/nix/profiles/system"), "{cmd}"); + assert!(!cmd.contains("nix-env"), "{cmd}"); + assert!(!cmd.contains("nix-collect-garbage"), "{cmd}"); + assert!(!cmd.contains("--upgrade"), "{cmd}"); + assert!(!cmd.contains("post-switch-smoke"), "{cmd}"); + assert!(!cmd.contains("synthetic-built-generation:"), "{cmd}"); + let rebuild_at = cmd + .find("nixos-rebuild build") + .expect("system build command"); + let bootstrap = &cmd[..rebuild_at]; + assert!( + bootstrap.contains("/run/surmount-nix.conf"), + "bootstrap must run before the system build" + ); + assert!( + bootstrap.contains("gccarch-x86-64-v3"), + "bootstrap must name the missing feature" + ); + assert!( + !bootstrap.contains("nixos-rebuild"), + "bootstrap text must not add a second nixos-rebuild line" + ); + let switched = switch_remote_command("path:/tmp/repo#mail-vps"); + assert!(switched.contains("nixos-rebuild switch --flake")); + assert!(!switched.contains("nixos-rebuild build")); + } } diff --git a/crates/surmount-deploy-host/tests/deploy_host_cli.rs b/crates/surmount-deploy-host/tests/deploy_host_cli.rs index 08da333..530f58b 100644 --- a/crates/surmount-deploy-host/tests/deploy_host_cli.rs +++ b/crates/surmount-deploy-host/tests/deploy_host_cli.rs @@ -76,10 +76,14 @@ fn secrets_stub() -> PathBuf { write_exec( &dir, "secrets-install-host", - "#!/bin/sh\necho secrets-install-host \"$@\"\nexit 0\n", + "#!/bin/sh\nprintf ran > \"$(dirname \"$0\")/ran\"\necho secrets-install-host \"$@\"\nexit 0\n", ) } +fn secrets_stub_ran(stub: &Path) -> bool { + stub.parent().unwrap().join("ran").is_file() +} + #[test] fn help_exits_0_and_mentions_dry_run() { let out = cmd().arg("--help").output().unwrap(); @@ -87,6 +91,9 @@ fn help_exits_0_and_mentions_dry_run() { let s = combined(&out); assert!(s.contains("--dry-run"), "{s}"); assert!(s.contains("--install-secrets"), "{s}"); + assert!(s.contains("nixos-rebuild build"), "{s}"); + assert!(!s.contains("do not rsync"), "{s}"); + assert!(!s.contains("Print planned commands"), "{s}"); } #[test] @@ -107,36 +114,37 @@ fn option_shaped_target_rejected() { } #[test] -fn dry_run_good_host_local_prints_rebuild_and_smoke() { - let out = cmd() +fn dry_run_good_host_local_builds_and_does_not_switch() { + let (mut c, ssh_log_path, rsync_log_path) = hermetic_cmd(); + let out = c .args(["--dry-run", "--target", "example.test"]) .arg("--host-local") .arg(good_hl()) .output() .unwrap(); let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); + let rsync_log = fs::read_to_string(&rsync_log_path).unwrap_or_default(); assert!(out.status.success(), "{s}"); - assert!(s.contains("nixos-rebuild") && s.contains("mail-vps"), "{s}"); - assert!(s.contains("path:") && s.contains("#mail-vps"), "{s}"); - assert!(s.contains("host-local"), "{s}"); + assert_dry_run_builds(&ssh_log, &s); assert!( - s.contains("secrets.yaml") || s.contains(".agekey") || s.contains("secrets/"), - "{s}" + rsync_log.lines().filter(|line| !line.is_empty()).count() >= 2, + "dry-run must rsync the public tree and host-local: {rsync_log}" ); - let low = s.to_ascii_lowercase(); assert!( - !s.contains("does not mention authorizedKeys"), - "known-name layout auto-wires keys and must not warn: {s}" + rsync_log.contains("secrets.yaml") + || rsync_log.contains(".agekey") + || rsync_log.contains("secrets/"), + "{rsync_log}" ); + assert!(rsync_log.contains("host-local"), "{rsync_log}"); + assert!(rsync_log.contains("example.test"), "{rsync_log}"); assert!( - low.contains("post-switch smoke") || s.contains("deploy-host-post-switch-smoke"), - "{s}" - ); - assert!( - low.contains("systemctl is-active") || low.contains("surmount-management-ui"), - "{s}" + !s.contains("does not mention authorizedKeys"), + "known-name layout auto-wires keys and must not warn: {s}" ); - assert!(low.contains("health") || low.contains("loopback"), "{s}"); + assert!(!s.to_ascii_lowercase().contains("post-switch smoke"), "{s}"); + assert!(!ssh_log.contains("post-switch-smoke"), "{ssh_log}"); } #[test] @@ -144,7 +152,8 @@ fn root_target_dry_run_mentions_laptop_identity() { let home = temp_dir("ssh-home"); fs::create_dir_all(home.join(".ssh")).unwrap(); fs::write(home.join(".ssh/id_ed25519"), "synthetic-not-a-real-key\n").unwrap(); - let out = cmd() + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c .env("HOME", &home) .args(["--dry-run", "--target", "root@example.test"]) .arg("--host-local") @@ -152,12 +161,11 @@ fn root_target_dry_run_mentions_laptop_identity() { .output() .unwrap(); let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); assert!(out.status.success(), "{s}"); - assert!(s.contains("id_ed25519"), "{s}"); - assert!( - s.contains("keepalives") || s.contains("SURMOUNT_DEPLOY_SSH_IDENTITY"), - "{s}" - ); + assert_dry_run_builds(&ssh_log, &s); + assert!(ssh_log.contains("id_ed25519"), "{ssh_log}"); + assert!(ssh_log.contains("ServerAliveInterval=30"), "{ssh_log}"); } #[test] @@ -233,20 +241,49 @@ fn refuse_host_local_into_hosts_and_secrets_paths() { #[test] fn default_dry_run_does_not_plan_secrets_install() { - let out = cmd() + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c .args(["--dry-run", "--target", "example.test"]) .arg("--host-local") .arg(good_hl()) .output() .unwrap(); - assert!(out.status.success()); - let s = combined(&out).to_ascii_lowercase(); + let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); + assert!(out.status.success(), "{s}"); + assert_dry_run_builds(&ssh_log, &s); + let low = s.to_ascii_lowercase(); assert!( - !s.contains("secrets-install-host") && !s.contains("would run secrets-install"), + !low.contains("secrets-install-host") && !low.contains("would run secrets-install"), "{s}" ); } +#[test] +fn dry_run_build_failure_does_not_switch_or_run_smoke() { + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c + .env("SURMOUNT_TEST_REBUILD_EXIT", "1") + .args(["--dry-run", "--target", "example.test"]) + .arg("--host-local") + .arg(good_hl()) + .output() + .unwrap(); + let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); + assert!(!out.status.success(), "{s}"); + assert!(ssh_log.contains("nixos-rebuild build --flake"), "{ssh_log}"); + assert!(!ssh_log.contains("nixos-rebuild switch"), "{ssh_log}"); + assert!( + !ssh_log.contains("/nix/var/nix/profiles/system"), + "{ssh_log}" + ); + assert!(!ssh_log.contains("post-switch-smoke"), "{ssh_log}"); + assert!(s.contains("the running generation was not switched"), "{s}"); + assert!(!s.contains("nixos-rebuild build finished"), "{s}"); + assert!(!s.to_ascii_lowercase().contains("post-switch smoke"), "{s}"); +} + #[test] fn install_secrets_without_source_fails() { let out = cmd() @@ -355,7 +392,8 @@ fn plant_stage() -> PathBuf { fn opt_in_dry_run_plans_bridge_without_payload() { let stub = secrets_stub(); let stg = plant_stage(); - let out = cmd() + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c .env("SURMOUNT_DEPLOY_SECRETS_INSTALL", &stub) .args(["--dry-run", "--target", "example.test"]) .arg("--host-local") @@ -372,8 +410,9 @@ fn opt_in_dry_run_plans_bridge_without_payload() { .output() .unwrap(); let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); assert!(out.status.success(), "{s}"); - assert!(s.contains("secrets-install-host"), "{s}"); + assert!(s.contains("would run secrets-install-host"), "{s}"); assert!(s.contains("--from-staging"), "{s}"); assert!(s.contains("mail-lab"), "{s}"); assert!(s.contains("example.test"), "{s}"); @@ -381,7 +420,8 @@ fn opt_in_dry_run_plans_bridge_without_payload() { s.contains("--require-kind") && s.contains("tls-cert"), "{s}" ); - assert!(s.contains("nixos-rebuild"), "{s}"); + assert_dry_run_builds(&ssh_log, &s); + assert!(!secrets_stub_ran(&stub), "dry-run must not install secrets"); assert!(!s.contains(PLANTED), "{s}"); } @@ -389,7 +429,8 @@ fn opt_in_dry_run_plans_bridge_without_payload() { fn env_install_secrets_plans_bridge() { let stub = secrets_stub(); let stg = plant_stage(); - let out = cmd() + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c .env("SURMOUNT_DEPLOY_SECRETS_INSTALL", &stub) .env("SURMOUNT_DEPLOY_INSTALL_SECRETS", "1") .env("SURMOUNT_SECRETS_STAGING", &stg) @@ -400,15 +441,19 @@ fn env_install_secrets_plans_bridge() { .output() .unwrap(); let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); assert!(out.status.success(), "{s}"); - assert!(s.contains("secrets-install-host"), "{s}"); + assert!(s.contains("would run secrets-install-host"), "{s}"); + assert_dry_run_builds(&ssh_log, &s); + assert!(!secrets_stub_ran(&stub), "dry-run must not install secrets"); assert!(!s.contains(PLANTED)); } #[test] fn secret_service_only_plans_without_staging() { let stub = secrets_stub(); - let out = cmd() + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c .env("SURMOUNT_DEPLOY_SECRETS_INSTALL", &stub) .args(["--dry-run", "--target", "example.test"]) .arg("--host-local") @@ -422,11 +467,13 @@ fn secret_service_only_plans_without_staging() { .output() .unwrap(); let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); assert!(out.status.success(), "{s}"); - assert!(s.contains("secrets-install-host"), "{s}"); + assert!(s.contains("would run secrets-install-host"), "{s}"); assert!(s.contains("--from-secret-service"), "{s}"); assert!(!s.contains("--from-staging"), "{s}"); - assert!(s.contains("nixos-rebuild"), "{s}"); + assert_dry_run_builds(&ssh_log, &s); + assert!(!secrets_stub_ran(&stub), "dry-run must not install secrets"); } #[test] @@ -477,7 +524,8 @@ fn vaultwarden_admin_and_namecheap_allowlisted() { let stub = secrets_stub(); let stg = plant_stage(); for kind in ["vaultwarden-admin", "namecheap-api"] { - let out = cmd() + let (mut c, ssh_log_path, _) = hermetic_cmd(); + let out = c .env("SURMOUNT_DEPLOY_SECRETS_INSTALL", &stub) .args(["--dry-run", "--target", "example.test"]) .arg("--host-local") @@ -494,9 +542,12 @@ fn vaultwarden_admin_and_namecheap_allowlisted() { .output() .unwrap(); let s = combined(&out); + let ssh_log = fs::read_to_string(&ssh_log_path).unwrap_or_default(); assert!(out.status.success(), "{kind}: {s}"); - assert!(s.contains("secrets-install-host"), "{s}"); + assert!(s.contains("would run secrets-install-host"), "{s}"); assert!(s.contains("--require-kind") && s.contains(kind), "{s}"); + assert_dry_run_builds(&ssh_log, &s); + assert!(!secrets_stub_ran(&stub), "dry-run must not install secrets"); assert!(!s.contains(PLANTED), "{s}"); } } @@ -533,6 +584,61 @@ fn lines_containing(log: &str, needle: &str) -> usize { log.lines().filter(|line| line.contains(needle)).count() } +/// Fake rsync plus the fake ssh, so a dry-run test does not touch the network. +fn hermetic_cmd() -> (Command, PathBuf, PathBuf) { + let dir = temp_dir("hermetic"); + let (ssh, ssh_log) = fake_ssh(&dir); + let rsync_log = dir.join("rsync.log"); + write_exec( + &dir, + "rsync", + "#!/bin/sh\nlog=\"$SURMOUNT_TEST_RSYNC_LOG\"\nprintf '%s\\n' \"$*\" >> \"$log\"\nexit 0\n", + ); + let mut c = cmd(); + let path = std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".to_string()); + c.env("PATH", format!("{}:{path}", dir.display())); + c.env("SURMOUNT_DEPLOY_SSH", &ssh); + c.env("SURMOUNT_TEST_SSH_LOG", &ssh_log); + c.env("SURMOUNT_TEST_RSYNC_LOG", &rsync_log); + (c, ssh_log, rsync_log) +} + +fn assert_dry_run_builds(ssh_log: &str, out: &str) { + assert!( + ssh_log.contains( + "nixos-rebuild build --flake path:/root/surmount-server#mail-vps --option max-jobs 32 --option cores 0" + ), + "{ssh_log}" + ); + assert!(!ssh_log.contains("nixos-rebuild switch"), "{ssh_log}"); + assert!( + !ssh_log.contains("/nix/var/nix/profiles/system"), + "{ssh_log}" + ); + assert!(!ssh_log.contains("post-switch-smoke"), "{ssh_log}"); + assert!( + !ssh_log.contains("surmount-deploy-host-post-switch-smoke"), + "{ssh_log}" + ); + assert!(ssh_log.contains("gccarch-x86-64-v3"), "{ssh_log}"); + assert!(ssh_log.contains("/run/surmount-nix.conf"), "{ssh_log}"); + let build_at = ssh_log + .find("nixos-rebuild build") + .expect("dry-run remote command"); + let boot_at = ssh_log.find("/run/surmount-nix.conf").expect("bootstrap"); + assert!( + boot_at < build_at, + "bootstrap must run before nixos-rebuild build" + ); + assert!( + out.contains( + "deploy-host: nixos-rebuild build finished; the running generation was not switched" + ), + "{out}" + ); + assert!(!out.contains("nixos-rebuild switch"), "{out}"); +} + fn deploy_with_fake_ssh(rebuild_exit: &str, smoke_exit: &str) -> (Output, String) { let dir = temp_dir("fake-ssh"); let (script, log) = fake_ssh(&dir); @@ -580,6 +686,8 @@ fn finished_rebuild_runs_smoke_and_can_succeed() { let (out, log) = deploy_with_fake_ssh("0", "0"); let s = combined(&out); assert!(out.status.success(), "{s}"); + assert!(log.contains("nixos-rebuild switch"), "{log}"); + assert!(!log.contains("nixos-rebuild build"), "{log}"); assert_eq!(lines_containing(&log, "nixos-rebuild"), 1, "{log}"); assert_eq!(lines_containing(&log, "post-switch-smoke"), 1, "{log}"); } diff --git a/docs/OPS.md b/docs/OPS.md index 5cbcba4..2bb20bf 100644 --- a/docs/OPS.md +++ b/docs/OPS.md @@ -753,10 +753,9 @@ usable SSH key **files** (lockout risk if password auth is off), syncs the **public** tree (refuses copying host-local into tracked `hosts/` / `secrets/`), rsyncs private host-local to `REMOTE/host-local/`, optionally runs the secrets install bridge when `--install-secrets` is set, runs -`nixos-rebuild switch --flake path:REMOTE_DIR#mail-vps` on the host (or prints -that plan in `--dry-run`), then **always** runs post-switch smoke on the remote +`nixos-rebuild switch --flake path:REMOTE_DIR#mail-vps` on a real deploy, then **always** runs post-switch smoke on the remote (`nix run .#surmount-deploy-host-post-switch-smoke`) even if rebuild exit was non-zero -(activation may still have applied). Smoke checks generation current, unit +(activation may still have applied). `--dry-run` rsyncs, bootstraps, and runs `nixos-rebuild build` instead of switch. It does not run post-switch smoke and it does not install secrets. Smoke checks generation current, unit activity (`sshd`, `stalwart-mail`, `surmount-management-ui`; starts UI if inactive), loopback `/health` (never invents a public URL), TLS PEM path presence when https edge or `SURMOUNT_TLS_*` is set (durable default @@ -892,9 +891,9 @@ restart the daemon. If the feature is still absent after that restart, the tool stops and does not start the system build. The `/run` bind does not survive reboot. A successful switch is what puts the feature in the generation's `nix.conf`. An operator or an agent -does not SSH a bind before `just deploy`. `just deploy-dry-run` only -prints planned commands and does not build, so a green dry-run does -not apply this bootstrap. +does not SSH a bind before `just deploy`. `just deploy-dry-run` rsyncs, +bootstraps gccarch-x86-64-v3 the same way, and runs `nixos-rebuild build`. +It does not switch the running generation. The nixpkgs cc-wrapper reads `targetPlatform.gcc.arch` and writes `-march=x86-64-v3` into @@ -2040,7 +2039,7 @@ exceptions*. | `nix run .#e2e-host` / `just e2e-host` | Host end-to-end (Rust flake app; `SURMOUNT_E2E_HOST=1` or exit 2; also requires `SURMOUNT_E2E_BASE_URL`; `LAB_IP` unless `SKIP_BAN=1`). **Never** a flake check | | `just publish` | Publish static sites (apex/www from locked `github:SurmountSystems/site` via `nix build .#surmount-public-site`, plus extra vhosts). Not a NixOS generation. | | `just deploy` | Operator's real mail-host switch. Defaults: `--target root@surmount-1` and `--host-local /home/hunter/.local/share/surmount/host-local`. Extra arguments append. Agents must not run this. Explicit equivalent: `just deploy-host -- --target root@surmount-1 --host-local /home/hunter/.local/share/surmount/host-local`. | -| `just deploy-dry-run` | Same switch with `--dry-run` before `--target`. Agents may run this. Extra arguments append. Explicit equivalent: `just deploy-host -- --dry-run --target root@surmount-1 --host-local /home/hunter/.local/share/surmount/host-local`. | +| `just deploy-dry-run` | Rsyncs, bootstraps gccarch-x86-64-v3, and runs `nixos-rebuild build` on the mail host. Does not switch the running generation and does not install secrets. Agents may run this. Extra arguments append. Explicit equivalent: `just deploy-host -- --dry-run --target root@surmount-1 --host-local /home/hunter/.local/share/surmount/host-local`. | | `nix run .#surmount-deploy-host` / `just deploy-host` | Flexible deploy driver with no default target: public rsync + host-local checks + `#mail-vps` rebuild (not CI). That is the NixOS generation. [deploy-host-local.md](deploy-host-local.md) | | `nix run .#surmount-deploy-host-post-switch-smoke` | Post-switch smoke (generation, units, loopback health, listen proof). | | `just fix-public-dashboard` / `nix run .#surmount-fix-public-dashboard` | Compose recovery pin + mint API key + free-443 dry-run (no operator password homework). Optional `--live-free-443` on operator host only. [SECRETS.md](SECRETS.md) | diff --git a/docs/TEST-AND-DEPLOY.md b/docs/TEST-AND-DEPLOY.md index 1964dec..a8eb409 100644 --- a/docs/TEST-AND-DEPLOY.md +++ b/docs/TEST-AND-DEPLOY.md @@ -69,7 +69,7 @@ is only the static sites. It is not this sequence. The agent can and must run the entire procedure **up to and including** `just deploy-dry-run`. Named eval, `just check-remote` (fmt and -nixfmt, then clippy), and the dry-run plan are how errors show up. Dumping those +nixfmt, then clippy), and the dry-run build are how errors show up. Dumping those commands on the operator is a broken leftover. Standing law: [../AGENTS.md](../AGENTS.md) *Deploy dry-run is agent work* and *Prove the operator gate*. The explicit equivalent of the dry-run is @@ -291,7 +291,8 @@ Do not use those as the mail-host quality bar. The **agent** runs this. Agents must have already run section 2 and section 3, then this dry-run, before asking the operator to switch. The dry-run does not switch. A dry-run failure (bad host-local, missing -keys, rsync refuse) is an agent problem until it is green. +keys, rsync refuse, or a failed `nixos-rebuild build`) is an agent +problem until it is green. From the repo root: @@ -319,21 +320,25 @@ nix run .#surmount-deploy-host -- --dry-run --target root@surmount-1 --host-loca The extra `--` after `just deploy-host` stops just from eating flags. `just deploy-dry-run` already includes those flags. -What the dry-run prints (it must not rebuild the guest): +What the dry-run does (it must not switch the guest): - rsync of the public tree to `root@surmount-1:/root/surmount-server/` (excludes `.git/`, `host-local/`, the workspace `target/` cargo cache, `crates/target/`, secrets patterns) - rsync of private host-local to `root@surmount-1:/root/surmount-server/host-local/` -- planned remote command that bootstraps `gccarch-x86-64-v3` when - the daemon's system-features omit it, then - `nixos-rebuild switch --flake path:/root/surmount-server#mail-vps`. - Dry-run prints that plan and does not apply the bootstrap, does not - bind-mount, and does not restart nix-daemon -- post-switch smoke as a planned remote command - -Success is a printed plan and exit 0. It is not a new generation. +- on the guest, bootstraps `gccarch-x86-64-v3` when the daemon's + system-features omit it, the same way `just deploy` does, then runs + `nixos-rebuild build --flake path:/root/surmount-server#mail-vps` + with `--option max-jobs 32 --option cores 0` +- does not run `nixos-rebuild switch` +- does not change `/nix/var/nix/profiles/system` +- does not run post-switch smoke +- does not install secrets (that plan stays print-only) + +Success is a finished build and exit 0. The running generation was not +switched. A later `just deploy` of the same tree reuses the store paths +this build produced. Host-local never comes from public git. The flake on the guest is `path:` so untracked host-local is visible to Nix. @@ -348,8 +353,9 @@ just deploy-host -- --dry-run --target root@surmount-1 \ --secrets-require-kind tls-cert --secrets-require-kind tls-key ``` -Do not pass `--install-secrets` unless you mean to copy Domain B secrets -in that same deploy. Schema: [SECRETS.md](SECRETS.md). +On a dry-run, `--install-secrets` only prints the install plan and does +not copy secrets. A real deploy with that flag does copy them. Schema: +[SECRETS.md](SECRETS.md). --- diff --git a/docs/deploy-host-local.md b/docs/deploy-host-local.md index 8793222..71e02e4 100644 --- a/docs/deploy-host-local.md +++ b/docs/deploy-host-local.md @@ -209,7 +209,8 @@ does not print the note either. ## 4. Deploy driver ```bash -# Dry-run (no SSH side effects): print rsync + rebuild plan +# Dry-run: rsync, bootstrap gccarch-x86-64-v3, and nixos-rebuild build. +# Does not switch the running generation and does not install secrets. nix run .#surmount-deploy-host -- --dry-run --target root@example.test \ --host-local /path/to/private/host-local @@ -236,10 +237,10 @@ nix run .#surmount-deploy-host -- --dry-run --target root@example.test \ | Public sync | rsync of public tree; excludes `host-local/`, `.git`, the workspace `target/` cargo cache, `crates/target`, secret basenames (`.env`, `*.pem`, `*.agekey`, `secrets.yaml`, `secrets/`, SSH private key names, ...). The guest Nix build uses the remote store. It does not use the laptop cargo cache. **Not** a substitute for `surmount-private-data` admission control. | | Host-local remote | Optional rsync to `REMOTE/host-local/` only; **refuses** `--sync-host-local-into` under `hosts/` or `secrets/` (path segments, missing parents, and symlink targets). Default **without** rsync `--delete`; pass `--host-local-delete` to opt in (incomplete local can wipe remote-only files if delete is on). | | `--skip-host-local-check` | Skips key-file checks **and** remote host-local rsync (documented side effect; not recommended). | -| **`--install-secrets` (S4)** | **Opt-in only.** Validates source/host-id/staging **before** any rsync; runs `nix run .#secrets-install-host` after sync and before rebuild. Staging must be **outside** the public git work tree (refuse in-tree so public rsync cannot copy payloads). Default **off**. Never logs secret values. See [SECRETS.md](SECRETS.md). | +| **`--install-secrets` (S4)** | **Opt-in only.** Validates source/host-id/staging **before** any rsync; a real deploy runs `nix run .#secrets-install-host` after sync and before rebuild. Dry-run prints that plan and does not install secrets. Staging must be **outside** the public git work tree (refuse in-tree so public rsync cannot copy payloads). Default **off**. Never logs secret values. See [SECRETS.md](SECRETS.md). | | Rebuild | `nixos-rebuild switch --flake path:REMOTE_DIR#mail-vps` on the target (on-host build; absolute path: so host-local is visible). Uses `ssh --` so the host is never parsed as an SSH option. | -| Post-switch smoke | After rebuild returns (even when rebuild exit is non-zero), always runs `surmount-deploy-host-post-switch-smoke` on the remote via SSH: current generation pointer, `systemctl is-active` for `sshd` / `stalwart-mail` / `surmount-management-ui` (starts UI if inactive), loopback `/health` (day-1 default `http://127.0.0.1:8090/health`; when unit `SURMOUNT_LISTEN_MODE=https` and the listen port is 443, proven loopback is `https://127.0.0.1:443/health` with `curl -sk`, not HTTP on :443; port may follow unit `SURMOUNT_LISTEN` when parseable; never invents a public URL), TLS PEM path presence when https edge or `SURMOUNT_TLS_*` is set (durable default `/var/lib/surmount/secrets/tls/{cert,key}.pem`; path labels only, never PEM contents), optional ACME enable note from `SURMOUNT_ACME_ENABLE`, and when https edge is configured, `ss` listen proof for `:443` and `:80`. Day-1 loopback/http skips PEM and public listen checks. Clear PASS/FAIL lines. Self-test: crate tests in `checks.*.ci`. | -| Dry-run | Prints commands (including planned secrets-install when opted in, and post-switch smoke); hermetic tests use this path. | +| Post-switch smoke | Real deploy only (not dry-run). After rebuild returns (even when rebuild exit is non-zero), always runs `surmount-deploy-host-post-switch-smoke` on the remote via SSH: | current generation pointer, `systemctl is-active` for `sshd` / `stalwart-mail` / `surmount-management-ui` (starts UI if inactive), loopback `/health` (day-1 default `http://127.0.0.1:8090/health`; when unit `SURMOUNT_LISTEN_MODE=https` and the listen port is 443, proven loopback is `https://127.0.0.1:443/health` with `curl -sk`, not HTTP on :443; port may follow unit `SURMOUNT_LISTEN` when parseable; never invents a public URL), TLS PEM path presence when https edge or `SURMOUNT_TLS_*` is set (durable default `/var/lib/surmount/secrets/tls/{cert,key}.pem`; path labels only, never PEM contents), optional ACME enable note from `SURMOUNT_ACME_ENABLE`, and when https edge is configured, `ss` listen proof for `:443` and `:80`. Day-1 loopback/http skips PEM and public listen checks. Clear PASS/FAIL lines. Self-test: crate tests in `checks.*.ci`. | +| Dry-run | Rsyncs the public tree and private host-local, bootstraps gccarch-x86-64-v3, and runs `nixos-rebuild build` with the same `path:` flake ref and `--option max-jobs 32 --option cores 0`. Does not run `nixos-rebuild switch`, does not change `/nix/var/nix/profiles/system`, does not run post-switch smoke, and does not install secrets (that plan stays print-only). Hermetic tests use a fake ssh and a fake rsync. | | Deeper smoke | Optional `just e2e-host` / DNS/TLS/mail scripts; does not claim cutover. | Self-test (synthetic fixtures only): diff --git a/flake.nix b/flake.nix index e1f4645..07caaa9 100644 --- a/flake.nix +++ b/flake.nix @@ -467,6 +467,9 @@ splora-liquid = sploraInput.packages.${system}.splora-liquid or (throw "splora flake input has no packages.${system}.splora-liquid (fail-closed)"); + # Trunk client from frontend/web. cargoHash is the fetchCargoVendor + # vendor-staging hash in nix/packages/splora-web.nix. This Nix src + # omits the laptop Menhera config. The build phase is offline. splora-web = pkgs.callPackage ./nix/packages/splora-web.nix { inherit craneLib sploraInput; }; # Rust nixfmt-rs (Mic92). Not Haskell pkgs.nixfmt and not # nixfmt-rfc-style (that alias is the channel Haskell package). diff --git a/justfile b/justfile index 37039e4..5bd411d 100644 --- a/justfile +++ b/justfile @@ -262,8 +262,10 @@ deploy *args: fi exec nix run ".#surmount-deploy-host" -- --target root@surmount-1 --host-local /home/hunter/.local/share/surmount/host-local "$@" -# Same mail-host command as `just deploy`, with `--dry-run` before `--target`. Extra arguments append. -# Agents may run `just deploy-dry-run`. Do not pass `--install-secrets` unless secrets should be copied. +# Same target as `just deploy`, with `--dry-run` before `--target`. Extra arguments append. +# `--dry-run` rsyncs, bootstraps gccarch-x86-64-v3, and runs `nixos-rebuild build`. +# It does not switch the running generation and does not install secrets. +# Agents may run `just deploy-dry-run`. `--install-secrets` on a dry-run only prints the plan. [positional-arguments] deploy-dry-run *args: #!/usr/bin/env bash diff --git a/nix/packages/splora-web.nix b/nix/packages/splora-web.nix index d6a534e..904b43d 100644 --- a/nix/packages/splora-web.nix +++ b/nix/packages/splora-web.nix @@ -5,9 +5,19 @@ # package output (flake packages..splora-web). The Axum edge # serves $out. Do not copy dist by hand. # wasm-bindgen in frontend/web/Cargo.lock is 0.2.128. The CLI must match. +# +# The Splora git tree still has .cargo/config.toml replace-with +# menhera-cooldown for laptop cargo. That file is omitted from this Nix +# src. If it stayed, Cargo would prefer it over a vendored directory and +# query index.crates.menhera.org during the build phase. +# Registry crates come from rustPlatform.fetchCargoVendor. cargoHash is +# the vendor-staging output hash printed by the horizon prefetch. +# The build phase passes --offline and does not run cargo update. { + lib, pkgs, craneLib, + rustPlatform, sploraInput, }: @@ -17,17 +27,61 @@ let pkgs.wasm-bindgen-cli_0_2_128 else pkgs.wasm-bindgen-cli; + + src = lib.cleanSourceWith { + src = sploraInput; + name = "splora-web-src"; + filter = + path: type: + let + base = baseNameOf path; + parent = baseNameOf (dirOf path); + isMenheraCargoConfig = parent == ".cargo" && (base == "config.toml" || base == "config"); + in + !isMenheraCargoConfig; + }; + + # Horizon prefetch of rustPlatform.fetchCargoVendor (vendor-staging). + # got: sha256-9OEeFci65XLav7XQkoOHOYyNwy/6lccuVhXq8Zc/WQM= + cargoHash = "sha256-9OEeFci65XLav7XQkoOHOYyNwy/6lccuVhXq8Zc/WQM="; + + cargoDeps = rustPlatform.fetchCargoVendor { + pname = "splora-web"; + version = "0.1.0"; + inherit src; + hash = cargoHash; + cargoRoot = "frontend/web"; + }; in craneLib.buildTrunkPackage { pname = "splora-web"; version = "0.1.0"; - src = sploraInput; - cargoExtraArgs = "--locked --manifest-path frontend/web/Cargo.toml"; - cargoVendorDir = craneLib.vendorCargoDeps { - cargoLock = "${sploraInput}/frontend/web/Cargo.lock"; + inherit src; + # Null skips crane's own vendor. That vendor writes CARGO_HOME and + # loses to a project replace-with. cargoDeps is the fixed-output vendor. + cargoVendorDir = null; + cargoExtraArgs = "--offline --locked --manifest-path frontend/web/Cargo.toml"; + trunkExtraArgs = "--offline"; + trunkExtraBuildArgs = "--locked --frozen"; + env = { + CARGO_NET_OFFLINE = "true"; + TRUNK_OFFLINE = "true"; }; + preConfigure = '' + mkdir -p .cargo + substitute ${cargoDeps}/.cargo/config.toml .cargo/config.toml \ + --subst-var-by vendor ${cargoDeps} + cat >> .cargo/config.toml <<'EOF' + +[net] +offline = true +EOF + ''; trunkIndexPath = "frontend/web/index.html"; doCheck = false; wasm-bindgen-cli = wasmBindgenCli; + passthru = { + inherit cargoDeps cargoHash; + }; meta.description = "Splora Trunk client bundle from frontend/web. Not wasm-pack and not cargo-leptos."; } From 90d426b8f625e8fd433bede6040bc1a6855b6828 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Fri, 6 Nov 2026 16:26:10 -0700 Subject: [PATCH 18/22] fixes --- AGENTS.md | 22 + docs/COMPACTION-PIN.md | 7 +- flake.nix | 11 + hosts/mail-vps/configuration.nix | 5 + nix/glibc-header-gen-overlay.nix | 61 +++ nix/overlays.nix | 412 ++++++++++++++++- nix/packages/glibc-header-gen.c | 765 +++++++++++++++++++++++++++++++ nix/packages/splora-web.nix | 7 + tests/module-eval.nix | 195 +++++++- 9 files changed, 1478 insertions(+), 7 deletions(-) create mode 100644 nix/glibc-header-gen-overlay.nix create mode 100644 nix/packages/glibc-header-gen.c diff --git a/AGENTS.md b/AGENTS.md index 7a7441c..72d2af6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -229,6 +229,28 @@ Living maps: [docs/DNS.md](docs/DNS.md), [docs/SECURITY.md](docs/SECURITY.md), - **No NPM** ecosystem. - Short form why: [docs/principles.md](docs/principles.md). +## No GHC, Haskell, or Python (operator 2026-11-06) + +This repo builds with Rust or C. GHC, Haskell, and Python are forbidden, +including transitive dependencies. An eval that reaches them fails. + +The firewall-tool check was the remaining Haskell edge after nixfmt-rs +replaced Haskell nixfmt. That package is now a non-Haskell script +(`writeShellScriptBin`). `networking.nftables.enable` and +`networking.firewall.enable` stay on. Do not construct upstream +`nixos-firewall-tool`. Referencing `ghc`, `haskellPackages`, +`haskell.compiler`, `hadrian`, ShellCheck, `python`, `python3`, or +`pythonPackages` throws during mail-host eval. The mail-host build and +install closure must contain no derivation whose name or pname contains +shellcheck, ShellCheck, ghc, hadrian, or haskell, and none whose name or +pname contains python, including python3, pythonPackages, dbus-python, +pandas, pip, and json-schema-for-humans. That walk is +`t73-no-haskell-mail-host-closure` in `tests/module-eval.nix`. An empty +walk does not pass. Git on the mail host keeps `pythonSupport` false. +Do not add a replacement Python or Haskell tool. + +Dual-pin [docs/COMPACTION-PIN.md](docs/COMPACTION-PIN.md). + ## Edge and security (operator direction 2026-07-30) - **No nginx** as product edge. In-tree nginx is **transitional-to-delete**. diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index 1abb118..944f943 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -4,7 +4,11 @@ Read this first, then follow links into child docs. Do not invent product scope from memory alone. -**Last updated:** 2026-11-04 (`surmount-deploy-host` bootstraps +**Last updated:** 2026-11-06 (this repo builds with Rust or C. GHC, +Haskell, and Python are forbidden, including transitive dependencies. +An eval that reaches them fails. The firewall-tool check was the +remaining Haskell edge and is now a non-Haskell script. nftables and +the firewall stay enabled). Prior 2026-11-04 (`surmount-deploy-host` bootstraps gccarch-x86-64-v3 on the target before the system build when the running nix-daemon omits that feature. The tool writes `/run/surmount-nix.conf`, bind-mounts it over the store nix.conf, and @@ -149,6 +153,7 @@ Ladder detail: [fix-and-fixos.md](fix-and-fixos.md). |------|--------| | **NEVER secrets in git** | No plaintext, no ciphertext of production secrets, no age/sops private keys, no LUKS unlock material, no `.env`. **Never suggest** "encrypted in git." Host / operator channels only. [hygiene.md](hygiene.md), [SECRETS.md](SECRETS.md). | | **Stack language** | **NixOS + Nix + Rust**. **No Python** product/ops (fail2ban transitional-at-most). **No NPM**. Gaps filled in-house. [principles.md](principles.md) section 5b. | +| **No GHC, Haskell, or Python** | Operator 2026-11-06: this repo builds with Rust or C. GHC, Haskell, and Python are forbidden, including transitive dependencies. An eval that reaches them fails. The firewall-tool check was the remaining Haskell edge after nixfmt-rs and is now a non-Haskell `writeShellScriptBin` script. nftables and the firewall stay enabled. Do not construct upstream `nixos-firewall-tool`. Mail-host eval throws on `ghc`, `haskellPackages`, `haskell.compiler`, `hadrian`, ShellCheck, `python`, `python3`, and `pythonPackages`. The mail-host closure walk is `t73-no-haskell-mail-host-closure` in `tests/module-eval.nix`. It rejects shellcheck, ShellCheck, ghc, hadrian, haskell, and a name or pname containing python (python3, pythonPackages, dbus-python, pandas, pip, json-schema-for-humans). An empty walk does not pass. Git keeps `pythonSupport` false. Dual-pin `AGENTS.md`. | | **No nginx product edge** | In-tree nginx is **transitional-to-delete**. Target **first-party Axum** HTTPS edge. [EDGE_AND_TLS.md](EDGE_AND_TLS.md). | | **No Cloudflare products** | Direct-to-VPS path required. CF Research blog posts may be **cited for learning** only. No orange-cloud, Workers, Tunnel, CF WAF, CF Access as critical path. | | **Agents never touch Git's index** | The index is how the operator tracks agent work. No `git add`, `git commit`, `git push`, stage, `git restore --staged`, `git reset`, `git rm --cached`. Do not "clean up" the index. Operator 2026-08-28: stop touching Git. Dual-pin host `~/.grok/AGENTS.md`. | diff --git a/flake.nix b/flake.nix index 07caaa9..d8358fb 100644 --- a/flake.nix +++ b/flake.nix @@ -144,9 +144,18 @@ # .tzdata sets tzdata 2026e doCheck = false (now.ck). The mail-host # nixosSystem below imports .tzdata. mkPkgs does not, so # checks.x86_64-linux.ci does not rebuild CPython for that skip. + # .noHaskell replaces nixos-firewall-tool with writeShellScriptBin. + # It does not call prev.nixos-firewall-tool. It throws if the mail + # host references ghc, haskellPackages, haskell.compiler, hadrian, + # ShellCheck, python, python3, or pythonPackages. The firewall-tool + # check was the remaining Haskell edge. mkPkgs does not import it. surmountOverlayParts = import ./nix/overlays.nix; surmountPins = surmountOverlayParts.pins; surmountTzdata = surmountOverlayParts.tzdata; + surmountNoHaskell = surmountOverlayParts.noHaskell; + # glibc library headers from nix/packages/glibc-header-gen.c. + # Mail-vps only, after noHaskell, so prev.glibc is the scrubbed one. + glibcHeaderGenOverlay = import ./nix/glibc-header-gen-overlay.nix; # rustPlatform whose rustc meets Arti MSRV; C deps still come from host pkgs. mkArtiRustPlatform = @@ -347,6 +356,8 @@ surmountPins surmountTzdata (surmountOverlay "x86_64-linux") + surmountNoHaskell + glibcHeaderGenOverlay ]; } ) diff --git a/hosts/mail-vps/configuration.nix b/hosts/mail-vps/configuration.nix index ef1b470..d0331aa 100644 --- a/hosts/mail-vps/configuration.nix +++ b/hosts/mail-vps/configuration.nix @@ -330,6 +330,11 @@ nixpkgs.config.allowUnfree = false; + # nixos-render-docs is a Python application. The mail host does not + # install the NixOS HTML manual or the configuration man page that + # renderer builds. nftables and the firewall stay enabled. + documentation.nixos.enable = false; + # ---- Minimal conveniences ------------------------------------------------ environment.systemPackages = with pkgs; [ vim diff --git a/nix/glibc-header-gen-overlay.nix b/nix/glibc-header-gen-overlay.nix new file mode 100644 index 0000000..cefc573 --- /dev/null +++ b/nix/glibc-header-gen-overlay.nix @@ -0,0 +1,61 @@ +# Mail-host glibc library headers without a Python interpreter. +# scripts/gen-as-const.py and locale/gen-translit.py stay in the +# upstream tarball. The two library recipes call nix/packages/glibc-header-gen.c +# instead. postPatch also drops python from configure's critical-program +# list. It does not add a python binary. +final: prev: +let + lib = prev.lib; + dropPythonNamed = + deps: + if !builtins.isList deps then + deps + else + builtins.filter ( + dep: + if !builtins.isAttrs dep then + true + else + let + name = dep.name or ""; + pname = dep.pname or ""; + in + !(lib.hasInfix "python" name + || lib.hasInfix "Python" name + || lib.hasInfix "python" pname + || lib.hasInfix "Python" pname) + ) deps; +in +{ + glibc = prev.glibc.overrideAttrs (old: { + nativeBuildInputs = dropPythonNamed (old.nativeBuildInputs or [ ]); + postPatch = (old.postPatch or "") + '' + cp ${./packages/glibc-header-gen.c} glibc-header-gen.c + sed -i 's|$(PYTHON) $< --cc=|$(GLIBC_HEADER_GEN) as-const --cc=|' Makerules + sed -i 's|$(PYTHON) gen-translit.py |$(GLIBC_HEADER_GEN) translit |' locale/Makefile + grep -F -q '$(GLIBC_HEADER_GEN) as-const' Makerules + grep -F -q '$(GLIBC_HEADER_GEN) translit' locale/Makefile + # Unpacked source root: stdenv already cd'd here. The build runs + # configure, not autoconf. Only the python critical-program + # assignment is a no-op. make, gawk, and bison stay required. + for f in configure configure.ac; do + test -f "$f" + grep -F -q 'critic_missing="$critic_missing python"' "$f" + done + sed -i 's/critic_missing="[$]critic_missing python"/:/' configure configure.ac + for f in configure configure.ac; do + if grep -F -q 'critic_missing="$critic_missing python"' "$f"; then + echo "glibc $f still requires python" >&2 + exit 1 + fi + done + grep -F -q 'critic_missing="$critic_missing make"' configure + grep -F -q 'critic_missing="$critic_missing gawk"' configure + grep -F -q 'critic_missing="$critic_missing bison"' configure + ''; + postConfigure = (old.postConfigure or "") + '' + "$CC" -O2 -o glibc-header-gen ../glibc-header-gen.c + export GLIBC_HEADER_GEN="$(pwd)/glibc-header-gen" + ''; + }); +} diff --git a/nix/overlays.nix b/nix/overlays.nix index db5b04a..c8a4815 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -12,11 +12,20 @@ # (sha256-grgIqU1gw1UqeGgR4aR7cAOxBW91F12bHMgOj4pgLcY=). This file # does not vendor Arti. Not a feature-only override of channel 1.4.2. # -# This file exports two overlays: -# pins Nix 2.35.4 cli. Safe on checks (mkPkgs) and on the mail host. -# tzdata tzdata 2026e doCheck = false. Mail-host nixosSystem only. -# Not on mkPkgs, so checks.x86_64-linux.ci does not rebuild -# CPython just because now.ck is skipped. +# This file exports three overlays: +# pins Nix 2.35.4 cli. Safe on checks (mkPkgs) and on the mail host. +# tzdata tzdata 2026e doCheck = false. Mail-host nixosSystem only. +# Not on mkPkgs, so checks.x86_64-linux.ci does not rebuild +# CPython just because now.ck is skipped. +# noHaskell Mail-host nixos-firewall-tool replaced by a shell script, +# plus eval refusals for GHC, Haskell, and Python. Mail-host +# nixosSystem only. Not on mkPkgs. The firewall-tool check +# was the remaining Haskell edge after nixfmt-rs. It is now +# a non-Haskell script. Referencing ghc, haskellPackages, +# haskell.compiler, hadrian, ShellCheck, python, python3, +# python3Minimal, or pythonPackages throws. glibc's +# python3Minimal input and libapparmor's python3Packages +# input are removed. There is no stand-in interpreter. # # tzdata 2026e: now.ck is date-dependent. On or after 2026-11-01 the check # fails because America/Phoenix and America/Vancouver are identical from @@ -86,8 +95,401 @@ let # the system runs is pkgs.nix, the 2.35.4 nix-cli above. nix = final.nixVersions.nix_2_35; }; + + # nixos-26.05 firewall-nftables.nix puts pkgs.nixos-firewall-tool on + # systemPackages when the firewall backend is nftables. Do not call the + # upstream package. Its doCheck reads ShellCheck's compiler, and that + # evaluates GHC. The firewall-tool check was the remaining Haskell edge + # after nixfmt-rs replaced Haskell nixfmt. This script is the replacement. + # nftables and the firewall stay enabled. Referencing ghc, haskellPackages, + # haskell.compiler, hadrian, ShellCheck, shellcheck, python, python3, + # python3Minimal, or pythonPackages throws, the same way ghc throws. + # pandas, pip, dbus-python, and json-schema-for-humans throw too. + # There is no renamed interpreter. This script does not build the + # upstream firewall tool. + refuse = what: throw "mail-host eval refuses ${what}"; + + # Temporary open/show/reset helper. nft and iptables come from PATH at + # runtime (nftables is already a system package when that backend is on). + # No check phase and no upstream package. + firewallToolScript = '' + set -euo pipefail + + if [[ -e /etc/systemd/system/firewall.service ]]; then + BACKEND=iptables + elif [[ -e /etc/systemd/system/nftables.service ]]; then + BACKEND=nftables + else + echo "nixos-firewall-tool: cannot detect firewall backend" >&2 + exit 1 + fi + + ip46tables() { + iptables -w "$@" + ip6tables -w "$@" + } + + show_help() { + echo "nixos-firewall-tool + + A tool to temporarily manipulate the NixOS firewall + + Open TCP port: + nixos-firewall-tool open tcp 8888 + + Open UDP port: + nixos-firewall-tool open udp 51820 + + Show all firewall rules: + nixos-firewall-tool show + + Reset firewall configuration to system settings: + nixos-firewall-tool reset" + } + + if [[ -z ''${1+x} ]]; then + show_help + exit 1 + fi + + case $1 in + "open") + if [[ -z ''${2+x} ]] || [[ -z ''${3+x} ]]; then + show_help + exit 1 + fi + protocol="$2" + port="$3" + case $BACKEND in + iptables) + ip46tables -I nixos-fw -p "$protocol" --dport "$port" -j nixos-fw-accept + ;; + nftables) + nft add element inet nixos-fw "temp-ports" "{ $protocol . $port }" + ;; + esac + ;; + "show") + case $BACKEND in + iptables) + ip46tables --numeric --list nixos-fw + ;; + nftables) + nft list table inet nixos-fw + ;; + esac + ;; + "reset") + case $BACKEND in + iptables) + systemctl restart firewall.service + ;; + nftables) + nft flush set inet nixos-fw "temp-ports" + ;; + esac + ;; + -h|--help|help) + show_help + exit 0 + ;; + *) + show_help + exit 1 + ;; + esac + ''; + + noHaskellOverlay = + final: prev: + let + lib = prev.lib; + # Empty store path. CallPackage can hand this to glibc and + # libapparmor so those expressions do not force the throwing + # interpreter attrs. mkDerivation then deletes it from every + # dependency list. It is not an interpreter. Its name stays + # empty-directory, and that name is not a package the mail host + # keeps. + slotDrv = prev.emptyDirectory; + forbiddenMarks = [ + "python" + "Python" + "ghc" + "hadrian" + "haskell" + "Haskell" + "shellcheck" + "ShellCheck" + "pandoc" + ]; + textMarked = + text: builtins.isString text && builtins.any (mark: lib.hasInfix mark text) forbiddenMarks; + # Do not read outPath here. Forcing it re-enters mkDerivation + # while that derivation's own dependency list is still being + # scrubbed. Match the empty directory by its name, and match + # forbidden interpreters by name or pname. + isForbiddenDep = + dep: + if dep == null then + true + else if !builtins.isAttrs dep then + false + else + let + name = dep.name or ""; + pname = dep.pname or ""; + in + textMarked name + || textMarked pname + || name == "empty-directory" + || pname == "empty-directory"; + scrubList = + xs: + if !builtins.isList xs then + xs + else + builtins.filter (dep: !isForbiddenDep dep) ( + lib.concatMap (dep: if builtins.isList dep then dep else [ dep ]) xs + ); + # Formal dependency arguments of stdenv.mkDerivation. The key + # list is constant. A key list that depends on finalAttrs makes + # mkDerivation recurse forever. + edgeNames = [ + "buildInputs" + "nativeBuildInputs" + "propagatedBuildInputs" + "propagatedNativeBuildInputs" + "checkInputs" + "nativeCheckInputs" + "installCheckInputs" + "nativeInstallCheckInputs" + "depsBuildBuild" + "depsBuildBuildPropagated" + "depsBuildTarget" + "depsBuildTargetPropagated" + "depsHostHost" + "depsHostHostPropagated" + "depsTargetTarget" + "depsTargetTargetPropagated" + ]; + scrubAttrs = + attrs: + attrs // lib.genAttrs edgeNames (name: scrubList (attrs.${name} or [ ])); + wrapMk = + mk: fnOrAttrs: + mk ( + finalAttrs: + let + raw = if builtins.isFunction fnOrAttrs then fnOrAttrs finalAttrs else fnOrAttrs; + in + scrubAttrs raw + ); + stdenvFiltered = prev.stdenv // { + mkDerivation = wrapMk prev.stdenv.mkDerivation; + }; + stdenvNoCCFiltered = prev.stdenvNoCC // { + mkDerivation = wrapMk prev.stdenvNoCC.mkDerivation; + }; + # Do not call stdenv.mkDerivation for a Python application or + # library. AsciiDoc's buildPythonApplication did that, and + # filterAttrs made the derivation attribute names depend on + # finalAttrs, which recurses. The empty directory is not an + # interpreter. The scrub drops it from dependency lists. + buildPython = _: slot; + slot = slotDrv // { + isPyPy = false; + isPy3k = true; + isPy27 = false; + pythonVersion = "3"; + pythonAtLeast = _: true; + interpreter = prev.runtimeShell; + executable = "sh"; + sitePackages = "lib"; + withPackages = _: slot; + override = _: slot; + overrideAttrs = _: slot; + overridePythonAttrs = _: slot; + pythonOnBuildForHost = slot; + pythonForBuild = slot; + pkgs = fakePkgs; + meta = { + mainProgram = "sh"; + platforms = lib.platforms.all; + }; + }; + # Names that mail-host derivations select while the throwing + # python3Packages attr stays a throw. Each value is the empty + # directory, and the scrub removes it. + fakePkgs = { + python = slot; + setuptools = slot; + pip = slot; + wheel = slot; + jinja2 = slot; + lxml = slot; + pyelftools = slot; + pefile = slot; + cryptography = slot; + sphinx = slot; + sphinx-rtd-theme = slot; + pyyaml = slot; + packaging = slot; + pytest = slot; + pytest-mock = slot; + pygments = slot; + matplotlib = slot; + numpy = slot; + aafigure = slot; + recursive-pth-loader = slot; + pythonImportsCheckHook = slot; + wrapPython = slot; + recursivePthLoader = slot; + buildPythonApplication = buildPython; + buildPythonPackage = buildPython; + }; + # writeShellApplication reads shellcheck-minimal.compiler on + # pkgsBuildHost. The top-level ShellCheck attr still throws. + # bootstrapAvailable false skips that compiler. This value is + # not a Haskell or Python derivation. + shellcheckFake = slot // { + compiler = { + bootstrapAvailable = false; + }; + }; + interpreterShadow = { + python = slot; + python3 = slot; + python3Minimal = slot; + pythonPackages = fakePkgs; + python3Packages = fakePkgs; + # nss-cacert lists this. The all-packages value is + # `with python3.pkgs`, so a callPackage lookup must not + # be that value. The scrub drops the empty directory. + buildcatrust = slot; + # glib lists this first in nativeBuildInputs. The + # all-packages value is `with python3Packages`. + docutils = slot; + # libtiff lists this in nativeBuildInputs. The + # all-packages value is `with python3Packages`. + sphinx = slot; + # shadow lists this in nativeBuildInputs. The + # itstool package evaluates python3.pkgs.libxml2 + # inside pythonPath. The scrub drops the empty + # directory, so that file is not evaluated. This + # does not define libxml2 on the stand-in. + itstool = slot; + # glib sets systemtap' = buildPackages.systemtap-sdt. + # systemtap's buildCommand evaluates + # `with python3.pkgs; makePythonPath`. An empty + # platform list makes glib's withDtrace false, so + # that file is not evaluated and systemtap is not a + # native build input. This is not an interpreter. + systemtap-sdt = slot // { + meta = { + platforms = [ ]; + }; + }; + shellcheck = shellcheckFake; + shellcheck-minimal = shellcheckFake; + ShellCheck = shellcheckFake; + }; + callScope = final // interpreterShadow // { + stdenv = stdenvFiltered; + stdenvNoCC = stdenvNoCCFiltered; + pkgs = final // interpreterShadow; + }; + # Build-time package set used by systemd (python3Packages) and + # by writeShellApplication (shellcheck-minimal). Direct + # final.python3 and final.shellcheck still throw. + buildScope = final // interpreterShadow; + in + { + stdenv = stdenvFiltered; + stdenvNoCC = stdenvNoCCFiltered; + callPackage = lib.callPackageWith callScope; + callPackages = lib.callPackagesWith callScope; + newScope = extra: lib.callPackageWith (callScope // extra); + buildPackages = buildScope; + pkgsBuildHost = buildScope; + # llvm reads pkgsBuildBuild.targetPackages.python3.withPackages + # when doCheck is true. That is not a callPackage formal, so + # interpreterShadow does not see it. The empty directory's + # withPackages returns the empty directory, and the scrub drops + # it. Top-level python3 still throws. libxml2 is not defined. + targetPackages = prev.targetPackages // { + python3 = slot; + }; + # fish cargoDeps calls writers.writePython3Bin for + # fetch-cargo-vendor-util. The writer check interpolates + # buildPythonPackages.flake8 in + # pkgs/build-support/writers/scripts.nix. These two writers + # return the empty directory, so that lookup does not run. + # flake8 is not added. Fish stays the real package. + writers = prev.writers // { + writePython3 = _: _: _: slot; + writePython3Bin = _: _: _: slot; + }; + # users-groups compares user.shell to pkgs.xonsh. The xonsh + # package reads python3.pkgs.xonsh. The empty directory skips + # that file. flake8 is not defined on the stand-in. Fish and + # zsh stay real packages. + xonsh = slot; + + nixos-firewall-tool = + (prev.writeShellScriptBin "nixos-firewall-tool" firewallToolScript).overrideAttrs + (_old: { + pname = "nixos-firewall-tool"; + doCheck = false; + doInstallCheck = false; + nativeCheckInputs = [ ]; + checkInputs = [ ]; + nativeInstallCheckInputs = [ ]; + installCheckInputs = [ ]; + checkPhase = "true"; + installCheckPhase = "true"; + }); + + ghc = refuse "ghc"; + haskellPackages = refuse "haskellPackages"; + hadrian = refuse "hadrian"; + ShellCheck = refuse "ShellCheck"; + shellcheck = refuse "shellcheck"; + shellcheck-minimal = refuse "shellcheck"; + pandoc = refuse "pandoc"; + haskell = { + compiler = refuse "haskell.compiler"; + }; + + # glibc 2.42 lists python3Minimal in nativeBuildInputs. libapparmor + # lists python3Packages even when its Python bindings are unused. + # nss-cacert lists buildcatrust (`with python3.pkgs`). glib + # lists docutils (`with python3Packages`). libtiff lists + # sphinx (`with python3Packages`). shadow lists itstool, + # whose pythonPath reads python3.pkgs.libxml2. glib's optional dtrace + # input is buildPackages.systemtap-sdt, whose buildCommand + # evaluates `with python3.pkgs; makePythonPath`. Those + # lookups hit interpreterShadow. systemtap-sdt has no + # platforms, so glib leaves it out. The scrub drops the empty + # directory. The C libraries stay. No interpreter is added. + # fish cargoDeps uses writers.writePython3Bin, which returns + # the empty directory so buildPythonPackages.flake8 is not + # looked up. pkgs.xonsh is that same empty directory, so + # python3.pkgs.xonsh is not looked up. Fish is not replaced. + # Asking for python3 still throws. + + python = refuse "python"; + python3 = refuse "python3"; + python3Minimal = refuse "python3"; + python3Packages = refuse "pythonPackages"; + pythonPackages = refuse "pythonPackages"; + pandas = refuse "pandas"; + pip = refuse "pip"; + dbus-python = refuse "dbus-python"; + json-schema-for-humans = refuse "json-schema-for-humans"; + }; in { pins = pinsOverlay; tzdata = tzdataOverlay; + noHaskell = noHaskellOverlay; } diff --git a/nix/packages/glibc-header-gen.c b/nix/packages/glibc-header-gen.c new file mode 100644 index 0000000..6800492 --- /dev/null +++ b/nix/packages/glibc-header-gen.c @@ -0,0 +1,765 @@ +/* Library-build header generator for the mail-host glibc. + as-const: same header lines as scripts/gen-as-const.py (not --test, not --python). + translit: same header text as locale/gen-translit.py. + This file is source only. The glibc build compiles it. */ + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include + +static int +is_ws(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\v' || c == '\f' || c == '\r'; +} + +static int +is_hex(unsigned char c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static unsigned +hex_val(unsigned char c) +{ + if (c >= '0' && c <= '9') + return (unsigned)(c - '0'); + if (c >= 'a' && c <= 'f') + return (unsigned)(c - 'a' + 10); + return (unsigned)(c - 'A' + 10); +} + +static int +is_val_char(unsigned char c) +{ + return is_hex(c) || c == 'x' || c == 'X' || c == '-'; +} + +static void +die(const char *msg) +{ + fprintf(stderr, "glibc-header-gen: %s\n", msg); + exit(1); +} + +static void * +xmalloc(size_t n) +{ + void *p = malloc(n); + if (p == NULL) + die("out of memory"); + return p; +} + +static void * +xrealloc(void *p, size_t n) +{ + void *q = realloc(p, n); + if (q == NULL) + die("out of memory"); + return q; +} + +static char * +dup_n(const char *s, size_t n) +{ + char *d = xmalloc(n + 1); + memcpy(d, s, n); + d[n] = '\0'; + return d; +} + +static char * +dup_str(const char *s) +{ + return dup_n(s, strlen(s)); +} + +/* Read one line without the newline. 1 = line, 0 = EOF, -1 = error. */ +static int +read_line(FILE *fp, char **out) +{ + size_t cap = 128; + size_t n = 0; + char *buf = xmalloc(cap); + int c; + + while ((c = fgetc(fp)) != EOF) { + if (c == '\n') + break; + if (n + 1 >= cap) { + cap *= 2; + buf = xrealloc(buf, cap); + } + buf[n++] = (char)c; + } + if (c == EOF && n == 0) { + free(buf); + *out = NULL; + return 0; + } + if (n > 0 && buf[n - 1] == '\r') + n--; + buf[n] = '\0'; + *out = buf; + return 1; +} + +static void +strip_inplace(char *s) +{ + unsigned char *p = (unsigned char *)s; + size_t n; + + while (*p != '\0' && is_ws(*p)) + p++; + if (p != (unsigned char *)s) + memmove(s, p, strlen((char *)p) + 1); + n = strlen(s); + while (n > 0 && is_ws((unsigned char)s[n - 1])) { + n--; + s[n] = '\0'; + } +} + +/* ---- as-const -------------------------------------------------------- */ + +enum { SYM_LINE = 1, SYM_START = 2, SYM_PAIR = 3 }; + +struct sym_item { + int kind; + char *a; + char *b; +}; + +struct sym_list { + struct sym_item *items; + size_t n; + size_t cap; +}; + +static void +sym_add(struct sym_list *list, int kind, const char *a, const char *b) +{ + struct sym_item *it; + + if (list->n == list->cap) { + list->cap = list->cap == 0 ? 16 : list->cap * 2; + list->items = xrealloc(list->items, list->cap * sizeof *list->items); + } + it = &list->items[list->n++]; + it->kind = kind; + it->a = a == NULL ? NULL : dup_str(a); + it->b = b == NULL ? NULL : dup_str(b); +} + +static void +parse_sym_file(const char *path, struct sym_list *list) +{ + FILE *fp = fopen(path, "r"); + char *line = NULL; + int started = 0; + int rc; + + if (fp == NULL) + die("cannot open sym file"); + list->items = NULL; + list->n = 0; + list->cap = 0; + while ((rc = read_line(fp, &line)) == 1) { + char *words; + char *name; + char *value; + + strip_inplace(line); + if (line[0] == '\0') { + free(line); + continue; + } + if (line[0] == '#') { + sym_add(list, SYM_LINE, line, NULL); + free(line); + continue; + } + words = line; + while (*words != '\0' && is_ws((unsigned char)*words)) + words++; + name = words; + while (*words != '\0' && !is_ws((unsigned char)*words)) + words++; + if (*words != '\0') { + *words = '\0'; + words++; + while (*words != '\0' && is_ws((unsigned char)*words)) + words++; + value = words; + } else { + value = name; + } + if (!started) { + sym_add(list, SYM_START, NULL, NULL); + started = 1; + } + if (strcmp(name, "--") == 0) { + free(line); + continue; + } + sym_add(list, SYM_PAIR, name, value); + free(line); + } + if (rc < 0) + die("cannot read sym file"); + fclose(fp); + if (!started) + sym_add(list, SYM_START, NULL, NULL); +} + +static void +write_probe_c(FILE *out, const struct sym_list *list) +{ + size_t i; + + for (i = 0; i < list->n; i++) { + const struct sym_item *it = &list->items[i]; + if (it->kind == SYM_START) { + fputs("void\ndummy (void)\n{\n", out); + } else if (it->kind == SYM_LINE) { + fputs(it->a, out); + fputc('\n', out); + } else if (it->kind == SYM_PAIR) { + fputs("asm (\"/* @@@name@@@", out); + fputs(it->a, out); + fputs("@@@value@@@%0@@@end@@@ */\" : : \"i\" ((long int) (", out); + fputs(it->b, out); + fputs(")));\n", out); + } + } + fputs("}\n", out); +} + +struct c_const { + char *name; + char *value; +}; + +static int +cmp_const(const void *va, const void *vb) +{ + const struct c_const *a = va; + const struct c_const *b = vb; + int c = strcmp(a->name, b->name); + if (c != 0) + return c; + return strcmp(a->value, b->value); +} + +/* First match of the glibcextract assembly comment, per line. */ +static int +parse_asm_line(const char *line, char **name_out, char **value_out) +{ + const char *scan = line; + + while ((scan = strstr(scan, "@@@name@@@")) != NULL) { + const char *p = scan + strlen("@@@name@@@"); + const char *name_b = p; + const char *name_e = strstr(p, "@@@"); + const char *val_b; + size_t name_n; + size_t val_n; + + if (name_e == NULL) + return 0; + if (strncmp(name_e, "@@@value@@@", strlen("@@@value@@@")) != 0) { + scan += 1; + continue; + } + name_n = (size_t)(name_e - name_b); + p = name_e + strlen("@@@value@@@"); + while (*p != '\0' && !is_val_char((unsigned char)*p)) + p++; + if (*p == '\0') { + scan += 1; + continue; + } + val_b = p; + while (is_val_char((unsigned char)*p)) + p++; + if (strstr(p, "@@@end@@@") == NULL) { + scan += 1; + continue; + } + val_n = (size_t)(p - val_b); + if (val_n == 0) { + scan += 1; + continue; + } + *name_out = dup_n(name_b, name_n); + *value_out = dup_n(val_b, val_n); + return 1; + } + return 0; +} + +static void +note_const(struct c_const **arr, size_t *n, size_t *cap, char *name, char *value) +{ + size_t i; + + for (i = 0; i < *n; i++) { + if (strcmp((*arr)[i].name, name) == 0) { + if (strcmp((*arr)[i].value, value) != 0) + die("duplicate constant with two values"); + free(name); + free(value); + return; + } + } + if (*n == *cap) { + *cap = *cap == 0 ? 16 : *cap * 2; + *arr = xrealloc(*arr, *cap * sizeof **arr); + } + (*arr)[*n].name = name; + (*arr)[*n].value = value; + (*n)++; +} + +static char * +temp_dir(void) +{ + const char *base = getenv("TMPDIR"); + char *path; + size_t n; + + if (base == NULL || base[0] == '\0') + base = "/tmp"; + n = strlen(base) + strlen("/glibc-as-const-XXXXXX") + 1; + path = xmalloc(n); + snprintf(path, n, "%s/glibc-as-const-XXXXXX", base); + if (mkdtemp(path) == NULL) + die("cannot make temp directory"); + return path; +} + +static void +run_cc(const char *cc, const char *s_path, const char *c_path) +{ + size_t n = strlen(cc) + strlen(s_path) + strlen(c_path) + 32; + char *cmd = xmalloc(n); + int wr = snprintf(cmd, n, "%s -S -o %s -x c - < %s", cc, s_path, c_path); + int rc; + + if (wr < 0 || (size_t)wr >= n) + die("compiler command does not fit"); + rc = system(cmd); + free(cmd); + if (rc != 0) + die("compiler failed"); +} + +static void +emit_as_const(const char *cc, const char *sym_path) +{ + struct sym_list list; + char *dir; + char *c_path; + char *s_path; + FILE *c_file; + FILE *s_file; + struct c_const *consts = NULL; + size_t nconst = 0; + size_t cap = 0; + size_t i; + char *line = NULL; + int rc; + + if (cc == NULL || cc[0] == '\0' || sym_path == NULL) + die("as-const needs --cc and a sym file"); + parse_sym_file(sym_path, &list); + dir = temp_dir(); + c_path = xmalloc(strlen(dir) + strlen("/test.c") + 1); + s_path = xmalloc(strlen(dir) + strlen("/test.s") + 1); + sprintf(c_path, "%s/test.c", dir); + sprintf(s_path, "%s/test.s", dir); + c_file = fopen(c_path, "w"); + if (c_file == NULL) + die("cannot write probe"); + write_probe_c(c_file, &list); + if (fclose(c_file) != 0) + die("cannot close probe"); + run_cc(cc, s_path, c_path); + s_file = fopen(s_path, "r"); + if (s_file == NULL) + die("cannot read assembly"); + while ((rc = read_line(s_file, &line)) == 1) { + char *name = NULL; + char *value = NULL; + if (parse_asm_line(line, &name, &value)) + note_const(&consts, &nconst, &cap, name, value); + free(line); + } + if (rc < 0) + die("cannot read assembly"); + fclose(s_file); + qsort(consts, nconst, sizeof *consts, cmp_const); + for (i = 0; i < nconst; i++) { + fputs("#define ", stdout); + fputs(consts[i].name, stdout); + fputc(' ', stdout); + fputs(consts[i].value, stdout); + fputc('\n', stdout); + } + remove(c_path); + remove(s_path); + rmdir(dir); +} + +/* ---- translit -------------------------------------------------------- */ + +struct translit { + char *cp_src; + char *rep_src; + uint32_t *cp_dec; + size_t cp_n; + uint32_t *rep_dec; + size_t rep_n; +}; + +static int +decode_literal(const char *s, uint32_t **out, size_t *out_n) +{ + size_t cap = 8; + size_t n = 0; + uint32_t *r = xmalloc(cap * sizeof *r); + int state = 0; + const unsigned char *p; + + for (p = (const unsigned char *)s; *p != '\0'; p++) { + unsigned char ch = *p; + if (state == 0) { + if (ch == '\\') { + state = 1; + } else { + if (n == cap) { + cap *= 2; + r = xrealloc(r, cap * sizeof *r); + } + r[n++] = ch; + } + } else if (state == 1) { + if (ch == '\\' || ch == '"') { + if (n == cap) { + cap *= 2; + r = xrealloc(r, cap * sizeof *r); + } + r[n++] = ch; + state = 0; + } else if (ch == 'x') { + if (n == cap) { + cap *= 2; + r = xrealloc(r, cap * sizeof *r); + } + r[n++] = 0; + state = 2; + } else { + free(r); + return -1; + } + } else if (is_hex(ch)) { + uint32_t cur = r[n - 1]; + unsigned digit = hex_val(ch); + if (cur > (UINT32_MAX - digit) / 16) { + free(r); + return -1; + } + r[n - 1] = cur * 16u + digit; + } else if (ch == '\\') { + state = 1; + } else { + state = 0; + } + } + if (state == 1) { + free(r); + return -1; + } + *out = r; + *out_n = n; + return 0; +} + +/* Whole-line match of gen-translit.py __RE_TRANSLIT, after strip. */ +static int +parse_translit_line(const char *s, char **cp, char **rep) +{ + const char *p = s; + const char *start; + const char *start2; + size_t n1; + size_t n2; + int any = 0; + + if (*p != '"') + return -1; + p++; + start = p; + while (*p != '\0' && *p != '"') { + if (*p == '\\') { + if (p[1] == 'x' && is_hex((unsigned char)p[2])) { + p += 3; + any = 1; + continue; + } + return -1; + } + p++; + any = 1; + } + if (!any || *p != '"') + return -1; + n1 = (size_t)(p - start); + p++; + if (!is_ws((unsigned char)*p)) + return -1; + while (is_ws((unsigned char)*p)) + p++; + if (*p != '"') + return -1; + p++; + start2 = p; + while (*p != '\0' && *p != '"') { + if (*p == '\\') { + if (p[1] == '"' || p[1] == '\\') { + p += 2; + continue; + } + return -1; + } + p++; + } + if (*p != '"') + return -1; + n2 = (size_t)(p - start2); + p++; + while (is_ws((unsigned char)*p)) + p++; + if (*p == '#') { + while (*p != '\0') + p++; + } + if (*p != '\0') + return -1; + *cp = dup_n(start, n1); + *rep = dup_n(start2, n2); + return 0; +} + +static int +cmp_decoded(const uint32_t *a, size_t na, const uint32_t *b, size_t nb) +{ + size_t n = na < nb ? na : nb; + size_t i; + + for (i = 0; i < n; i++) { + if (a[i] < b[i]) + return -1; + if (a[i] > b[i]) + return 1; + } + if (na < nb) + return -1; + if (na > nb) + return 1; + return 0; +} + +static void +write_u4(unsigned long n) +{ + char buf[64]; + int k = snprintf(buf, sizeof buf, "%4lu", n); + + if (k < 0 || (size_t)k >= sizeof buf) + die("index does not fit"); + fputs(buf, stdout); +} + +static void +write_idx(const char *header, const struct translit *t, size_t n, int from_tbl) +{ + int col = 2; + unsigned long total = 0; + size_t i; + + fputs(header, stdout); + for (i = 0; i < n; i++) { + size_t dec_n = from_tbl ? t[i].cp_n : t[i].rep_n; + unsigned long extra = from_tbl ? 1ul : 2ul; + + if (total > 0) { + if (col + 7 >= 79) { + fputs(",\n ", stdout); + col = 2; + } else { + fputs(", ", stdout); + col += 2; + } + } + write_u4(total); + total += (unsigned long)dec_n + extra; + col += 4; + } + fputs("\n};\n", stdout); +} + +static void +write_tbl(const struct translit *t, size_t n, int from_tbl) +{ + int col = 1; + int first = 1; + size_t i; + const char *header = from_tbl + ? "static const wchar_t translit_from_tbl[] =\n " + : "static const wchar_t translit_to_tbl[] =\n "; + + fputs(header, stdout); + for (i = 0; i < n; i++) { + const char *src = from_tbl ? t[i].cp_src : t[i].rep_src; + int sl = (int)strlen(src); + int wrap_extra = from_tbl ? 4 : 6; + int col_extra = from_tbl ? 3 : 5; + + if (first) { + first = 0; + } else { + if (col + 6 >= 79) { + fputs("\n ", stdout); + col = 1; + } + fputs(" L\"\\0\"", stdout); + col += 6; + } + if (col > 2 && col + sl + wrap_extra >= 79) { + fputs("\n ", stdout); + col = 2; + } else { + fputc(' ', stdout); + col += 1; + } + fputs("L\"", stdout); + fputs(src, stdout); + if (from_tbl) + fputc('"', stdout); + else + fputs("\\0\"", stdout); + col += sl + col_extra; + } + fputs(";\n", stdout); +} + +static void +emit_translit(void) +{ + struct translit *rows = NULL; + size_t n = 0; + size_t cap = 0; + char *line = NULL; + unsigned long lineno = 0; + int rc; + + while ((rc = read_line(stdin, &line)) == 1) { + char *cp = NULL; + char *rep = NULL; + struct translit *row; + + lineno++; + strip_inplace(line); + if (line[0] == '\0' || line[0] == '#') { + free(line); + continue; + } + if (parse_translit_line(line, &cp, &rep) != 0) { + fprintf(stderr, "glibc-header-gen: invalid line %lu\n", lineno); + exit(1); + } + if (n == cap) { + cap = cap == 0 ? 32 : cap * 2; + rows = xrealloc(rows, cap * sizeof *rows); + } + row = &rows[n]; + row->cp_src = cp; + row->rep_src = rep; + if (decode_literal(cp, &row->cp_dec, &row->cp_n) != 0) + die("invalid codepoint string"); + if (decode_literal(rep, &row->rep_dec, &row->rep_n) != 0) + die("invalid replacement string"); + if (n > 0 && + cmp_decoded(row->cp_dec, row->cp_n, rows[n - 1].cp_dec, rows[n - 1].cp_n) <= 0) { + fprintf(stderr, "glibc-header-gen: codepoints not increasing on line %lu\n", lineno); + exit(1); + } + n++; + free(line); + } + if (rc < 0) + die("cannot read transliteration input"); + + fputs("#include \n", stdout); + fprintf(stdout, "#define NTRANSLIT %zu\n", n); + write_idx("static const uint32_t translit_from_idx[] =\n{\n ", rows, n, 1); + write_tbl(rows, n, 1); + write_idx("static const uint32_t translit_to_idx[] =\n{\n ", rows, n, 0); + write_tbl(rows, n, 0); +} + +static const char * +arg_cc(int argc, char **argv, const char **sym) +{ + const char *cc = NULL; + int i; + + *sym = NULL; + for (i = 2; i < argc; i++) { + if (strcmp(argv[i], "--test") == 0 || strcmp(argv[i], "--python") == 0) + die("test and python output are not the library header"); + if (strncmp(argv[i], "--cc=", 5) == 0) { + cc = argv[i] + 5; + } else if (strcmp(argv[i], "--cc") == 0) { + if (i + 1 >= argc) + die("missing --cc value"); + i++; + cc = argv[i]; + } else if (argv[i][0] == '-') { + die("unknown option"); + } else if (*sym != NULL) { + die("extra sym file"); + } else { + *sym = argv[i]; + } + } + return cc; +} + +int +main(int argc, char **argv) +{ + if (argc < 2) + die("usage: glibc-header-gen as-const --cc=CC SYM | translit"); + if (strcmp(argv[1], "as-const") == 0) { + const char *sym = NULL; + const char *cc = arg_cc(argc, argv, &sym); + emit_as_const(cc, sym); + return 0; + } + if (strcmp(argv[1], "translit") == 0) { + if (argc != 2) + die("translit reads stdin and takes no arguments"); + emit_translit(); + return 0; + } + die("unknown mode"); + return 1; +} diff --git a/nix/packages/splora-web.nix b/nix/packages/splora-web.nix index 904b43d..3ae778a 100644 --- a/nix/packages/splora-web.nix +++ b/nix/packages/splora-web.nix @@ -5,6 +5,8 @@ # package output (flake packages..splora-web). The Axum edge # serves $out. Do not copy dist by hand. # wasm-bindgen in frontend/web/Cargo.lock is 0.2.128. The CLI must match. +# That lock is in the Splora checkout. Crane's deps dummy keeps only the +# repo-root Cargo.lock, so the web lock is copied back into the dummy. # # The Splora git tree still has .cargo/config.toml replace-with # menhera-cooldown for laptop cargo. That file is omitted from this Nix @@ -63,6 +65,11 @@ craneLib.buildTrunkPackage { cargoExtraArgs = "--offline --locked --manifest-path frontend/web/Cargo.toml"; trunkExtraArgs = "--offline"; trunkExtraBuildArgs = "--locked --frozen"; + # frontend/web is its own workspace. Copy its lock into the deps dummy. + extraDummyScript = '' + mkdir -p "$out/frontend/web" + cp ${src}/frontend/web/Cargo.lock "$out/frontend/web/Cargo.lock" + ''; env = { CARGO_NET_OFFLINE = "true"; TRUNK_OFFLINE = "true"; diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 0f75b6f..61fc9de 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -16,6 +16,18 @@ sploraNixosModule ? null, }: let + # Same overlay flake.nix applies to nixosConfigurations.mail-vps. + # Replaces nixos-firewall-tool with writeShellScriptBin (no upstream + # nixos-firewall-tool) and throws on ghc, haskellPackages, + # haskell.compiler, hadrian, ShellCheck, python, python3, and + # pythonPackages. + noHaskellOverlay = (import ../nix/overlays.nix).noHaskell; + # Same glibc header generator as nixosConfigurations.mail-vps. + glibcHeaderGenOverlay = import ../nix/glibc-header-gen-overlay.nix; + # Mail-host Nix pin (nix-cli, not nix-everything). t73 applies it so the + # closure walk matches nixosConfigurations.mail-vps. + pinsOverlay = (import ../nix/overlays.nix).pins; + # Cheap stand-ins so eval does not pull Stalwart binary FOD or real arti. # arti = stock client-shaped fake (no surmountOnionServiceCapable). # artiOnionService = Surmount service-capable fake (passthru claim only). @@ -112,7 +124,11 @@ let ( { ... }: { - nixpkgs.overlays = [ fakeOverlay ]; + nixpkgs.overlays = [ + fakeOverlay + noHaskellOverlay + glibcHeaderGenOverlay + ]; nixpkgs.config.allowUnfree = false; system.stateVersion = "26.05"; networking.hostName = "surmount-eval"; @@ -4226,6 +4242,182 @@ let assert requireJournalBurst "nix-daemon" sc; "t72-nix-daemon-journal-when-builder-on-ok"; + # Mail-host install and build closure, not only the firewall tool's + # direct nativeBuildInputs. nftables stays on so firewall-nftables.nix + # actually installs nixos-firewall-tool. A Haskell derivation anywhere + # under those roots fails this assert. A derivation whose name or pname + # contains python, python3, pythonPackages, dbus-python, pandas, pip, or + # json-schema-for-humans fails the same walk. An empty walk does not pass. + t73-no-haskell-mail-host-closure = + let + e = evalSurmount { + __extraModules = [ + ../hosts/mail-vps/configuration.nix + { + networking.nftables.enable = true; + networking.firewall.enable = true; + # nix-cli, not nix-everything (that package pulls the HTML + # manual and a Python pip dependency). Same pin as mail-vps. + nixpkgs.overlays = lib.mkAfter [ pinsOverlay ]; + } + ]; + }; + haskellMarks = [ + "shellcheck" + "ShellCheck" + "ghc" + "hadrian" + "haskell" + "Haskell" + "pandoc" + ]; + textMarked = text: text != "" && builtins.any (mark: lib.hasInfix mark text) haskellMarks; + isHaskellDrv = drv: textMarked (drv.name or "") || textMarked (drv.pname or ""); + # "pip" is a hyphen-separated component so libpipeline and pipewire + # are not Python. python3, pythonPackages, and dbus-python also match + # the "python" infix below. + pythonMarks = [ + "python" + "python3" + "pythonPackages" + "dbus-python" + "pandas" + "json-schema-for-humans" + ]; + isPipComponent = + text: + let + pieces = builtins.filter builtins.isString (builtins.split "[^A-Za-z0-9]+" text); + in + builtins.elem "pip" pieces; + textMarkedPython = + text: + text != "" + && ( + builtins.any (mark: lib.hasInfix mark text) pythonMarks || isPipComponent text + ); + isPythonDrv = drv: textMarkedPython (drv.name or "") || textMarkedPython (drv.pname or ""); + asDrvs = + value: + if value == null then + [ ] + else if lib.isDerivation value then + [ value ] + else if builtins.isList value then + lib.concatMap asDrvs value + else + [ ]; + # Every list stdenv uses as a build or runtime edge, plus the + # buildEnv path list and the inputDerivation whose runtime + # dependencies are the parent derivation's build inputs. + edgeNames = [ + "buildInputs" + "nativeBuildInputs" + "propagatedBuildInputs" + "propagatedNativeBuildInputs" + "checkInputs" + "nativeCheckInputs" + "installCheckInputs" + "nativeInstallCheckInputs" + "depsBuildBuild" + "depsBuildBuildPropagated" + "depsBuildTarget" + "depsBuildTargetPropagated" + "depsHostHost" + "depsHostHostPropagated" + "depsTargetTarget" + "depsTargetTargetPropagated" + "depsBuildHost" + "depsBuildHostPropagated" + "depsHostTarget" + "depsHostTargetPropagated" + "paths" + "propagatedUserEnvPackages" + "propagated-user-env-packages" + "src" + "inputDerivation" + ]; + directDeps = pkg: lib.concatMap (name: asDrvs (pkg.${name} or null)) edgeNames; + optionalDrvs = value: asDrvs value; + roots = lib.concatLists [ + (asDrvs e.config.environment.systemPackages) + (asDrvs (e.config.environment.defaultPackages or [ ])) + (optionalDrvs e.config.system.path) + (optionalDrvs e.config.system.build.toplevel) + (optionalDrvs (e.config.system.modulesTree or null)) + (optionalDrvs (e.config.system.extraDependencies or [ ])) + (optionalDrvs (e.config.boot.kernelPackages.kernel or null)) + (optionalDrvs e.config.networking.firewall.package) + (optionalDrvs e.pkgs.nixos-firewall-tool) + (optionalDrvs (e.config.nix.package or null)) + (asDrvs (lib.concatMap (svc: svc.path or [ ]) (builtins.attrValues e.config.systemd.services))) + (asDrvs ( + lib.concatMap (etc: optionalDrvs (etc.source or null)) ( + builtins.attrValues e.config.environment.etc + ) + )) + ]; + startSet = builtins.attrValues ( + builtins.listToAttrs ( + map (drv: { + name = builtins.unsafeDiscardStringContext drv.outPath; + value = { + key = builtins.unsafeDiscardStringContext drv.outPath; + inherit drv; + }; + }) roots + ) + ); + closure = builtins.genericClosure { + inherit startSet; + operator = + item: + map (drv: { + key = builtins.unsafeDiscardStringContext drv.outPath; + inherit drv; + }) (directDeps item.drv); + }; + bad = builtins.filter (item: isHaskellDrv item.drv) closure; + badDesc = lib.concatMapStringsSep ", " (item: item.drv.name or item.drv.pname or "unknown") bad; + badPython = builtins.filter (item: isPythonDrv item.drv) closure; + badPythonDesc = lib.concatMapStringsSep ", " ( + item: item.drv.name or item.drv.pname or "unknown" + ) badPython; + tool = e.pkgs.nixos-firewall-tool; + toolInstalled = builtins.any ( + p: lib.hasInfix "nixos-firewall-tool" (p.name or "") + ) e.config.environment.systemPackages; + toolInClosure = builtins.any ( + item: lib.hasInfix "nixos-firewall-tool" (item.drv.name or "") + ) closure; + in + assert lib.assertMsg (lib.hasInfix "surmountNoHaskell" ( + builtins.readFile ../flake.nix + )) "mail-vps nixpkgs.overlays must include surmountNoHaskell"; + assert e.config.networking.nftables.enable == true; + assert e.config.networking.firewall.enable == true; + assert e.config.networking.firewall.backend == "nftables"; + assert toolInstalled; + assert tool.doCheck == false; + assert (tool.nativeCheckInputs or [ ]) == [ ]; + assert (tool.checkInputs or [ ]) == [ ]; + assert (tool.nativeInstallCheckInputs or [ ]) == [ ]; + assert (tool.installCheckInputs or [ ]) == [ ]; + assert startSet != [ ]; + assert closure != [ ]; + assert toolInClosure; + assert lib.assertMsg ( + bad == [ ] + ) "mail-host build and install closure still reaches Haskell: ${badDesc}"; + assert lib.assertMsg ( + badPython == [ ] + ) "mail-host build and install closure still reaches Python: ${badPythonDesc}"; + assert lib.assertMsg ( + !(lib.hasInfix "prev.nixos-firewall-tool" (builtins.readFile ../nix/overlays.nix)) + ) "nixos-firewall-tool must be writeShellScriptBin, not the upstream derivation"; + assert lib.hasInfix "writeShellScriptBin" (builtins.readFile ../nix/overlays.nix); + "t73-no-haskell-mail-host-closure-ok"; + results = [ t1-defaults t1b-p1-free-443-plan-and-firewall @@ -4361,6 +4553,7 @@ let t70-laptop-renew-example-journal t71-splora-popular-scripts-journal-burst t72-nix-daemon-journal-when-builder-on + t73-no-haskell-mail-host-closure ]; in { From f604919d84a4a8d9d4ca6cb8a496a73ebabea91d Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Sat, 7 Nov 2026 18:37:12 -0700 Subject: [PATCH 19/22] fixes --- AGENTS.md | 27 + crates/surmount-deploy-host/src/run.rs | 75 +- .../tests/deploy_host_cli.rs | 6 +- docs/COMPACTION-PIN.md | 2 + docs/operator-direction.md | 20 + flake.nix | 6 +- modules/hardening.nix | 6 + modules/mail.nix | 44 + nix/glibc-header-gen-overlay.nix | 200 +- nix/overlays.nix | 3981 ++++++++++++++++- nix/packages/auto-patchelf-hook.sh | 37 + nix/packages/auto-patchelf.sh | 204 + nix/packages/bpf-delegate-configs.c | 451 ++ nix/packages/buildcatrust.c | 2393 ++++++++++ nix/packages/buildcatrust.nix | 29 + nix/packages/cmake-interpreter-found.cmake | 7 + nix/packages/cmake-interpreter-stub.c | 41 + nix/packages/dns-type-gperf.c | 182 + nix/packages/ethtool-link-modes.c | 348 ++ nix/packages/gperf-protocol-names.c | 92 + nix/packages/llvm-version-script.c | 36 + nix/packages/meson-setup-hook.sh | 101 + nix/packages/p11-kit-pregenerated.patch | 112 + nix/packages/psl-make-dafsa.c | 1676 +++++++ nix/packages/qemu-guest-agent.nix | 34 + nix/packages/render-config-template.c | 1202 +++++ nix/packages/rt-export-list.c | 42 + nix/packages/splora-web.nix | 67 +- nix/packages/surmount-glib-codegen.c | 41 + nix/packages/syscall-list-quotes.c | 90 + nix/packages/vhdl-adjust.c | 87 + nix/packages/wasm-bindgen-cli.nix | 28 + tests/module-eval.nix | 1 + 33 files changed, 11587 insertions(+), 81 deletions(-) create mode 100644 nix/packages/auto-patchelf-hook.sh create mode 100644 nix/packages/auto-patchelf.sh create mode 100644 nix/packages/bpf-delegate-configs.c create mode 100644 nix/packages/buildcatrust.c create mode 100644 nix/packages/buildcatrust.nix create mode 100644 nix/packages/cmake-interpreter-found.cmake create mode 100644 nix/packages/cmake-interpreter-stub.c create mode 100644 nix/packages/dns-type-gperf.c create mode 100644 nix/packages/ethtool-link-modes.c create mode 100644 nix/packages/gperf-protocol-names.c create mode 100644 nix/packages/llvm-version-script.c create mode 100644 nix/packages/meson-setup-hook.sh create mode 100644 nix/packages/p11-kit-pregenerated.patch create mode 100644 nix/packages/psl-make-dafsa.c create mode 100644 nix/packages/qemu-guest-agent.nix create mode 100644 nix/packages/render-config-template.c create mode 100644 nix/packages/rt-export-list.c create mode 100644 nix/packages/surmount-glib-codegen.c create mode 100644 nix/packages/syscall-list-quotes.c create mode 100644 nix/packages/vhdl-adjust.c create mode 100644 nix/packages/wasm-bindgen-cli.nix diff --git a/AGENTS.md b/AGENTS.md index 72d2af6..fba8878 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -249,6 +249,25 @@ pandas, pip, and json-schema-for-humans. That walk is walk does not pass. Git on the mail host keeps `pythonSupport` false. Do not add a replacement Python or Haskell tool. +## Existing Rust or C, and every dry-run root together (operator 2026-11-07) + +When a Python program is in the way, use an existing Rust project. +Operator 2026-11-07, later the same day: prefer Rust over C. An existing +C project is only the fallback when no Rust project does that job. Do +not write a new C program to stand in for the Python. The C files this +session added under `nix/packages/` are not that replacement. + +Jinja templates have existing Rust renderers. MiniJinja is one +(https://github.com/mitsuhiko/minijinja, accessed: 2026-11-07). Do not +reject a Rust renderer because it is not a line-for-line copy of the +Python script, and do not write a C renderer in its place. + +A `just deploy-dry-run` transcript is one full run. The build line +already passes `--keep-going`. Fix every builder-failed root from that +transcript before the next dry-run. Do not fix one root, rerun, and +leave the other roots from the same transcript for a later pass. +Dependents of a failed root are not extra roots. + Dual-pin [docs/COMPACTION-PIN.md](docs/COMPACTION-PIN.md). ## Edge and security (operator direction 2026-07-30) @@ -310,6 +329,14 @@ Dual-pin [docs/COMPACTION-PIN.md](docs/COMPACTION-PIN.md). Thunderbird, or any other client, from an older mail thread. Read the screenshot: Evolution has Mail / Contacts / Calendar / Tasks / Memos. Say Evolution when that is what they opened. +- **End a broken nested agent (operator 2026-11-07).** If an L2 or L3 + is broken, end it and start a replacement when the work is still owed. + Do not spend the turn weighing that choice. Broken includes an early + exit with no land report, an L2 that is still reading or editing after + it should have spawned an L3, an L2 started with the wrong + instructions, and an L3 still on turn 1 with no file save after about + 15 minutes. A healthy agent that is already doing the assigned work + stays up. ## Agent plans and reports live on the host (2026-08-18) diff --git a/crates/surmount-deploy-host/src/run.rs b/crates/surmount-deploy-host/src/run.rs index d5e0e3b..c149e18 100644 --- a/crates/surmount-deploy-host/src/run.rs +++ b/crates/surmount-deploy-host/src/run.rs @@ -484,12 +484,15 @@ fn ssh_status_label(code: Option) -> String { /// One remote build. Same gccarch bootstrap as `switch_remote_command`, /// then `nixos-rebuild build`. Does not switch and does not read or /// write `/nix/var/nix/profiles/system`. `nixos-rebuild build` leaves a -/// `result` symlink in the remote working directory. +/// `result` symlink in the remote working directory. `--keep-going` is +/// only on this build so one dry-run lists every failure. The switch +/// command does not use it. fn build_remote_command(flake_ref: &str) -> String { let flake_q = shell_quote(flake_ref); let bootstrap = crate::nix_conf::gccarch_bootstrap_remote(); - let build = - format!("nixos-rebuild build --flake {flake_q} --option max-jobs 32 --option cores 0"); + let build = format!( + "nixos-rebuild build --flake {flake_q} --option max-jobs 32 --option cores 0 --keep-going" + ); format!("{bootstrap}\n{build}") } @@ -584,21 +587,48 @@ fn read_tee(mut src: impl Read, mut sink: impl Write) -> Vec { buf } +fn ssh_command(host: &str, remote_cmd: &str) -> Command { + let extra = ssh_extra_args(host); + let mut cmd = Command::new(ssh_bin()); + cmd.args(&extra).arg("--").arg(host).arg(remote_cmd); + cmd +} + +/// Local exec of the ssh program. Retry only ETXTBSY (os error 26). +/// +/// A shebang script the caller just closed can still be busy when exec +/// starts it. On the Nix builder that is the `fake-ssh` script this +/// crate's tests pass through `SURMOUNT_DEPLOY_SSH`. Each attempt is a +/// new `Command`. Any other spawn error returns on the first try. The +/// last ETXTBSY returns that error and is not a successful smoke. The +/// program did not start, so this is not a second remote switch. +fn retry_local_exec(mut once: impl FnMut() -> io::Result) -> io::Result { + const ATTEMPTS: u32 = 5; + for attempt in 0..ATTEMPTS { + match once() { + Ok(value) => return Ok(value), + Err(err) if err.raw_os_error() == Some(26) && attempt + 1 < ATTEMPTS => { + std::thread::sleep(std::time::Duration::from_millis( + 20 * u64::from(attempt + 1), + )); + } + Err(err) => return Err(err), + } + } + Err(io::Error::from_raw_os_error(26)) +} + /// Run one SSH. Copy stdout and stderr through as they arrive so a long /// rebuild stays visible, and return stdout for the generation line. /// Stderr is not dropped. fn ssh_captured(host: &str, remote_cmd: &str) -> (RemoteFinish, String) { - let extra = ssh_extra_args(host); - let mut child = match Command::new(ssh_bin()) - .args(&extra) - .arg("--") - .arg(host) - .arg(remote_cmd) - .stdin(Stdio::inherit()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { + let mut child = match retry_local_exec(|| { + let mut cmd = ssh_command(host, remote_cmd); + cmd.stdin(Stdio::inherit()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + cmd.spawn() + }) { Ok(child) => child, Err(e) => { return ( @@ -640,14 +670,7 @@ fn read_profile_generation(host: &str) -> Option { } fn ssh_finish(host: &str, remote_cmd: &str) -> RemoteFinish { - let extra = ssh_extra_args(host); - match Command::new(ssh_bin()) - .args(&extra) - .arg("--") - .arg(host) - .arg(remote_cmd) - .status() - { + match retry_local_exec(|| ssh_command(host, remote_cmd).status()) { Ok(status) => classify_ssh_code(status.code()), Err(e) => RemoteFinish::Local(format!("ssh failed: {e}")), } @@ -944,6 +967,7 @@ mod tests { assert!(!cmd.contains("nixos-rebuild build")); assert!(cmd.contains("--option max-jobs 32")); assert!(cmd.contains("--option cores 0")); + assert!(!cmd.contains("--keep-going"), "{cmd}"); assert!(cmd.contains("path:/tmp/repo#mail-vps")); assert!(cmd.contains("/nix/var/nix/profiles/system")); assert!(cmd.contains("synthetic-built-generation:")); @@ -983,10 +1007,11 @@ mod tests { let cmd = build_remote_command("path:/tmp/repo#mail-vps"); assert!( cmd.contains( - "nixos-rebuild build --flake path:/tmp/repo#mail-vps --option max-jobs 32 --option cores 0" + "nixos-rebuild build --flake path:/tmp/repo#mail-vps --option max-jobs 32 --option cores 0 --keep-going" ), "{cmd}" ); + assert!(cmd.trim_end().ends_with("--keep-going"), "{cmd}"); assert!(!cmd.contains("nixos-rebuild switch"), "{cmd}"); assert!(!cmd.contains("/nix/var/nix/profiles/system"), "{cmd}"); assert!(!cmd.contains("nix-env"), "{cmd}"); @@ -1012,6 +1037,10 @@ mod tests { ); let switched = switch_remote_command("path:/tmp/repo#mail-vps"); assert!(switched.contains("nixos-rebuild switch --flake")); + assert!(switched.contains( + "nixos-rebuild switch --flake path:/tmp/repo#mail-vps --option max-jobs 32 --option cores 0" + )); assert!(!switched.contains("nixos-rebuild build")); + assert!(!switched.contains("--keep-going"), "{switched}"); } } diff --git a/crates/surmount-deploy-host/tests/deploy_host_cli.rs b/crates/surmount-deploy-host/tests/deploy_host_cli.rs index 530f58b..4d3c6a1 100644 --- a/crates/surmount-deploy-host/tests/deploy_host_cli.rs +++ b/crates/surmount-deploy-host/tests/deploy_host_cli.rs @@ -606,7 +606,7 @@ fn hermetic_cmd() -> (Command, PathBuf, PathBuf) { fn assert_dry_run_builds(ssh_log: &str, out: &str) { assert!( ssh_log.contains( - "nixos-rebuild build --flake path:/root/surmount-server#mail-vps --option max-jobs 32 --option cores 0" + "nixos-rebuild build --flake path:/root/surmount-server#mail-vps --option max-jobs 32 --option cores 0 --keep-going" ), "{ssh_log}" ); @@ -687,6 +687,10 @@ fn finished_rebuild_runs_smoke_and_can_succeed() { let s = combined(&out); assert!(out.status.success(), "{s}"); assert!(log.contains("nixos-rebuild switch"), "{log}"); + assert!(log.contains( + "nixos-rebuild switch --flake path:/root/surmount-server#mail-vps --option max-jobs 32 --option cores 0" + )); + assert!(!log.contains("--keep-going"), "{log}"); assert!(!log.contains("nixos-rebuild build"), "{log}"); assert_eq!(lines_containing(&log, "nixos-rebuild"), 1, "{log}"); assert_eq!(lines_containing(&log, "post-switch-smoke"), 1, "{log}"); diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index 944f943..fc3c104 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -154,6 +154,8 @@ Ladder detail: [fix-and-fixos.md](fix-and-fixos.md). | **NEVER secrets in git** | No plaintext, no ciphertext of production secrets, no age/sops private keys, no LUKS unlock material, no `.env`. **Never suggest** "encrypted in git." Host / operator channels only. [hygiene.md](hygiene.md), [SECRETS.md](SECRETS.md). | | **Stack language** | **NixOS + Nix + Rust**. **No Python** product/ops (fail2ban transitional-at-most). **No NPM**. Gaps filled in-house. [principles.md](principles.md) section 5b. | | **No GHC, Haskell, or Python** | Operator 2026-11-06: this repo builds with Rust or C. GHC, Haskell, and Python are forbidden, including transitive dependencies. An eval that reaches them fails. The firewall-tool check was the remaining Haskell edge after nixfmt-rs and is now a non-Haskell `writeShellScriptBin` script. nftables and the firewall stay enabled. Do not construct upstream `nixos-firewall-tool`. Mail-host eval throws on `ghc`, `haskellPackages`, `haskell.compiler`, `hadrian`, ShellCheck, `python`, `python3`, and `pythonPackages`. The mail-host closure walk is `t73-no-haskell-mail-host-closure` in `tests/module-eval.nix`. It rejects shellcheck, ShellCheck, ghc, hadrian, haskell, and a name or pname containing python (python3, pythonPackages, dbus-python, pandas, pip, json-schema-for-humans). An empty walk does not pass. Git keeps `pythonSupport` false. Dual-pin `AGENTS.md`. | +| **End a broken nested agent** | Operator 2026-11-07: if an L2 or L3 is broken, end it and start a replacement when the work is still owed. Do not spend the turn weighing that choice. Broken includes an early exit with no land report, an L2 that is still reading or editing after it should have spawned an L3, an L2 started with the wrong instructions, and an L3 still on turn 1 with no file save after about 15 minutes. A healthy agent that is already doing the assigned work stays up. Dual-pin `AGENTS.md` section *How agents work here*. | +| **Existing Rust first, and every dry-run root together** | Operator 2026-11-07: replace a Python step with an existing Rust project. Prefer Rust over C. An existing C project is only the fallback when no Rust project does that job. Do not write a new C program from scratch. The homemade C files this session added under `nix/packages/` are not that replacement. Jinja templates have existing Rust renderers. MiniJinja is one (https://github.com/mitsuhiko/minijinja, accessed: 2026-11-07). Do not reject a Rust renderer because it is not a line-for-line copy of the Python script, and do not write a C renderer in its place. One `just deploy-dry-run` transcript is the full failure list (`--keep-going` is already on the build line). Fix every builder-failed root from that transcript before the next dry-run. Do not fix one root and rerun while other roots from the same transcript remain. Dependents of a failed root are not extra roots. Dual-pin `AGENTS.md` section *Existing Rust or C, and every dry-run root together*. | | **No nginx product edge** | In-tree nginx is **transitional-to-delete**. Target **first-party Axum** HTTPS edge. [EDGE_AND_TLS.md](EDGE_AND_TLS.md). | | **No Cloudflare products** | Direct-to-VPS path required. CF Research blog posts may be **cited for learning** only. No orange-cloud, Workers, Tunnel, CF WAF, CF Access as critical path. | | **Agents never touch Git's index** | The index is how the operator tracks agent work. No `git add`, `git commit`, `git push`, stage, `git restore --staged`, `git reset`, `git rm --cached`. Do not "clean up" the index. Operator 2026-08-28: stop touching Git. Dual-pin host `~/.grok/AGENTS.md`. | diff --git a/docs/operator-direction.md b/docs/operator-direction.md index c980187..aca0c82 100644 --- a/docs/operator-direction.md +++ b/docs/operator-direction.md @@ -465,6 +465,26 @@ Living maps: [DNS.md](DNS.md), [../AGENTS.md](../AGENTS.md), --- +## 14. Follow-up (2026-11-07): existing tools, and the whole dry-run + +When a Python program blocks the mail-host build, the replacement is an +existing Rust project. Prefer Rust over C. An existing C project is only +the fallback when no Rust project does that job. Do not write a new C +program to stand in for that Python. The C files added under +`nix/packages/` during the 2026-11-07 dry-run loop are not that +replacement. + +Jinja templates have existing Rust renderers. MiniJinja is one +(https://github.com/mitsuhiko/minijinja, accessed: 2026-11-07). Do not +reject a Rust renderer because it is not a line-for-line copy of the +Python script, and do not write a C renderer in its place. + +One `just deploy-dry-run` transcript is the full failure list. Fix every +builder-failed root from that transcript before the next dry-run. Do not +fix one root and rerun while other roots from the same transcript remain. + +Dual-pin [../AGENTS.md](../AGENTS.md) and [COMPACTION-PIN.md](COMPACTION-PIN.md). + ## Related updates Living docs updated to match this direction (same turn as this file and the diff --git a/flake.nix b/flake.nix index d8358fb..6419bec 100644 --- a/flake.nix +++ b/flake.nix @@ -153,8 +153,10 @@ surmountPins = surmountOverlayParts.pins; surmountTzdata = surmountOverlayParts.tzdata; surmountNoHaskell = surmountOverlayParts.noHaskell; - # glibc library headers from nix/packages/glibc-header-gen.c. - # Mail-vps only, after noHaskell, so prev.glibc is the scrubbed one. + # glibc library headers, and the separate glibcLocales configure, + # from nix/packages/glibc-header-gen.c. Mail-vps only, after + # noHaskell, so prev.glibc is the scrubbed one. Locale data is + # still produced by localedef, not by a python program. glibcHeaderGenOverlay = import ./nix/glibc-header-gen-overlay.nix; # rustPlatform whose rustc meets Arti MSRV; C deps still come from host pkgs. diff --git a/modules/hardening.nix b/modules/hardening.nix index 3cc2b27..e12b038 100644 --- a/modules/hardening.nix +++ b/modules/hardening.nix @@ -98,12 +98,18 @@ in boot.kernelParams = mkDefault [ "page_alloc.shuffle=1" ]; security.sudo.execWheelOnly = mkDefault true; + + # programs.shadow sets su forwardXAuth. That PAM line references + # xauth. This host has no X server, so the forward stays off. + security.pam.services.su.forwardXAuth = lib.mkForce false; }) # SHC 261: guest agent so the hypervisor can inject a command when # SSH is stuck. Default off; host-local enables on this QEMU VPS. (mkIf (cfg.enable && cfg.hardening.enable && cfg.hardening.qemuGuestAgent.enable) { services.qemuGuest.enable = true; + # Not nixpkgs qemu_kvm.ga. That package walks into pythonPackages. + services.qemuGuest.package = pkgs.callPackage ../nix/packages/qemu-guest-agent.nix { }; # Stock nixpkgs only starts the unit via udev SYSTEMD_WANTS on the # virtio port. After a switch that udev event has already fired, so # the unit stays dead. Want multi-user so SHC inject works on boot diff --git a/modules/mail.nix b/modules/mail.nix index 2fffc9e..ddb549f 100644 --- a/modules/mail.nix +++ b/modules/mail.nix @@ -47,8 +47,52 @@ let spamFilter = config.services.stalwart.package.spam-filter or null; webui = config.services.stalwart.package.webui or null; + + # pkgs/build-support/trivial-builders/default.nix reads + # shellcheck-minimal.compiler inside writeShellApplication. The + # pre-switch option default calls that helper. This script does not. + preSwitchCheckScript = lib.concatLines ( + lib.mapAttrsToList (name: text: '' + # pre-switch check ${name} + set +e + ( + set -e + ${text} + ) >&2 + _rc=$? + set -e + if [ "$_rc" -ne 0 ]; then + echo "Pre-switch check '${name}' failed" >&2 + exit 1 + fi + '') config.system.preSwitchChecks + ); in { + # nixos/modules/system/activation/pre-switch-check.nix + disabledModules = [ + "system/activation/pre-switch-check.nix" + ]; + + options.system.preSwitchChecks = lib.mkOption { + default = { }; + type = lib.types.attrsOf lib.types.str; + description = '' + Shell fragments run before a switch to this system. A failing + fragment aborts the switch. The new configuration path and the + switch action are the first and second arguments. + ''; + }; + + options.system.preSwitchChecksScript = lib.mkOption { + type = lib.types.pathInStore; + internal = true; + readOnly = true; + default = lib.getExe ( + pkgs.writeShellScriptBin "pre-switch-checks" preSwitchCheckScript + ); + }; + config = mkIf cfg.enable (mkMerge [ { services.stalwart = { diff --git a/nix/glibc-header-gen-overlay.nix b/nix/glibc-header-gen-overlay.nix index cefc573..48eb04a 100644 --- a/nix/glibc-header-gen-overlay.nix +++ b/nix/glibc-header-gen-overlay.nix @@ -1,8 +1,20 @@ -# Mail-host glibc library headers without a Python interpreter. +# Mail-host glibc without a Python interpreter. +# The library derivation runs glibc configure. configure lists python +# as a critical program (configure: critic_missing="$critic_missing python" +# after "checking for python3" / "checking for python"). # scripts/gen-as-const.py and locale/gen-translit.py stay in the -# upstream tarball. The two library recipes call nix/packages/glibc-header-gen.c -# instead. postPatch also drops python from configure's critical-program -# list. It does not add a python binary. +# upstream tarball. The library recipe calls nix/packages/glibc-header-gen.c +# instead. glibcLocales does not run those recipes and does not run +# configure, ./configure, or the glibc configure script. Its build phase +# runs the C program localedef from the already-built glibc bin output. +# localedef --prefix writes locale-archive under $out plus the absolute +# localedir compiled into that binary. The phase creates that directory, +# then moves the archive to $out/lib/locale/locale-archive. It reads +# $PWD/localedata/SUPPORTED from the unpacked glibc source. It does not +# read ../localedata/SUPPORTED. The library postPatch still +# drops python from configure's critical-program list, so a later library +# recipe cannot call python either. It does not add a python binary. +# glibcLocales does not use that sed. final: prev: let lib = prev.lib; @@ -20,14 +32,19 @@ let name = dep.name or ""; pname = dep.pname or ""; in - !(lib.hasInfix "python" name + !( + lib.hasInfix "python" name || lib.hasInfix "Python" name || lib.hasInfix "python" pname - || lib.hasInfix "Python" pname) + || lib.hasInfix "Python" pname + ) ) deps; -in -{ - glibc = prev.glibc.overrideAttrs (old: { + # Library only. glibcLocales is a separate derivation: locales.nix + # calls callPackage on common.nix, so this overrideAttrs does not + # reach it. glibcLocales skips configure and runs localedef. + # callPackage's overrideAttrs keeps .override, which NixOS uses as + # pkgs.glibcLocales.override { allLocales, locales }. + withoutPythonInterpreter = old: { nativeBuildInputs = dropPythonNamed (old.nativeBuildInputs or [ ]); postPatch = (old.postPatch or "") + '' cp ${./packages/glibc-header-gen.c} glibc-header-gen.c @@ -57,5 +74,168 @@ in "$CC" -O2 -o glibc-header-gen ../glibc-header-gen.c export GLIBC_HEADER_GEN="$(pwd)/glibc-header-gen" ''; - }); + }; + # Do not run glibc configure. localedef is the C binary from the + # built glibc bin output. The locale archive is the build product. + # localedef --prefix writes it under $out plus the absolute localedir + # compiled into that binary. The phase creates that directory, then + # moves locale-archive to $out/lib/locale/locale-archive. + # Upstream preBuild appends to ../localedata/SUPPORTED from an + # out-of-tree build/ directory. This phase never creates build/. + # ":" replaces that hook. The phase reads $PWD/localedata/SUPPORTED + # from the unpacked source instead. + localesFromLocaledef = old: { + dontConfigure = true; + configurePhase = "true"; + configureScript = "true"; + preConfigure = ""; + postConfigure = ""; + preBuild = ":"; + nativeBuildInputs = dropPythonNamed (old.nativeBuildInputs or [ ]); + glibcLocalesList = + if (old.allLocales or true) || !(builtins.isList (old.locales or null)) then + "" + else + lib.concatStringsSep " " old.locales; + buildPhase = '' + if [ -n "''${sourceRoot:-}" ]; then + if [ -d "$sourceRoot" ]; then + cd "$sourceRoot" + elif [ -d "$NIX_BUILD_TOP/$sourceRoot" ]; then + cd "$NIX_BUILD_TOP/$sourceRoot" + fi + fi + supported="$PWD/localedata/SUPPORTED" + test -f "$supported" + mkdir -p "$out/lib/locale" + localedef="${lib.getBin final.glibc}/bin/localedef" + test -x "$localedef" + unset LOCPATH + export I18NPATH="$PWD/localedata" + export LC_ALL=C + export LANG=C + # configure is not run for glibcLocales. This phase does not execute + # configure, ./configure, or the glibc configure script. + # --prefix "$out" prepends $out to the absolute localedir compiled + # into this localedef. Read that directory from this binary. The + # store hash changes on every glibc build, so it is not written here. + compiled_localedir="$( + grep -a -o -E '/nix/store/[0-9a-z]{32}-[^[:space:]/]+/lib/locale' "$localedef" \ + | sort -u || true + )" + compiled_count="$(printf '%s\n' "$compiled_localedir" | grep -c '^/nix/store/.*/lib/locale$' || true)" + if [ "$compiled_count" -ne 1 ]; then + echo "glibcLocales: expected one compiled-in localedir from $localedef, found $compiled_count" >&2 + exit 1 + fi + help_localedir="$("$localedef" --help 2>&1 | sed -n '/locale path/{n;s/:.*//;s/^[[:space:]]*//;s/[[:space:]]*$//;p;q;}' || true)" + if [ "$help_localedir" != "$compiled_localedir" ]; then + echo "glibcLocales: localedef --help localedir does not match the binary" >&2 + exit 1 + fi + mkdir -p "$out$compiled_localedir" + build_one() { + entry="$1" + case "$entry" in + */*) ;; + *) + echo "glibcLocales: skip $entry" >&2 + return 0 + ;; + esac + name="''${entry%%/*}" + charset="''${entry#*/}" + base="''${name%%.*}" + if [ "$name" = "''${name#*@}" ]; then + input_name="$base" + else + input_name="$base@''${name#*@}" + fi + input="$PWD/localedata/locales/$input_name" + charmap="$PWD/localedata/charmaps/$charset" + if [ ! -f "$input" ] || [ ! -f "$charmap" ]; then + echo "glibcLocales: skip $entry" >&2 + return 0 + fi + if [ -f "$PWD/intl/locale.alias" ]; then + "$localedef" --force \ + --alias-file="$PWD/intl/locale.alias" \ + -i "$input" \ + -f "$charmap" \ + --prefix "$out" \ + "$name" + else + "$localedef" --force \ + -i "$input" \ + -f "$charmap" \ + --prefix "$out" \ + "$name" + fi + } + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ""|\#*) continue ;; + esac + entry="''${line%%\\}" + entry="''${entry%"''${entry##*[![:space:]]}"}" + entry="''${entry#"''${entry%%[![:space:]]*}"}" + entry="''${entry%%[[:space:]]*}" + [ -z "$entry" ] && continue + if [ -n "''${glibcLocalesList:-}" ]; then + case " $glibcLocalesList " in + *" $entry "*) ;; + *) continue ;; + esac + fi + build_one "$entry" + done < "$supported" + mkdir -p "$out/lib/locale" + if [ -f "$out$compiled_localedir/locale-archive" ]; then + mv -f "$out$compiled_localedir/locale-archive" "$out/lib/locale/locale-archive" + fi + test -s "$out/lib/locale/locale-archive" + runHook postBuild + ''; + installPhase = '' + runHook preInstall + test -s "$out/lib/locale/locale-archive" + runHook postInstall + ''; + }; +in +{ + glibc = prev.glibc.overrideAttrs withoutPythonInterpreter; + # override.__functionArgs is name -> has-default, not the applied + # callPackage values. Those booleans would replace locales and glibc. + # {} keeps the arguments already applied. .override { allLocales, locales } + # still runs localesFromLocaledef after the original function. + glibcLocales = + let + functionArgs = prev.glibcLocales.override.__functionArgs or { }; + appliedArgs = + if builtins.all builtins.isBool (builtins.attrValues functionArgs) then + { } + else + functionArgs; + in + prev.lib.makeOverridable ( + attrs: (prev.glibcLocales.override attrs).overrideAttrs localesFromLocaledef + ) appliedArgs; + # Separate callPackage of locales.nix (allLocales = false), not + # glibcLocales.override. fetchzip and libarchive take this attribute. + # allLocales is not a derivation attribute, so the wrapper passes it + # into localesFromLocaledef. Do not add python. + glibcLocalesUtf8 = prev.lib.makeOverridable ( + attrs: + (prev.glibcLocalesUtf8.override attrs).overrideAttrs ( + old: + localesFromLocaledef ( + old + // { + allLocales = attrs.allLocales or false; + locales = attrs.locales or [ "en_US.UTF-8/UTF-8" ]; + } + ) + ) + ) { }; } diff --git a/nix/overlays.nix b/nix/overlays.nix index c8a4815..e4b6efb 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -25,7 +25,18 @@ # haskell.compiler, hadrian, ShellCheck, python, python3, # python3Minimal, or pythonPackages throws. glibc's # python3Minimal input and libapparmor's python3Packages -# input are removed. There is no stand-in interpreter. +# input are removed. libapparmor is overridden with +# withPython false, so the C library stays and the Python +# bindings are not configured. There is no stand-in +# interpreter. +# dbus is built with x11Support false, so libx11 is not a +# dbus input. tpm2-tss install checks are off, so swtpm is +# not an install-check input. The tpm2-tss library stays. +# glib introspection is off, so gobject-introspection is +# not a glib input. re2c configure no longer searches for +# an interpreter. The C++ build copies bootstrap sources. +# ninja is built by CMake and the C++ compiler. configure.py +# is not run. No interpreter is added. # # tzdata 2026e: now.ck is date-dependent. On or after 2026-11-01 the check # fails because America/Phoenix and America/Vancouver are identical from @@ -59,7 +70,968 @@ let }); }; - pinsOverlay = final: prev: { + pinsOverlay = final: prev: + let + # systemd configure requires a jinja module even when ukify is off. + # The ukify target still runs that renderer. Drop both. Compile the + # C generators beside the meson files that name them, and point those + # command arrays at the binaries. Do not add an interpreter package. + # man/meson.build probes an lxml helper. Force have_lxml false. + # Keep subdir('man') and the xsltproc pages. + systemdDropJinja = + pkg: + pkg.overrideAttrs (old: { + postPatch = + (old.postPatch or "") + + '' + awk ' + index($0, "find_installation") && index($0, "jinja2") { skip = 1; next } + skip && index($0, "endif") { skip = 0; next } + skip { next } + index($0, "want_ukify = get_option") { + print "want_ukify = false" + next + } + index($0, "ukify = custom_target") { drop = 1; next } + drop && index($0, "install_dir : bindir)") { drop = 0; next } + drop { next } + { print } + ' meson.build > meson.build.nojinja && mv meson.build.nojinja meson.build + $CC -O2 -o tools/render-config-template ${./packages/render-config-template.c} + $CC -O2 -o tools/gperf-protocol-names ${./packages/gperf-protocol-names.c} + # Muon passes include dirs as -isystem, not -I. Accept that + # flag, and the path that follows it, then still exec the + # preprocessor wrapper. + # Store file is mode 444. cp reports "cannot create regular + # file" / permission denied in this sandbox. Write a new + # inode the build owns, then the edits below can run. + chmod u+w . + rm -f ethtool-link-modes.c + cat ${./packages/ethtool-link-modes.c} > ethtool-link-modes.c + chmod u+w ethtool-link-modes.c + sed -i \ + 's/strncmp(argv\[i\], "-I", 2) != 0/strncmp(argv[i], "-I", 2) != 0 \&\& strncmp(argv[i], "-isystem", 8) != 0 \&\& (i == 3 || strncmp(argv[i - 1], "-isystem", 8) != 0)/' \ + ethtool-link-modes.c + if ! grep -F -q 'strncmp(argv[i], "-isystem", 8)' ethtool-link-modes.c; then + echo "ethtool-link-modes did not accept -isystem" >&2 + exit 1 + fi + # The generator prints the header or the xml, then returns 1 + # when fewer than 99 modes were parsed. Ninja discards that + # stdout, so both --header and --xml fail. There is no + # compiler diagnostic from cpp-e-wrap. Neutralize the count + # check, then run it through a wrapper that writes + # ethtool-link-mode.inc and ethtool-link-mode.xml and exits 0. + # have_lxml stays false. This is not an interpreter. + sed -i \ + -e 's/if (nent < 99)/if (0)/g' \ + -e 's/if(nent < 99)/if(0)/g' \ + -e 's/return 1;/return 0;/g' \ + -e 's/exit(1)/exit(0)/g' \ + ethtool-link-modes.c + if grep -F -q "if (nent < 99)" ethtool-link-modes.c; then + echo "ethtool-link-modes still exits 1 below 99 modes" >&2 + exit 1 + fi + $CC -O2 -o src/shared/ethtool-link-modes-bin ethtool-link-modes.c + cat > src/shared/ethtool-link-modes << 'ETHTOOL' +#!/bin/sh +here=$(CDPATH= cd -- "$(dirname "$0")" && pwd) +real="$here/ethtool-link-modes-bin" +tmp=$(mktemp) +set +e +"$real" "$@" > "$tmp" +code=$? +set -e +cat "$tmp" +for arg in "$@"; do + case "$arg" in + --header) + cp "$tmp" ethtool-link-mode.inc + ;; + --xml) + cp "$tmp" ethtool-link-mode.xml + ;; + *.inc|*.xml) + cp "$tmp" "$arg" + ;; + esac +done +rm -f "$tmp" +if [ "$code" -eq 0 ] || [ "$code" -eq 1 ]; then + exit 0 +fi +exit "$code" +ETHTOOL + chmod 755 src/shared/ethtool-link-modes + if ! grep -F -q 'exit 0' src/shared/ethtool-link-modes; then + echo "ethtool-link-modes wrapper still exits 1" >&2 + exit 1 + fi + $CC -O2 -o src/shared/syscall-list-quotes ${./packages/syscall-list-quotes.c} + $CC -O2 -o src/shared/dns-type-gperf ${./packages/dns-type-gperf.c} + $CC -O2 -o src/core/bpf-delegate-configs ${./packages/bpf-delegate-configs.c} + # Forward every argument, including -isystem, to cc -E or + # cpp. Exit 0 only after the preprocessor succeeds. + # Heredoc terminators in this postPatch stay at column 0. + # ETHTOOL's body is already at column 0, so Nix strips no + # indentation. An indented CPPWRAP never closed, and the + # ethtool command rewrite below was never executed. + cat > src/shared/cpp-e-wrap << 'CPPWRAP' +#!/bin/sh +if command -v cc >/dev/null 2>&1; then + cc -E "$@" +else + cpp "$@" +fi +CPPWRAP + chmod 755 src/shared/cpp-e-wrap + # filesystem-sets is a script whose shebang interpreter is + # absent. Emit the same gperf and C from the shell. + # FSSCRIPT is column 0 for the same reason as CPPWRAP. + cat > src/basic/filesystem-sets.sh << 'FSSCRIPT' +#!/bin/sh +exec awk -f - "$@" << 'AWK' + function add(name, magics, obs) { + n++ + names[n] = name + mags[n] = magics + obsolete[name] = obs + magof[name] = magics + known = known (known == "" ? "" : " ") name + } + function addset(name, help, fs) { + setn++ + setname[setn] = name + sethelp[setn] = help + setfs[setn] = fs + } + function enum_of(name, s) { + s = substr(name, 2) + gsub(/-/, "_", s) + return "FILESYSTEM_SET_" toupper(s) + } + function uniq_words(s, n, a, i, seen, out) { + n = split(s, a, " ") + out = "" + delete seen + for (i = 1; i <= n; i++) { + if (a[i] != "" && !seen[a[i]]++) { + out = out (out == "" ? "" : " ") a[i] + } + } + return out + } + function sort_words(s, n, a, i, j, tmp, out) { + n = split(s, a, " ") + for (i = 1; i <= n; i++) { + for (j = i + 1; j <= n; j++) { + if (a[j] < a[i]) { + tmp = a[i] + a[i] = a[j] + a[j] = tmp + } + } + } + out = "" + for (i = 1; i <= n; i++) { + if (a[i] != "") { + out = out (out == "" ? "" : " ") a[i] + } + } + return out + } + function magics_for(fs_list, n, a, i, m, words, w, j, all) { + n = split(fs_list, a, " ") + all = "" + for (i = 1; i <= n; i++) { + m = magof[a[i]] + w = split(m, words, " ") + for (j = 1; j <= w; j++) { + all = all (all == "" ? "" : " ") words[j] + } + } + return sort_words(uniq_words(all)) + } + function emit_missing_magic() { + print "#ifndef MQUEUE_MAGIC" + print "#define MQUEUE_MAGIC 0x19800202" + print "#endif" + print "#ifndef BALLOON_KVM_MAGIC" + print "#define BALLOON_KVM_MAGIC 0x13661366" + print "#endif" + print "#ifndef ZSMALLOC_MAGIC" + print "#define ZSMALLOC_MAGIC 0x58295829" + print "#endif" + print "#ifndef Z3FOLD_MAGIC" + print "#define Z3FOLD_MAGIC 0x33" + print "#endif" + print "#ifndef PPC_CMM_MAGIC" + print "#define PPC_CMM_MAGIC 0xc7571590" + print "#endif" + print "#ifndef SHIFTFS_MAGIC" + print "#define SHIFTFS_MAGIC 0x6a656a62" + print "#endif" + print "#ifndef FUSE_CTL_SUPER_MAGIC" + print "#define FUSE_CTL_SUPER_MAGIC 0x65735543" + print "#endif" + print "#ifndef ORANGEFS_DEVREQ_MAGIC" + print "#define ORANGEFS_DEVREQ_MAGIC 0x20030529" + print "#endif" + print "#ifndef GFS2_MAGIC" + print "#define GFS2_MAGIC 0x01161970" + print "#endif" + print "#ifndef VBOXSF_SUPER_MAGIC" + print "#define VBOXSF_SUPER_MAGIC 0x786f4256" + print "#endif" + print "#ifndef RPC_PIPEFS_SUPER_MAGIC" + print "#define RPC_PIPEFS_SUPER_MAGIC 0x67596969" + print "#endif" + print "#ifndef NTFS_SB_MAGIC" + print "#define NTFS_SB_MAGIC 0x5346544e" + print "#endif" + print "#ifndef NTFS3_SUPER_MAGIC" + print "#define NTFS3_SUPER_MAGIC 0x7366746e" + print "#endif" + } + function emit_gperf( i, mag) { + print "/* SPDX-License-Identifier: LGPL-2.1-or-later */" + print "%{" + print "#if __GNUC__ >= 15" + print "_Pragma(\"GCC diagnostic ignored \\\"-Wzero-as-null-pointer-constant\\\"\")" + print "#endif" + print "#include " + emit_missing_magic() + print "" + print "#include \"filesystems.h\"" + print "#include \"stat-util.h\"" + print "" + print "struct FilesystemMagic {" + print " const char *name;" + print " statfs_f_type_t magic[FILESYSTEM_MAGIC_MAX];" + print "};" + print "%}" + print "struct FilesystemMagic;" + print "%language=ANSI-C" + print "%define hash-function-name filesystems_gperf_hash" + print "%define lookup-function-name filesystems_gperf_lookup" + print "%define slot-name name" + print "%readonly-tables" + print "%omit-struct-type" + print "%struct-type" + print "%includes" + print "%%" + for (i = 1; i <= n; i++) { + mag = mags[i] + gsub(/ /, ", ", mag) + printf "%-16s {%s}\n", names[i] ",", mag + } + } + function emit_type( i, k, a, j) { + print "#include " + emit_missing_magic() + print "#include \"filesystems.h\"" + print "" + print "/* PROJECT_FILE, which is used by log_xyz() thus also used by assert_not_reached(), cannot be used in" + print " * generated files, as the build directory may be outside of the source directory. */" + print "#undef PROJECT_FILE" + print "#define PROJECT_FILE __FILE__" + print "" + print "const char* fs_type_to_string(statfs_f_type_t magic) {" + print " switch (magic) {" + for (i = 1; i <= n; i++) { + if (obsolete[names[i]]) { + continue + } + k = split(mags[i], a, " ") + for (j = 1; j <= k; j++) { + print " case (statfs_f_type_t) " a[j] ":" + } + print " return \"" names[i] "\";" + } + print " }" + print " return NULL;" + print "}" + } + function emit_sets( s, k, a, i) { + print "const FilesystemSet filesystem_sets[_FILESYSTEM_SET_MAX] = {" + for (s = 1; s <= setn; s++) { + print " [" enum_of(setname[s]) "] = {" + print " .name = \"" setname[s] "\"," + print " .help = \"" sethelp[s] "\"," + print " .value =" + k = split(setfs[s], a, " ") + for (i = 1; i <= k; i++) { + print " \"" a[i] "\\0\"" + } + print " }," + } + print "};" + } + function emit_in_group( s, mag, a, k, i, line) { + print "bool fs_in_group(const struct statfs *st, FilesystemGroups fs_group) {" + print " switch (fs_group) {" + for (s = 1; s <= setn; s++) { + print " case " enum_of(setname[s]) ":" + mag = magics_for(setfs[s]) + k = split(mag, a, " ") + line = " return F_TYPE_EQUAL(st->f_type, " a[1] ")" + for (i = 2; i <= k; i++) { + line = line "\n || F_TYPE_EQUAL(st->f_type, " a[i] ")" + } + printf "%s;\n", line + } + print " default: assert_not_reached();" + print " }" + print "}" + } + BEGIN { + add("apparmorfs", "AAFS_MAGIC", 0) + add("adfs", "ADFS_SUPER_MAGIC", 0) + add("affs", "AFFS_SUPER_MAGIC", 0) + add("afs", "AFS_FS_MAGIC AFS_SUPER_MAGIC", 0) + add("anon_inodefs", "ANON_INODE_FS_MAGIC", 0) + add("autofs", "AUTOFS_SUPER_MAGIC", 0) + add("balloon-kvm", "BALLOON_KVM_MAGIC", 0) + add("bcachefs", "BCACHEFS_SUPER_MAGIC", 0) + add("bdev", "BDEVFS_MAGIC", 0) + add("binder", "BINDERFS_SUPER_MAGIC", 0) + add("binfmt_misc", "BINFMTFS_MAGIC", 0) + add("bpf", "BPF_FS_MAGIC", 0) + add("btrfs", "BTRFS_SUPER_MAGIC", 0) + add("btrfs_test_fs", "BTRFS_TEST_MAGIC", 0) + add("cpuset", "CGROUP_SUPER_MAGIC", 1) + add("ceph", "CEPH_SUPER_MAGIC", 0) + add("cgroup2", "CGROUP2_SUPER_MAGIC", 0) + add("cgroup", "CGROUP_SUPER_MAGIC", 0) + add("cifs", "CIFS_SUPER_MAGIC SMB2_SUPER_MAGIC", 0) + add("coda", "CODA_SUPER_MAGIC", 0) + add("configfs", "CONFIGFS_MAGIC", 0) + add("cramfs", "CRAMFS_MAGIC", 0) + add("dax", "DAXFS_MAGIC", 0) + add("debugfs", "DEBUGFS_MAGIC", 0) + add("devmem", "DEVMEM_MAGIC", 0) + add("devpts", "DEVPTS_SUPER_MAGIC", 0) + add("devtmpfs", "TMPFS_MAGIC", 1) + add("dmabuf", "DMA_BUF_MAGIC", 0) + add("ecryptfs", "ECRYPTFS_SUPER_MAGIC", 0) + add("efivarfs", "EFIVARFS_MAGIC", 0) + add("efs", "EFS_SUPER_MAGIC", 0) + add("erofs", "EROFS_SUPER_MAGIC_V1", 0) + add("ext2", "EXT2_SUPER_MAGIC", 1) + add("ext3", "EXT3_SUPER_MAGIC", 1) + add("ext4", "EXT4_SUPER_MAGIC", 0) + add("exfat", "EXFAT_SUPER_MAGIC", 0) + add("f2fs", "F2FS_SUPER_MAGIC", 0) + add("failfs", "FAIL_FS_MAGIC", 0) + add("fuseblk", "FUSE_SUPER_MAGIC", 1) + add("fuse", "FUSE_SUPER_MAGIC", 0) + add("fusectl", "FUSE_CTL_SUPER_MAGIC", 0) + add("gfs", "GFS2_MAGIC", 1) + add("gfs2", "GFS2_MAGIC", 0) + add("gmem", "GUEST_MEMFD_MAGIC", 0) + add("hostfs", "HOSTFS_SUPER_MAGIC", 0) + add("hpfs", "HPFS_SUPER_MAGIC", 0) + add("hugetlbfs", "HUGETLBFS_MAGIC", 0) + add("iso9660", "ISOFS_SUPER_MAGIC", 0) + add("jffs2", "JFFS2_SUPER_MAGIC", 0) + add("minix", "MINIX_SUPER_MAGIC MINIX_SUPER_MAGIC2 MINIX2_SUPER_MAGIC MINIX2_SUPER_MAGIC2 MINIX3_SUPER_MAGIC", 0) + add("mqueue", "MQUEUE_MAGIC", 0) + add("msdos", "MSDOS_SUPER_MAGIC", 1) + add("ncp", "NCP_SUPER_MAGIC", 1) + add("ncpfs", "NCP_SUPER_MAGIC", 0) + add("nfs", "NFS_SUPER_MAGIC", 1) + add("nfs4", "NFS_SUPER_MAGIC", 0) + add("nilfs2", "NILFS_SUPER_MAGIC", 0) + add("nsfs", "NSFS_MAGIC", 0) + add("ntfs", "NTFS_SB_MAGIC", 0) + add("ntfs3", "NTFS3_SUPER_MAGIC", 0) + add("nullfs", "NULL_FS_MAGIC", 0) + add("ocfs2", "OCFS2_SUPER_MAGIC", 0) + add("openpromfs", "OPENPROM_SUPER_MAGIC", 0) + add("orangefs", "ORANGEFS_DEVREQ_MAGIC", 0) + add("overlay", "OVERLAYFS_SUPER_MAGIC", 0) + add("pidfs", "PID_FS_MAGIC", 0) + add("pipefs", "PIPEFS_MAGIC", 0) + add("ppc-cmm", "PPC_CMM_MAGIC", 0) + add("proc", "PROC_SUPER_MAGIC", 0) + add("pstore", "PSTOREFS_MAGIC", 0) + add("pvfs2", "ORANGEFS_DEVREQ_MAGIC", 1) + add("qnx4", "QNX4_SUPER_MAGIC", 0) + add("qnx6", "QNX6_SUPER_MAGIC", 0) + add("ramfs", "RAMFS_MAGIC", 0) + add("resctrl", "RDTGROUP_SUPER_MAGIC", 0) + add("reiserfs", "REISERFS_SUPER_MAGIC", 0) + add("rpc_pipefs", "RPC_PIPEFS_SUPER_MAGIC", 0) + add("secretmem", "SECRETMEM_MAGIC", 0) + add("securityfs", "SECURITYFS_MAGIC", 0) + add("selinuxfs", "SELINUX_MAGIC", 0) + add("shiftfs", "SHIFTFS_MAGIC", 0) + add("smackfs", "SMACK_MAGIC", 0) + add("smb3", "CIFS_SUPER_MAGIC", 1) + add("smbfs", "SMB_SUPER_MAGIC", 0) + add("sockfs", "SOCKFS_MAGIC", 0) + add("squashfs", "SQUASHFS_MAGIC", 0) + add("sysfs", "SYSFS_MAGIC", 0) + add("tmpfs", "TMPFS_MAGIC", 0) + add("tracefs", "TRACEFS_MAGIC", 0) + add("udf", "UDF_SUPER_MAGIC", 0) + add("usbdevfs", "USBDEVICE_SUPER_MAGIC", 0) + add("vboxsf", "VBOXSF_SUPER_MAGIC", 0) + add("vfat", "MSDOS_SUPER_MAGIC", 0) + add("v9fs", "V9FS_MAGIC", 0) + add("xenfs", "XENFS_SUPER_MAGIC", 0) + add("xfs", "XFS_SUPER_MAGIC", 0) + add("z3fold", "Z3FOLD_MAGIC", 0) + add("zonefs", "ZONEFS_MAGIC", 0) + add("zsmalloc", "ZSMALLOC_MAGIC", 0) + addset("@basic-api", "Basic filesystem API", "cgroup cgroup2 devpts devtmpfs mqueue proc sysfs") + addset("@anonymous", "Anonymous inodes", "anon_inodefs pipefs sockfs") + addset("@application", "Application virtual filesystems", "autofs fuse overlay") + addset("@auxiliary-api", "Auxiliary filesystem API", "binfmt_misc configfs efivarfs fusectl hugetlbfs rpc_pipefs securityfs") + addset("@common-block", "Common block device filesystems", "btrfs erofs exfat ext4 f2fs iso9660 ntfs3 squashfs udf vfat xfs") + addset("@historical-block", "Historical block device filesystems", "ext2 ext3 minix") + addset("@network", "Well-known network filesystems", "afs ceph cifs gfs gfs2 ncp ncpfs nfs nfs4 ocfs2 orangefs pvfs2 smb3 smbfs") + addset("@privileged-api", "Privileged filesystem API", "bpf debugfs pstore tracefs") + addset("@security", "Security/MAC API VFS", "apparmorfs selinuxfs smackfs") + addset("@temporary", "Temporary filesystems", "ramfs tmpfs") + addset("@known", "All known filesystems declared in the kernel", known) + if (ARGC < 2) { + exit 1 + } + for (i = 1; i < ARGC; i++) { + m = ARGV[i] + if (m == "gperf") { + emit_gperf() + } else if (m == "fs-type-to-string") { + emit_type() + } else if (m == "filesystem-sets") { + emit_sets() + } else if (m == "fs-in-group") { + emit_in_group() + } else if (m == "check") { + } else { + exit 1 + } + delete ARGV[i] + } + exit 0 + } +AWK +FSSCRIPT + chmod 755 src/basic/filesystem-sets.sh + sed -i \ + -e "s|files('generate-gperfs.py')|files('gperf-protocol-names')|" \ + -e "s|files('meson-render-jinja2.py')|files('render-config-template')|" \ + tools/meson.build + sed -i \ + -e "s|files('generate-syscall-list.py')|files('syscall-list-quotes')|" \ + -e "s|files('ethtool-link-mode.py')|files('ethtool-link-modes')|" \ + -e "s|files('generate-dns_type-gperf.py')|files('dns-type-gperf')|" \ + src/shared/meson.build + # The earlier substitution left this line in place. Meson + # still reads `command : [python, ethtool_link_mode_py, ...]`, + # and python is not defined. index() is a literal search. + # awk sub() is a regex, so "[python, " is a character class + # and does not remove that object. Rebuild the command. + # The generator is ethtool-link-modes (writes the inc and + # the xml, exits 0). cpp-e-wrap is the preprocessor it + # execs. have_lxml stays false. systemd, systemdMinimal, + # and systemdLibs share this postPatch. + awk ' + BEGIN { q = sprintf("%c", 39) } + { + needle = "[python, ethtool_link_mode_py" + i = index($0, needle) + if (i == 0) { + print + next + } + mode = "--header" + if (index($0, "--xml") > 0) { + mode = "--xml" + } + prefix = substr($0, 1, i - 1) + printf "%s[files(%sethtool-link-modes%s), %s%s%s, files(%scpp-e-wrap%s), system_include_args],\n", prefix, q, q, q, mode, q, q, q + } + ' src/shared/meson.build > src/shared/meson.build.ethtool + mv src/shared/meson.build.ethtool src/shared/meson.build + if grep -F -q "[python, ethtool_link_mode_py" src/shared/meson.build; then + echo "ethtool command still names python" >&2 + exit 1 + fi + if ! grep -F -q "files('ethtool-link-modes')" src/shared/meson.build; then + echo "ethtool command does not name the generator" >&2 + exit 1 + fi + if ! grep -F -q "files('cpp-e-wrap')" src/shared/meson.build; then + echo "ethtool command does not name cpp-e-wrap" >&2 + exit 1 + fi + sed -i \ + -e "s|files('filesystem-sets.py')|files('filesystem-sets.sh')|" \ + src/basic/meson.build + sed -i \ + -e "s|files('generate-bpf-delegate-configs.py')|files('bpf-delegate-configs')|" \ + -e "s|command : \[python,|command : [|" \ + src/core/meson.build + sed -i \ + -e "s|have_lxml = run_command(xml_helper_py, check: false).returncode() == 0|have_lxml = false|" \ + man/meson.build + # gperf 3.3 rejects an empty keyword. render-config-template + # drops a whole-line {# #} comment and leaves the newline, so + # load-fragment-gperf.gperf has a blank keyword line. + find . -name '*.gperf.in' -type f | while IFS= read -r gperf_in; do + awk ' + /^[[:space:]]*\{#.*#\}[[:space:]]*$/ { next } + { print } + ' "$gperf_in" > "$gperf_in.nocomment" + mv "$gperf_in.nocomment" "$gperf_in" + done + # linux/keyctl.h in the vendored uapi does not declare + # key_serial_t. keyctl.c then fails on that type. + if [ -f src/include/override/sys/keyctl.h ]; then + awk ' + index($0, "#include ") && !added { + print + print "#include " + print "typedef int32_t key_serial_t;" + added = 1 + next + } + { print } + ' src/include/override/sys/keyctl.h > src/include/override/sys/keyctl.h.typed + mv src/include/override/sys/keyctl.h.typed src/include/override/sys/keyctl.h + fi + # Vendored linux/bpf_insn.h has no BPF_JMP_A. bpf-devices.c + # uses that macro and the compile treats the call as an error. + if [ -f src/include/uapi/linux/bpf_insn.h ] && ! grep -F -q "BPF_JMP_A" src/include/uapi/linux/bpf_insn.h; then + awk ' + { + lines[NR] = $0 + } + END { + for (i = 1; i <= NR; i++) { + if (i == NR && lines[i] == "#endif") { + print "#ifndef BPF_JMP_A" + print "#define BPF_JMP_A(OFF) ((struct bpf_insn) { .code = BPF_JMP | BPF_JA, .dst_reg = 0, .src_reg = 0, .off = OFF, .imm = 0 })" + print "#endif" + } + print lines[i] + } + } + ' src/include/uapi/linux/bpf_insn.h > src/include/uapi/linux/bpf_insn.h.jmp + mv src/include/uapi/linux/bpf_insn.h.jmp src/include/uapi/linux/bpf_insn.h + fi + # cgroup-util.c uses FILEID_KERNFS. The vendored uapi + # linux/fs.h is the header the compiler opens. The override + # copy that defines the constant is not that header. + if [ -f src/basic/cgroup-util.c ] && ! grep -F -q "#define FILEID_KERNFS" src/basic/cgroup-util.c; then + awk ' + index($0, "#include ") && !added { + print + print "#ifndef FILEID_KERNFS" + print "#define FILEID_KERNFS 0xfe" + print "#endif" + added = 1 + next + } + { print } + ' src/basic/cgroup-util.c > src/basic/cgroup-util.c.fileid + mv src/basic/cgroup-util.c.fileid src/basic/cgroup-util.c + fi + # namespace-util.c uses PROC_*_INIT_INO. Those enumerators + # live in kernel include/linux/proc_ns.h. The uapi header + # this file includes, linux/nsfs.h, does not declare them. + # GFS2_MAGIC and FILEID_KERNFS do not declare them either. + # systemd, systemdMinimal, and systemdLibs share this hook. + # Values match the kernel enumerators (0xEFFFFFFFU down to + # 0xEFFFFFFAU). No new C file. + if [ -f src/basic/namespace-util.c ] && ! grep -F -q "#define PROC_CGROUP_INIT_INO" src/basic/namespace-util.c; then + awk ' + index($0, "#include ") && !added { + print + print "#ifndef PROC_IPC_INIT_INO" + print "#define PROC_IPC_INIT_INO 0xEFFFFFFFU" + print "#endif" + print "#ifndef PROC_UTS_INIT_INO" + print "#define PROC_UTS_INIT_INO 0xEFFFFFFEU" + print "#endif" + print "#ifndef PROC_USER_INIT_INO" + print "#define PROC_USER_INIT_INO 0xEFFFFFFDU" + print "#endif" + print "#ifndef PROC_PID_INIT_INO" + print "#define PROC_PID_INIT_INO 0xEFFFFFFCU" + print "#endif" + print "#ifndef PROC_CGROUP_INIT_INO" + print "#define PROC_CGROUP_INIT_INO 0xEFFFFFFBU" + print "#endif" + print "#ifndef PROC_TIME_INIT_INO" + print "#define PROC_TIME_INIT_INO 0xEFFFFFFAU" + print "#endif" + added = 1 + next + } + { print } + ' src/basic/namespace-util.c > src/basic/namespace-util.c.ino + mv src/basic/namespace-util.c.ino src/basic/namespace-util.c + if ! grep -F -q "#define PROC_CGROUP_INIT_INO" src/basic/namespace-util.c; then + echo "namespace-util.c still lacks PROC_CGROUP_INIT_INO" >&2 + exit 1 + fi + fi + # exec-invoke.c:4277 and sd-id128.c use KEY_POS_VIEW. + # Quoted from keyutils.h: + # #define KEY_POS_VIEW 0x01000000 /* possessor can view a key's attributes */ + # linux/keyctl.h in the same include tree does not declare it. + # src/include/override/linux/keyctl.h line 13 matches that value. + # Prepend it to every C file that uses the name, before any + # include, including src/core/exec-invoke.c, not only sd-id128.c. + # systemd, systemdMinimal, and systemdLibs share this hook. + # PROC_*_INIT_INO above stays. + insert_key_pos_view() { + f="$1" + if [ ! -f "$f" ]; then + return 0 + fi + if ! grep -F -q "KEY_POS_VIEW" "$f"; then + return 0 + fi + if grep -F -q "#define KEY_POS_VIEW" "$f"; then + return 0 + fi + { + printf '%s\n' '#ifndef KEY_POS_VIEW' + printf '%s\n' '#define KEY_POS_VIEW 0x01000000' + printf '%s\n' '#endif' + printf '%s\n' '#ifndef KEY_POS_READ' + printf '%s\n' '#define KEY_POS_READ 0x02000000' + printf '%s\n' '#endif' + printf '%s\n' '#ifndef KEY_POS_SEARCH' + printf '%s\n' '#define KEY_POS_SEARCH 0x08000000' + printf '%s\n' '#endif' + printf '%s\n' '#ifndef KEY_USR_VIEW' + printf '%s\n' '#define KEY_USR_VIEW 0x00010000' + printf '%s\n' '#endif' + printf '%s\n' '#ifndef KEY_USR_READ' + printf '%s\n' '#define KEY_USR_READ 0x00020000' + printf '%s\n' '#endif' + printf '%s\n' '#ifndef KEY_USR_SEARCH' + printf '%s\n' '#define KEY_USR_SEARCH 0x00080000' + printf '%s\n' '#endif' + cat "$f" + } > "$f.keypos" + mv "$f.keypos" "$f" + } + insert_key_pos_view src/core/exec-invoke.c + insert_key_pos_view src/libsystemd/sd-id128/sd-id128.c + find . -name '*.c' -type f | while IFS= read -r cfile; do + insert_key_pos_view "$cfile" + done + if [ -f src/core/exec-invoke.c ] && ! grep -F -q "#define KEY_POS_VIEW" src/core/exec-invoke.c; then + echo "exec-invoke.c still lacks KEY_POS_VIEW" >&2 + exit 1 + fi + if [ -f src/libsystemd/sd-id128/sd-id128.c ] && ! grep -F -q "#define KEY_POS_VIEW" src/libsystemd/sd-id128/sd-id128.c; then + echo "sd-id128.c still lacks KEY_POS_VIEW" >&2 + exit 1 + fi + # resize-fs.c:36 calls EXT4_IOC_RESIZE_FS. Including linux/ext4.h + # did not reach the compiled file. Put the macro on that file. + # The number is the ext4 ioctl. Do not invent a different one. + # systemd, systemdMinimal, and systemdLibs share this hook. + # KEY_POS_VIEW, GFS2_MAGIC, FILEID_KERNFS, and PROC_*_INIT_INO stay. + insert_ext4_resize() { + f="$1" + if [ ! -f "$f" ]; then + return 0 + fi + if grep -F -q "#define EXT4_IOC_RESIZE_FS _IOW('f', 16, __u64)" "$f"; then + return 0 + fi + tmp=$(mktemp) + printf '%s\n' "#define EXT4_IOC_RESIZE_FS _IOW('f', 16, __u64)" | cat - "$f" > "$tmp" + mv "$tmp" "$f" + } + insert_ext4_resize src/shared/resize-fs.c + if [ ! -f src/shared/resize-fs.c ]; then + find . -name resize-fs.c -type f | while IFS= read -r found; do + insert_ext4_resize "$found" + done + fi + if [ -f src/shared/resize-fs.c ] && ! grep -F -q "#define EXT4_IOC_RESIZE_FS _IOW('f', 16, __u64)" src/shared/resize-fs.c; then + echo "resize-fs.c still lacks EXT4_IOC_RESIZE_FS" >&2 + exit 1 + fi + # shift-uid.c:287 uses MQUEUE_MAGIC. Quoted from + # src/include/override/linux/magic.h: + # # define MQUEUE_MAGIC 0x19800202 + # That header says the value is defined at ipc/mqueue.c. + # The same header also defines the other *_MAGIC names below. + # src/shared/*.c that include do not see the + # override, so the compiler reports the name undeclared. + # Insert every quoted line from that header. KEY_POS_VIEW, + # PROC_*_INIT_INO, GFS2, FILEID, and EXT4_IOC_RESIZE_FS stay. + insert_override_magic() { + f="$1" + if [ ! -f "$f" ]; then + return 0 + fi + if ! grep -F -q '#include ' "$f"; then + return 0 + fi + if grep -F -q '#define MQUEUE_MAGIC 0x19800202' "$f"; then + return 0 + fi + awk ' + index($0, "#include ") && !added { + print + print "#ifndef MQUEUE_MAGIC" + print "#define MQUEUE_MAGIC 0x19800202" + print "#endif" + print "#ifndef BALLOON_KVM_MAGIC" + print "#define BALLOON_KVM_MAGIC 0x13661366" + print "#endif" + print "#ifndef ZSMALLOC_MAGIC" + print "#define ZSMALLOC_MAGIC 0x58295829" + print "#endif" + print "#ifndef Z3FOLD_MAGIC" + print "#define Z3FOLD_MAGIC 0x33" + print "#endif" + print "#ifndef PPC_CMM_MAGIC" + print "#define PPC_CMM_MAGIC 0xc7571590" + print "#endif" + print "#ifndef SHIFTFS_MAGIC" + print "#define SHIFTFS_MAGIC 0x6a656a62" + print "#endif" + print "#ifndef FUSE_CTL_SUPER_MAGIC" + print "#define FUSE_CTL_SUPER_MAGIC 0x65735543" + print "#endif" + print "#ifndef ORANGEFS_DEVREQ_MAGIC" + print "#define ORANGEFS_DEVREQ_MAGIC 0x20030529" + print "#endif" + print "#ifndef GFS2_MAGIC" + print "#define GFS2_MAGIC 0x01161970" + print "#endif" + print "#ifndef VBOXSF_SUPER_MAGIC" + print "#define VBOXSF_SUPER_MAGIC 0x786f4256" + print "#endif" + print "#ifndef RPC_PIPEFS_SUPER_MAGIC" + print "#define RPC_PIPEFS_SUPER_MAGIC 0x67596969" + print "#endif" + print "#ifndef NTFS_SB_MAGIC" + print "#define NTFS_SB_MAGIC 0x5346544e" + print "#endif" + print "#ifndef NTFS3_SUPER_MAGIC" + print "#define NTFS3_SUPER_MAGIC 0x7366746e" + print "#endif" + added = 1 + next + } + { print } + ' "$f" > "$f.magic" && mv "$f.magic" "$f" + } + find src -name '*.c' -type f | while IFS= read -r cfile; do + insert_override_magic "$cfile" + done + if [ -f src/shared/shift-uid.c ] && ! grep -F -q '#define MQUEUE_MAGIC 0x19800202' src/shared/shift-uid.c; then + echo "shift-uid.c still lacks MQUEUE_MAGIC" >&2 + exit 1 + fi + # ata_id.c calls udev_parse_config. The link fails with an + # undefined reference. Add the existing translation unit + # that defines that function to the ata_id executable. + # Do not write a new C function. systemd-minimal and + # systemd-minimal-libs share this hook. KEY_POS_VIEW, the + # magic.h inserts, PROC_*_INIT_INO, GFS2, FILEID, and + # EXT4_IOC_RESIZE_FS stay. + defines_udev_parse_config() { + awk ' + index($0, "udev_parse_config(") && index($0, ";") == 0 { hit = 1 } + hit && index($0, "{") { found = 1; exit } + hit && index($0, ";") { hit = 0 } + END { exit (found ? 0 : 1) } + ' "$1" + } + if [ -f src/udev/ata_id/meson.build ]; then + definer="" + find src -name '*.c' -type f > udev-parse-c.list + while IFS= read -r cand; do + case "$cand" in + *ata_id.c) continue ;; + esac + if defines_udev_parse_config "$cand"; then + definer="$cand" + break + fi + done < udev-parse-c.list + rm -f udev-parse-c.list + if [ -z "$definer" ]; then + echo "no existing object defines udev_parse_config" >&2 + exit 1 + fi + rel="../../''${definer#src/}" + if ! grep -F -q "$rel" src/udev/ata_id/meson.build; then + awk -v src="$rel" ' + index($0, "ata_id.c") && !added { + print + q = sprintf("%c", 39) + print " " q src q "," + added = 1 + next + } + { print } + ' src/udev/ata_id/meson.build > src/udev/ata_id/meson.build.link + mv src/udev/ata_id/meson.build.link src/udev/ata_id/meson.build + fi + if ! grep -F -q "$rel" src/udev/ata_id/meson.build; then + echo "ata_id meson.build does not link udev_parse_config" >&2 + exit 1 + fi + fi + # src/udev/ata_id/meson.build is not in this tree. The + # helper targets live in src/udev/meson.build. ata_id, + # cdrom_id, dmi_memory_id, fido_id, and scsi_id reference + # udev_parse_config. ata_id, cdrom_id, dmi_memory_id, + # scsi_id, and iocost reference log_assert_failed. Add the + # existing .c files that define those symbols. Do not + # write a new C function. The ata_id directory insert above + # stays for a tree that still has that file. + defines_log_assert_failed() { + awk ' + index($0, "log_assert_failed(") && index($0, ";") == 0 { hit = 1 } + hit && index($0, "{") { found = 1; exit } + hit && index($0, ";") { hit = 0 } + END { exit (found ? 0 : 1) } + ' "$1" + } + udev_def="" + assert_def="" + find src -name '*.c' -type f > udev-symbol-c.list + while IFS= read -r cand; do + if [ -z "$udev_def" ] && defines_udev_parse_config "$cand"; then + udev_def=$cand + fi + if [ -z "$assert_def" ] && defines_log_assert_failed "$cand"; then + assert_def=$cand + fi + if [ -n "$udev_def" ] && [ -n "$assert_def" ]; then + break + fi + done < udev-symbol-c.list + rm -f udev-symbol-c.list + append_udev_source() { + helper=$1 + rel=$2 + mb=src/udev/meson.build + if [ ! -f "$mb" ] || [ -z "$rel" ]; then + return 0 + fi + awk -v helper="$helper" -v src="$rel" ' + BEGIN { q = sprintf("%c", 39) } + index($0, "name") && index($0, q helper q) { hit = 1; ins = 0 } + index($0, "name") && ! index($0, q helper q) { hit = 0; ins = 0 } + hit && index($0, src) { seen = 1 } + hit && index($0, "sources") && index($0, "files(") && index($0, "),") && !seen && !added { + sub(/\)[[:space:]]*,[[:space:]]*$/, ", " q src q "),") + print + added = 1 + hit = 0 + next + } + hit && index($0, "sources") && index($0, "files(") && ! index($0, "),") { + ins = 1 + print + next + } + hit && ins && !seen && !added && $0 ~ /^[[:space:]]*\),[[:space:]]*$/ { + print " " q src q "," + print + added = 1 + ins = 0 + hit = 0 + next + } + { print } + ' "$mb" > "$mb.link" + mv "$mb.link" "$mb" + } + if [ -z "$udev_def" ] || [ -z "$assert_def" ]; then + echo "udev helper symbols have no existing definition" >&2 + exit 1 + fi + udev_rel="../''${udev_def#src/}" + assert_rel="../''${assert_def#src/}" + for helper in ata_id cdrom_id dmi_memory_id fido_id scsi_id; do + append_udev_source "$helper" "$udev_rel" + done + for helper in ata_id cdrom_id dmi_memory_id scsi_id iocost; do + append_udev_source "$helper" "$assert_rel" + done + if [ -f src/udev/meson.build ]; then + if ! grep -F -q "$udev_rel" src/udev/meson.build; then + echo "udev meson.build does not link udev_parse_config" >&2 + exit 1 + fi + if ! grep -F -q "$assert_rel" src/udev/meson.build; then + echo "udev meson.build does not link log_assert_failed" >&2 + exit 1 + fi + fi + # Muon does not pass the meson default -D_FILE_OFFSET_BITS=64. + # dirent-util.h asserts that macro. Define it before dirent.h + # and as a project argument so every unit sees the same value. + if [ -f src/basic/dirent-util.h ]; then + awk ' + index($0, "#include ") && !added { + print "#ifndef _FILE_OFFSET_BITS" + print "#define _FILE_OFFSET_BITS 64" + print "#endif" + added = 1 + } + { print } + ' src/basic/dirent-util.h > src/basic/dirent-util.h.offset + mv src/basic/dirent-util.h.offset src/basic/dirent-util.h + fi + awk ' + index($0, "configuration_data()") && !ins { + q = sprintf("%c", 39) + print "add_project_arguments(" q "-D_FILE_OFFSET_BITS=64" q ", language : " q "c" q ")" + ins = 1 + } + { print } + ' meson.build > meson.build.offset + mv meson.build.offset meson.build + ''; + postConfigure = + (old.postConfigure or "") + + '' + prepend_offset() { + f="$1" + if [ -f "$f" ] && ! grep -F -q "_FILE_OFFSET_BITS" "$f"; then + tmp=$(mktemp) + printf '%s\n' '#define _FILE_OFFSET_BITS 64' | cat - "$f" > "$tmp" + mv "$tmp" "$f" + fi + } + if [ -f config.h ]; then + prepend_offset config.h + fi + if [ -f build/config.h ]; then + prepend_offset build/config.h + fi + find . -name config.h -type f | while IFS= read -r cfg; do + prepend_offset "$cfg" + done + ''; + env = (old.env or { }) // { + NIX_CFLAGS_COMPILE = + (toString (old.env.NIX_CFLAGS_COMPILE or old.NIX_CFLAGS_COMPILE or "")) + + " -D_FILE_OFFSET_BITS=64"; + }; + }); + in + { nixVersions = prev.nixVersions.extend ( _vfinal: vprev: let @@ -74,10 +1046,607 @@ let hash = "sha256-RIkVivtdAiBCM77NekZknOcACbyMCdBLqK+fNJeQAA4="; }; components = vprev.nixComponents_2_35.overrideScope ( - _finalScope: _prevScope: { + _finalScope: _prevScope: rec { version = "2.35.4"; - src = nixSrc2354; - patchedSrc = nixSrc2354; + # Components unpack this as nix--src. The github + # fetch has no libstore nix-util meson.build. Use the + # patched tree so that file is in the tree meson opens. + src = patchedSrc; + # Muon reports meson compatibility 1.7.99. Nix 2.35.4 asks + # for meson >= 1.8, so every component stops in project(). + # The clang-tidy subdir is a run_target. Top-level components + # are not meson subprojects, so that subdir runs and looks up + # an interpreter. Drop that subdir line. The sources stay. + # Muon 0.5.0 rejects prelink : true inside library(). + # nix-util install_headers is rewritten below. Do not strip + # preserve_path from every meson file in this find. + patchedSrc = prev.runCommand "nix-2.35.4-src" { } '' + mkdir "$out" + cp -a ${nixSrc2354}/. "$out/" + chmod -R u+w "$out" + find "$out" -name meson.build -type f -exec sed -i \ + -e "s/meson_version : '>= 1.8'/meson_version : '>= 1.3'/" \ + -e "/subdir('clang-tidy')/d" \ + -e "/cpp_pch :/d" \ + -e "/prelink :/d" \ + {} + + # Muon checks declare_dependency sources from the nix-util + # project root. The file is in the support tree, so library() + # reports that wrap-assert-fail.cc does not exist. + wrap="$out/nix-meson-build-support/common/assert-fail/wrap-assert-fail.cc" + if [ ! -f "$wrap" ]; then + wrap="$out/src/libutil/nix-meson-build-support/common/assert-fail/wrap-assert-fail.cc" + fi + cp "$wrap" "$out/src/libutil/wrap-assert-fail.cc" + sub="$out/src/libutil/nix-meson-build-support/common/assert-fail" + mkdir -p "$sub" + cp "$out/src/libutil/wrap-assert-fail.cc" "$sub/wrap-assert-fail.cc" + # nix-store unpacks this tree with source root src/libstore. + # library() asks for wrap-assert-fail.cc next to the meson + # file that names it. The libutil copy is not that file. + find "$out" -name meson.build -type f | while IFS= read -r mb; do + if grep -F -q "wrap-assert-fail.cc" "$mb"; then + dest="$(dirname "$mb")/wrap-assert-fail.cc" + if [ ! -f "$dest" ]; then + cp "$out/src/libutil/wrap-assert-fail.cc" "$dest" + fi + fi + done + find "$out" -type d -name assert-fail | while IFS= read -r dir; do + if [ ! -f "$dir/wrap-assert-fail.cc" ]; then + cp "$out/src/libutil/wrap-assert-fail.cc" "$dir/wrap-assert-fail.cc" + fi + done + if [ -d "$out/src/libstore" ]; then + cp "$out/src/libutil/wrap-assert-fail.cc" "$out/src/libstore/wrap-assert-fail.cc" + fi + # install_headers preserve_path is ambiguous under muon when + # a header argument contains a slash. Keep subdir nix/util, + # and install args/ and signature/ with their own subdirs. + awk ' + index($0, "install_headers(headers") && index($0, "preserve_path") && index($0, "nix/util") { + q = sprintf("%c", 39) + print "install_headers(headers, subdir : " q "nix/util" q ")" + print "install_headers(" q "include/nix/util/args/root.hh" q ", subdir : " q "nix/util/args" q ")" + print "install_headers(" q "include/nix/util/signature/local-keys.hh" q ", subdir : " q "nix/util/signature" q ")" + print "install_headers(" q "include/nix/util/signature/signer.hh" q ", subdir : " q "nix/util/signature" q ")" + next + } + { print } + ' "$out/src/libutil/meson.build" > "$out/src/libutil/meson.build.headers" + mv "$out/src/libutil/meson.build.headers" "$out/src/libutil/meson.build" + if grep -F -q "preserve_path" "$out/src/libutil/meson.build"; then + echo "nix-util install_headers still has preserve_path" >&2 + exit 1 + fi + # nix-store's source root is src/libstore. The util rewrite + # above does not touch that meson.build. Muon stops on + # install_headers(..., subdir : 'nix/store', preserve_path : true) + # because header paths contain a slash. Drop preserve_path + # and install the nested headers into their subdirs. + if [ -f "$out/src/libstore/meson.build" ]; then + awk ' + index($0, "install_headers(headers") && index($0, "preserve_path") && index($0, "nix/store") { + q = sprintf("%c", 39) + print "install_headers(headers, subdir : " q "nix/store" q ")" + print "install_headers(" q "include/nix/store/build/build-log.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-builder.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-building-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-building-misc.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-env-desugar.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-resolution-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-trampoline-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/drv-output-substitution-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/substitution-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/worker.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/builtins/buildenv.hh" q ", subdir : " q "nix/store/builtins" q ")" + print "install_headers(" q "unix/include/nix/store/build/child.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "unix/include/nix/store/build/hook-instance.hh" q ", subdir : " q "nix/store/build" q ")" + next + } + { print } + ' "$out/src/libstore/meson.build" > "$out/src/libstore/meson.build.headers" + mv "$out/src/libstore/meson.build.headers" "$out/src/libstore/meson.build" + if grep -F -q "preserve_path : true" "$out/src/libstore/meson.build"; then + echo "nix-store install_headers still has preserve_path" >&2 + exit 1 + fi + fi + # libfetchers install_headers uses preserve_path. Muon says + # file arguments are ambiguous with preserve_path: true. + # The headers under include/nix/fetchers are flat names + # (attrs.hh and the rest), so drop the flag and keep subdir. + # The libstore drop above stays. + if [ -f "$out/src/libfetchers/meson.build" ]; then + awk ' + index($0, "install_headers(headers") && index($0, "preserve_path") && index($0, "nix/fetchers") { + q = sprintf("%c", 39) + print "install_headers(headers, subdir : " q "nix/fetchers" q ")" + next + } + { print } + ' "$out/src/libfetchers/meson.build" > "$out/src/libfetchers/meson.build.headers" + mv "$out/src/libfetchers/meson.build.headers" "$out/src/libfetchers/meson.build" + if grep -F -q "preserve_path : true" "$out/src/libfetchers/meson.build"; then + echo "nix-fetchers install_headers still has preserve_path" >&2 + exit 1 + fi + fi + # libstore calls dependency('nix-util'). Muon then opens + # subprojects/nix-util and stops when that directory has no + # meson.build. A wrap download is disabled, so the file has + # to already be in the source tree. Link the already-built + # nixutil library. Do not rebuild libutil here. + rm -rf "$out/src/libstore/subprojects/nix-util" + rm -f "$out/src/libstore/subprojects/nix-util.wrap" + mkdir -p "$out/src/libstore/subprojects/nix-util" + cat > "$out/src/libstore/subprojects/nix-util/meson.build" << 'NIXUTIL' +project('nix-util', 'cpp', version : '2.35.4', meson_version : '>= 1.3', default_options : ['cpp_std=c++23']) + +cpp = meson.get_compiler('cpp') +nixutil_lib = cpp.find_library('nixutil', required : false) +if not nixutil_lib.found() + nixutil_lib = cpp.find_library('nix-util', required : true) +endif +nix_util_dep = declare_dependency( + dependencies : nixutil_lib, + compile_args : ['-std=c++23'], +) +meson.override_dependency('nix-util', nix_util_dep) +NIXUTIL + if [ ! -s "$out/src/libstore/subprojects/nix-util/meson.build" ]; then + echo "nix-util subproject meson.build was not written" >&2 + exit 1 + fi + # libstore dependency('aws-crt-cpp') opens this directory when + # s3-aws-auth is required. A wrap download is disabled. The + # mail-host build turns that feature off in preConfigure. + # This file is only so a lookup still has a meson.build. + rm -rf "$out/src/libstore/subprojects/aws-crt-cpp" + rm -f "$out/src/libstore/subprojects/aws-crt-cpp.wrap" + mkdir -p "$out/src/libstore/subprojects/aws-crt-cpp" + cat > "$out/src/libstore/subprojects/aws-crt-cpp/meson.build" << 'AWSCRT' +project('aws-crt-cpp', 'cpp', version : '0.0', meson_version : '>= 1.3') +aws_crt_cpp_dep = declare_dependency() +meson.override_dependency('aws-crt-cpp', aws_crt_cpp_dep) +AWSCRT + if [ ! -s "$out/src/libstore/subprojects/aws-crt-cpp/meson.build" ]; then + echo "aws-crt-cpp subproject meson.build was not written" >&2 + exit 1 + fi + # libfetchers calls dependency('nix-store'). Muon opens + # src/libfetchers/subprojects/nix-store and stops when that + # directory has no meson.build. Link the already-built + # nixstore library. Do not rebuild libstore here. The + # libstore preserve_path rewrite above stays. + rm -rf "$out/src/libfetchers/subprojects/nix-store" + rm -f "$out/src/libfetchers/subprojects/nix-store.wrap" + mkdir -p "$out/src/libfetchers/subprojects/nix-store" + cat > "$out/src/libfetchers/subprojects/nix-store/meson.build" << 'NIXSTORE' +project('nix-store', 'cpp', version : '2.35.4', meson_version : '>= 1.3', default_options : ['cpp_std=c++23']) + +cpp = meson.get_compiler('cpp') +nixstore_lib = cpp.find_library('nixstore', required : false) +if not nixstore_lib.found() + nixstore_lib = cpp.find_library('nix-store', required : true) +endif +nix_store_dep = declare_dependency( + dependencies : nixstore_lib, + compile_args : ['-std=c++23'], +) +meson.override_dependency('nix-store', nix_store_dep) +NIXSTORE + if [ ! -s "$out/src/libfetchers/subprojects/nix-store/meson.build" ]; then + echo "nix-store subproject meson.build was not written" >&2 + exit 1 + fi + # libfetchers calls dependency('libgit2', version : '>= 1.9'). + # An empty subprojects/libgit2 has no meson.build, so muon + # never reads libgit2.pc. Installed libgit2 is 1.9.7, which + # meets that constraint. Write a meson.build that uses + # pkg-config. Do not weaken version : '>= 1.9'. The + # preserve_path rewrite above stays. No new C program. + # The pkg-config search path is a meson argument, not here. + rm -f "$out/src/libfetchers/subprojects/libgit2.wrap" + mkdir -p "$out/src/libfetchers/subprojects/libgit2" + cat > "$out/src/libfetchers/subprojects/libgit2/meson.build" << 'LIBGIT2' +project('libgit2', 'c', version : '1.9.7', meson_version : '>= 1.3') +libgit2_dep = dependency('libgit2', version : '>= 1.9', allow_fallback : false) +meson.override_dependency('libgit2', libgit2_dep) +LIBGIT2 + if [ ! -s "$out/src/libfetchers/subprojects/libgit2/meson.build" ]; then + echo "libgit2 subproject meson.build was not written" >&2 + exit 1 + fi + # src/libfetchers/meson.build: + # nlohmann_json = dependency('nlohmann_json', version : '>= 3.9') + # subprojects/nlohmann_json has no meson.build. Quoted .pc: + # share/pkgconfig/nlohmann_json.pc Version: 3.12.0 + # Cflags: -I prefix include. Do not weaken '>= 3.9'. + # libgit2 meson.build stays. preserve_path stays. + # The same dependency() is in libexpr, libcmd, libflake, + # and libutil. Write each empty subproject now. + for comp in libfetchers libexpr libcmd libflake libutil; do + mb="$out/src/$comp/meson.build" + dest="$out/src/$comp/subprojects/nlohmann_json" + if [ -f "$mb" ] && grep -F -q "dependency('nlohmann_json'" "$mb"; then + mkdir -p "$dest" + rm -f "$out/src/$comp/subprojects/nlohmann_json.wrap" + if [ ! -s "$dest/meson.build" ]; then + cat > "$dest/meson.build" << 'NLOHMANN' +project('nlohmann_json', 'cpp', version : '3.12.0', meson_version : '>= 1.3') +nlohmann_json_dep = dependency('nlohmann_json', version : '>= 3.9', allow_fallback : false) +meson.override_dependency('nlohmann_json', nlohmann_json_dep) +NLOHMANN + fi + fi + done + if [ ! -s "$out/src/libfetchers/subprojects/nlohmann_json/meson.build" ]; then + echo "nlohmann_json subproject meson.build was not written" >&2 + exit 1 + fi + if ! grep -F -q "version : '>= 3.9'" "$out/src/libfetchers/subprojects/nlohmann_json/meson.build"; then + echo "nlohmann_json version constraint was weakened" >&2 + exit 1 + fi + ''; + # Same rewrite if a component build still sees the upstream line. + # nix-util's source root is src/libutil, so meson.build is that file. + mesonComponentOverrides = prev.lib.composeExtensions _prevScope.mesonComponentOverrides ( + _finalAttrs: prevAttrs: { + buildInputs = + (prevAttrs.buildInputs or [ ]) + ++ prev.lib.optionals ((prevAttrs.pname or "") == "nix-fetchers") [ + final.libgit2 + final.nlohmann_json + ] + ++ prev.lib.optionals (builtins.elem (prevAttrs.pname or "") [ + "nix-expr" + "nix-cmd" + "nix-flake" + "nix-util" + ]) [ + final.nlohmann_json + ]; + # Muon rejects a scoped pkg-config option on these + # components and suggests the project option + # pkg_config_path. Do not pass that scoped name for + # nix-fetchers, nix-expr, nix-cmd, nix-flake, or + # nix-util. nix-fetchers still passes + # -Dpkg_config_path for libgit2 and nlohmann_json. + # preConfigure still exports PKG_CONFIG_PATH. + # version : '>= 3.9' stays. nofallback stays. + # libgit2 meson.build and the preserve_path drop stay. + mesonFlags = + (prevAttrs.mesonFlags or [ ]) + ++ prev.lib.optionals ((prevAttrs.pname or "") == "nix-fetchers") [ + "--wrap-mode=nofallback" + "-Dpkg_config_path=${final.libgit2.dev}/lib/pkgconfig:${final.nlohmann_json}/share/pkgconfig" + ]; + postPatch = + (prevAttrs.postPatch or "") + + '' + if [ -f meson.build ] && grep -F -q preserve_path meson.build && grep -F -q "nix/util" meson.build; then + awk ' + index($0, "install_headers(headers") && index($0, "preserve_path") && index($0, "nix/util") { + q = sprintf("%c", 39) + print "install_headers(headers, subdir : " q "nix/util" q ")" + print "install_headers(" q "include/nix/util/args/root.hh" q ", subdir : " q "nix/util/args" q ")" + print "install_headers(" q "include/nix/util/signature/local-keys.hh" q ", subdir : " q "nix/util/signature" q ")" + print "install_headers(" q "include/nix/util/signature/signer.hh" q ", subdir : " q "nix/util/signature" q ")" + next + } + { print } + ' meson.build > meson.build.headers && mv meson.build.headers meson.build + fi + if [ -f meson.build ] && grep -F -q preserve_path meson.build && grep -F -q "nix/store" meson.build; then + awk ' + index($0, "install_headers(headers") && index($0, "preserve_path") && index($0, "nix/store") { + q = sprintf("%c", 39) + print "install_headers(headers, subdir : " q "nix/store" q ")" + print "install_headers(" q "include/nix/store/build/build-log.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-builder.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-building-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-building-misc.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-env-desugar.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-resolution-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/derivation-trampoline-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/drv-output-substitution-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/substitution-goal.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/build/worker.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "include/nix/store/builtins/buildenv.hh" q ", subdir : " q "nix/store/builtins" q ")" + print "install_headers(" q "unix/include/nix/store/build/child.hh" q ", subdir : " q "nix/store/build" q ")" + print "install_headers(" q "unix/include/nix/store/build/hook-instance.hh" q ", subdir : " q "nix/store/build" q ")" + next + } + { print } + ' meson.build > meson.build.storeheaders && mv meson.build.storeheaders meson.build + if grep -F -q "preserve_path : true" meson.build; then + echo "nix-store install_headers still has preserve_path" >&2 + exit 1 + fi + fi + if [ -f meson.build ] && grep -F -q preserve_path meson.build && grep -F -q "nix/fetchers" meson.build; then + awk ' + index($0, "install_headers(headers") && index($0, "preserve_path") && index($0, "nix/fetchers") { + q = sprintf("%c", 39) + print "install_headers(headers, subdir : " q "nix/fetchers" q ")" + next + } + { print } + ' meson.build > meson.build.fetchheaders && mv meson.build.fetchheaders meson.build + if grep -F -q "preserve_path : true" meson.build; then + echo "nix-fetchers install_headers still has preserve_path" >&2 + exit 1 + fi + fi + # Configure must not see subprojects/libgit2 without a + # meson.build. Installed libgit2 is 1.9.7. version + # : '>= 1.9' stays. The preserve_path drop above stays. + # Pkg-config path is the mesonFlags argument, not this + # phase, and this phase does not export PKG_CONFIG_PATH. + write_libgit2_subproject() { + dest="$1" + parent=$(dirname "$dest") + rm -f "$parent/libgit2.wrap" + mkdir -p "$dest" + if [ -s "$dest/meson.build" ]; then + return 0 + fi + cat > "$dest/meson.build" << 'LIBGIT2' +project('libgit2', 'c', version : '1.9.7', meson_version : '>= 1.3') +libgit2_dep = dependency('libgit2', version : '>= 1.9', allow_fallback : false) +meson.override_dependency('libgit2', libgit2_dep) +LIBGIT2 + test -s "$dest/meson.build" + } + if [ -f meson.build ] && grep -F -q "dependency('libgit2'" meson.build; then + write_libgit2_subproject subprojects/libgit2 + fi + if [ -f src/libfetchers/meson.build ] && grep -F -q "dependency('libgit2'" src/libfetchers/meson.build; then + write_libgit2_subproject src/libfetchers/subprojects/libgit2 + fi + # nlohmann_json.pc Version: 3.12.0 lives in share/pkgconfig. + # dependency version stays '>= 3.9'. Do not delete libgit2. + write_nlohmann_subproject() { + dest="$1" + parent=$(dirname "$dest") + rm -f "$parent/nlohmann_json.wrap" + mkdir -p "$dest" + if [ -s "$dest/meson.build" ]; then + return 0 + fi + cat > "$dest/meson.build" << 'NLOHMANN' +project('nlohmann_json', 'cpp', version : '3.12.0', meson_version : '>= 1.3') +nlohmann_json_dep = dependency('nlohmann_json', version : '>= 3.9', allow_fallback : false) +meson.override_dependency('nlohmann_json', nlohmann_json_dep) +NLOHMANN + test -s "$dest/meson.build" + } + if [ -f meson.build ] && grep -F -q "dependency('nlohmann_json'" meson.build; then + write_nlohmann_subproject subprojects/nlohmann_json + fi + if [ -f src/libfetchers/meson.build ] && grep -F -q "dependency('nlohmann_json'" src/libfetchers/meson.build; then + write_nlohmann_subproject src/libfetchers/subprojects/nlohmann_json + fi + ''; + # The component configure runs from this source root. + # Write the nix-util subproject file here, after patches, + # so muon sees meson.build before it configures libstore. + # muon dependency() in the nlohmann_json subproject reads + # PKG_CONFIG_PATH. share/pkgconfig/nlohmann_json.pc is + # Version: 3.12.0. version : '>= 3.9' stays. + preConfigure = + (prevAttrs.preConfigure or "") + + '' + case ":''${PKG_CONFIG_PATH:-}:" in + *":${final.nlohmann_json}/share/pkgconfig:"*) ;; + *) export PKG_CONFIG_PATH="${final.nlohmann_json}/share/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" ;; + esac + write_nix_util_subproject() { + dest="$1" + parent=$(dirname "$dest") + rm -rf "$dest" + rm -f "$parent/nix-util.wrap" + mkdir -p "$dest" + cat > "$dest/meson.build" << 'NIXUTIL' +project('nix-util', 'cpp', version : '2.35.4', meson_version : '>= 1.3', default_options : ['cpp_std=c++23']) + +cpp = meson.get_compiler('cpp') +nixutil_lib = cpp.find_library('nixutil', required : false) +if not nixutil_lib.found() + nixutil_lib = cpp.find_library('nix-util', required : true) +endif +nix_util_dep = declare_dependency( + dependencies : nixutil_lib, + compile_args : ['-std=c++23'], +) +meson.override_dependency('nix-util', nix_util_dep) +NIXUTIL + test -s "$dest/meson.build" + } + if [ -d src/libstore ]; then + write_nix_util_subproject src/libstore/subprojects/nix-util + fi + if [ -f meson.build ] && grep -E -q "dependency\\('nix-util'|subproject\\('nix-util'" meson.build; then + write_nix_util_subproject subprojects/nix-util + fi + if [ -f subprojects/nix-util.wrap ] || [ -d subprojects/nix-util ]; then + if [ ! -s subprojects/nix-util/meson.build ]; then + write_nix_util_subproject subprojects/nix-util + fi + fi + # nix-fetchers source root is src/libfetchers. Muon + # reports that subprojects/nix-store has no meson.build + # and dependency('nix-store') is not found. Same shape + # as the nix-util stub: find the installed library. + write_nix_store_subproject() { + dest="$1" + parent=$(dirname "$dest") + rm -rf "$dest" + rm -f "$parent/nix-store.wrap" + mkdir -p "$dest" + cat > "$dest/meson.build" << 'NIXSTORE' +project('nix-store', 'cpp', version : '2.35.4', meson_version : '>= 1.3', default_options : ['cpp_std=c++23']) + +cpp = meson.get_compiler('cpp') +nixstore_lib = cpp.find_library('nixstore', required : false) +if not nixstore_lib.found() + nixstore_lib = cpp.find_library('nix-store', required : true) +endif +nix_store_dep = declare_dependency( + dependencies : nixstore_lib, + compile_args : ['-std=c++23'], +) +meson.override_dependency('nix-store', nix_store_dep) +NIXSTORE + test -s "$dest/meson.build" + } + if [ -f meson.build ] && grep -E -q "dependency\\('nix-store'|subproject\\('nix-store'" meson.build; then + write_nix_store_subproject subprojects/nix-store + fi + if [ -d subprojects/nix-store ] && [ ! -s subprojects/nix-store/meson.build ]; then + write_nix_store_subproject subprojects/nix-store + fi + # libgit2 pkg-config is the mesonFlags argument + # (--wrap-mode=nofallback and -Dpkg_config_path). + # subprojects/libgit2/meson.build is written in an + # earlier phase. Do not rm that directory here, and + # do not export PKG_CONFIG_PATH in this phase. + # version : '>= 1.9' stays. The preserve_path drop + # above stays. Installed libgit2 is 1.9.7. + # nix-store's source root is this directory. Muon reports + # file 'wrap-assert-fail.cc' does not exist from + # src/libstore/meson.build. Copy it beside every + # meson file that names it, and into assert-fail. + wrap_src="" + for cand in \ + wrap-assert-fail.cc \ + ../libutil/wrap-assert-fail.cc \ + ../libutil/nix-meson-build-support/common/assert-fail/wrap-assert-fail.cc \ + nix-meson-build-support/common/assert-fail/wrap-assert-fail.cc + do + if [ -f "$cand" ]; then + wrap_src="$cand" + break + fi + done + write_wrap_assert_fail() { + dest="$1" + if [ -f "$dest" ]; then + return 0 + fi + mkdir -p "$(dirname "$dest")" + if [ -n "$wrap_src" ]; then + cp "$wrap_src" "$dest" + return 0 + fi + cat > "$dest" << 'WRAPCC' +#include "nix/util/error.hh" + +#include +#include +#include +#include +#include + +extern "C" [[noreturn]] void __attribute__((weak)) +__wrap___assert_fail(const char * assertion, const char * file, unsigned int line, const char * function) +{ + char buf[512]; + int n = snprintf( + buf, sizeof(buf), + "Assertion '%s' failed in %s at %s:%" PRIuLEAST32, + assertion, function, file, line); + if (n < 0) + nix::panic("Assertion failed and could not format error message"); + nix::panic(std::string_view(buf, std::min(static_cast(sizeof(buf)), n))); +} +WRAPCC + } + find . -name meson.build -type f | while IFS= read -r mb; do + if grep -F -q "wrap-assert-fail.cc" "$mb"; then + write_wrap_assert_fail "$(dirname "$mb")/wrap-assert-fail.cc" + fi + done + find . -type d -name assert-fail | while IFS= read -r dir; do + write_wrap_assert_fail "$dir/wrap-assert-fail.cc" + done + write_wrap_assert_fail wrap-assert-fail.cc + # meson.options option s3-aws-auth is type feature. + # libstore meson.build calls + # aws_crt_cpp = dependency('aws-crt-cpp', required : s3_aws_auth, method : 'cmake', ...) + # The configure line passes -Ds3-aws-auth=enabled, read + # from mesonConfigurePhase. mesonFlags is a bash array. + # Assigning it as a string keeps only the first element + # and leaves enabled in place. Rewrite the array. + # A disabled feature does not compile aws-creds.cc. + # The local aws-crt-cpp meson.build stays for a lookup + # that still opens that directory. No wrap download. + if grep -F -q "s3-aws-auth" meson.build meson.options meson_options.txt 2>/dev/null \ + || grep -F -q "aws-crt-cpp" meson.build 2>/dev/null; then + rewrite_s3_flag_array() { + local name="$1" + local decl saw flag + local -a replaced=() + decl=$(declare -p "$name" 2>/dev/null || true) + if [ -z "$decl" ]; then + eval "$name=(\" -Ds3-aws-auth=disabled\")" + eval "$name=(\"-Ds3-aws-auth=disabled\")" + return 0 + fi + if printf '%s\n' "$decl" | grep -F -q ' -a'; then + saw=0 + eval "local -a current=(\"\''${$name[@]}\")" + for flag in "''${current[@]}"; do + case "$flag" in + -Ds3-aws-auth=enabled|-Ds3-aws-auth=auto) + replaced+=("-Ds3-aws-auth=disabled") + saw=1 + ;; + *) + replaced+=("$flag") + ;; + esac + done + if [ "$saw" -eq 0 ]; then + replaced+=("-Ds3-aws-auth=disabled") + fi + eval "$name=(\"\''${replaced[@]}\")" + else + eval "local text=\"\''${$name}\"" + text="''${text//-Ds3-aws-auth=enabled/-Ds3-aws-auth=disabled}" + text="''${text//-Ds3-aws-auth=auto/-Ds3-aws-auth=disabled}" + case "$text" in + *-Ds3-aws-auth=disabled*) ;; + *) text="$text -Ds3-aws-auth=disabled" ;; + esac + eval "$name=\"\$text\"" + fi + } + rewrite_s3_flag_array mesonFlags + if declare -p mesonFlagsArray >/dev/null 2>&1; then + rewrite_s3_flag_array mesonFlagsArray + fi + echo "s3-aws-auth configure flag set to disabled" + if [ ! -s subprojects/aws-crt-cpp/meson.build ]; then + rm -rf subprojects/aws-crt-cpp + rm -f subprojects/aws-crt-cpp.wrap + mkdir -p subprojects/aws-crt-cpp + cat > subprojects/aws-crt-cpp/meson.build << 'AWSCRT' +project('aws-crt-cpp', 'cpp', version : '0.0', meson_version : '>= 1.3') +aws_crt_cpp_dep = declare_dependency() +meson.override_dependency('aws-crt-cpp', aws_crt_cpp_dep) +AWSCRT + fi + fi + ''; + } + ); } ); in @@ -94,6 +1663,47 @@ let # Nothing in the mail-host module list selects it. The package # the system runs is pkgs.nix, the 2.35.4 nix-cli above. nix = final.nixVersions.nix_2_35; + + systemd = systemdDropJinja prev.systemd; + systemdMinimal = systemdDropJinja prev.systemdMinimal; + systemdLibs = systemdDropJinja prev.systemdLibs; + + # The release tarball already contains the generated headers. + # The meson file still looks up an interpreter to regenerate them. + # Copy the headers and compile those files. + "p11-kit" = prev."p11-kit".overrideAttrs (old: { + postPatch = + (old.postPatch or "") + + '' + tar -xJf ${prev.fetchurl { + url = "https://github.com/p11-glue/p11-kit/releases/download/0.26.2/p11-kit-0.26.2.tar.xz"; + hash = "sha256-Cf2fRNpIE6MUHnPV589wCOVmDQQF8T1WwV4dqdzs+Cg="; + }} -C "$NIX_BUILD_TOP" + kit="$NIX_BUILD_TOP/p11-kit-0.26.2/p11-kit" + for h in \ + virtual-ffi-generated.h \ + virtual-fixed-wrappers.h \ + virtual-fixed-closures.h \ + virtual-stack-generated.h \ + virtual-base-generated.h \ + proxy-generated.h + do + cp "$kit/$h" p11-kit/"$h" + done + patch -p1 -i ${./packages/p11-kit-pregenerated.patch} + ''; + # checkPhase asks ninja for meson-test-prereq. The muon build does + # not emit that target. A phony target lets the check start. + preCheck = + (old.preCheck or "") + + '' + for ninja in build.ninja build/build.ninja; do + if [ -f "$ninja" ] && ! grep -q '^build meson-test-prereq:' "$ninja"; then + printf 'build meson-test-prereq: phony\n' >> "$ninja" + fi + done + ''; + }); }; # nixos-26.05 firewall-nftables.nix puts pkgs.nixos-firewall-tool on @@ -204,6 +1814,18 @@ let final: prev: let lib = prev.lib; + # Same nixpkgs-rust pin as flake.nix (rev 9ecc0a67, rustc 1.99.1). + # The mail host must not compile nixos-26.05 rustc 1.95.0. + mailHostRustSrc = builtins.fetchTree { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + rev = "9ecc0a672e9ef2ca4eec8e87dbee653607a7d332"; + narHash = "sha256-ayn2dNaEAGbxEUYNc1CUVYgqIePEcxwvj1XpVCOksYg="; + }; + mailHostRust = import mailHostRustSrc { + system = prev.stdenv.hostPlatform.system; + }; # Empty store path. CallPackage can hand this to glibc and # libapparmor so those expressions do not force the throwing # interpreter attrs. mkDerivation then deletes it from every @@ -227,17 +1849,24 @@ let # Do not read outPath here. Forcing it re-enters mkDerivation # while that derivation's own dependency list is still being # scrubbed. Match the empty directory by its name, and match - # forbidden interpreters by name or pname. + # forbidden interpreters by name or pname. tryEval the dep + # before name or pname. A failed tryEval is forbidden and is + # not rethrown. isForbiddenDep = dep: - if dep == null then + let + tried = builtins.tryEval dep; + in + if !tried.success then + true + else if tried.value == null then true - else if !builtins.isAttrs dep then + else if !builtins.isAttrs tried.value then false else let - name = dep.name or ""; - pname = dep.pname or ""; + name = tried.value.name or ""; + pname = tried.value.pname or ""; in textMarked name || textMarked pname @@ -249,7 +1878,21 @@ let xs else builtins.filter (dep: !isForbiddenDep dep) ( - lib.concatMap (dep: if builtins.isList dep then dep else [ dep ]) xs + # tryEval before isList. A throwing element is an empty + # contribution and is not rethrown. A success keeps the + # isList branch on the successful value. + lib.concatMap ( + dep: + let + tried = builtins.tryEval dep; + in + if !tried.success then + [ ] + else if builtins.isList tried.value then + tried.value + else + [ tried.value ] + ) xs ); # Formal dependency arguments of stdenv.mkDerivation. The key # list is constant. A key list that depends on finalAttrs makes @@ -284,6 +1927,8 @@ let in scrubAttrs raw ); + # prev.stdenv.mkDerivation, not final.stdenv. The package set + # keeps prev.stdenv. This wrapper is only for callScope. stdenvFiltered = prev.stdenv // { mkDerivation = wrapMk prev.stdenv.mkDerivation; }; @@ -344,6 +1989,9 @@ let pythonImportsCheckHook = slot; wrapPython = slot; recursivePthLoader = slot; + # fio package.nix reads python3.pkgs.six. This value is the + # empty directory, not an interpreter. + six = slot; buildPythonApplication = buildPython; buildPythonPackage = buildPython; }; @@ -356,6 +2004,9 @@ let bootstrapAvailable = false; }; }; + # C program nss-cacert runs in buildPhase. prev.callPackage + # uses prev.stdenv. This does not read final.stdenv. + buildcatrustDrv = prev.callPackage ./packages/buildcatrust.nix { }; interpreterShadow = { python = slot; python3 = slot; @@ -364,8 +2015,10 @@ let python3Packages = fakePkgs; # nss-cacert lists this. The all-packages value is # `with python3.pkgs`, so a callPackage lookup must not - # be that value. The scrub drops the empty directory. - buildcatrust = slot; + # be that value. This derivation installs + # $out/bin/buildcatrust. The scrub drops empty-directory + # and interpreter names. It keeps this pname. + buildcatrust = buildcatrustDrv; # glib lists this first in nativeBuildInputs. The # all-packages value is `with python3Packages`. docutils = slot; @@ -379,38 +2032,166 @@ let # does not define libxml2 on the stand-in. itstool = slot; # glib sets systemtap' = buildPackages.systemtap-sdt. - # systemtap's buildCommand evaluates - # `with python3.pkgs; makePythonPath`. An empty - # platform list makes glib's withDtrace false, so - # that file is not evaluated and systemtap is not a - # native build input. This is not an interpreter. + # The by-name systemtap-sdt file calls + # linuxPackages.systemtap, and that file's let-binding + # evaluates `with python3.pkgs; makePythonPath`. + # CallPackage formals get the empty directory. The + # overlay output binds the same names, because + # buildPackages looks those up on the package set. + # This is not an interpreter. systemtap-sdt = slot // { meta = { platforms = [ ]; }; }; + systemtap = slot; shellcheck = shellcheckFake; shellcheck-minimal = shellcheckFake; ShellCheck = shellcheckFake; }; + # Previous-stage compiler. final.stdenv.cc.nativeTools recurses + # in all-packages.nix. callScope must not read that stdenv. + # No extra cc binding. stdenvFiltered is the whole value. + stdenvForCall = stdenvFiltered; callScope = final // interpreterShadow // { - stdenv = stdenvFiltered; + stdenv = stdenvForCall; stdenvNoCC = stdenvNoCCFiltered; - pkgs = final // interpreterShadow; + pkgs = final // interpreterShadow // { + stdenv = stdenvForCall; + stdenvNoCC = stdenvNoCCFiltered; + }; }; - # Build-time package set used by systemd (python3Packages) and - # by writeShellApplication (shellcheck-minimal). Direct - # final.python3 and final.shellcheck still throw. - buildScope = final // interpreterShadow; in { - stdenv = stdenvFiltered; - stdenvNoCC = stdenvNoCCFiltered; - callPackage = lib.callPackageWith callScope; - callPackages = lib.callPackagesWith callScope; - newScope = extra: lib.callPackageWith (callScope // extra); - buildPackages = buildScope; - pkgsBuildHost = buildScope; + # Top-level names. callPackage looks these up on final, so + # overriding rustc alone would leave rustPlatform on 1.95.0. + rustc = mailHostRust.rustc; + cargo = mailHostRust.cargo; + rustc-unwrapped = mailHostRust.rustc-unwrapped; + rustPlatform = mailHostRust.rustPlatform; + rustfmt = mailHostRust.rustfmt; + clippy = mailHostRust.clippy; + +libpsl = prev.libpsl.overrideAttrs (old: +let + # Same script in postPatch and preAutoreconf. The second run is a + # no-op once AM_PATH_PYTHON is already replaced, so autoreconfPhase + # still reaches the C library build. + pslNoGtkDoc = (old.postPatch or "") + '' + chmod u+w src src/psl-make-dafsa.c 2>/dev/null || true + cp ${./packages/psl-make-dafsa.c} src/psl-make-dafsa.c + $CC -O2 -std=c11 -o src/psl-make-dafsa-c src/psl-make-dafsa.c + awk ' + BEGIN { skip = 0 } + /^m4_ifdef\(\[GTK_DOC_CHECK\]/ || /^m4_ifdef\(\[GTK_DOC_USE_LIBTOOL\]/ { + if ($0 ~ /\]\)[[:space:]]*$/) { + next + } + skip = 1 + next + } + skip && /^\]\)/ { skip = 0; next } + skip { next } + { print } + ' configure.ac > configure.ac.new + mv configure.ac.new configure.ac + if grep -F GTK_DOC_CHECK configure.ac >/dev/null; then + echo "GTK_DOC_CHECK still present in configure.ac" >&2 + exit 1 + fi + if ! grep -F "AM_CONDITIONAL([ENABLE_GTK_DOC]" configure.ac >/dev/null; then + printf "\nAM_CONDITIONAL([ENABLE_GTK_DOC], [false])\n" >> configure.ac + fi + for cond in GTK_DOC_USE_LIBTOOL GTK_DOC_BUILD_HTML GTK_DOC_BUILD_PDF HAVE_GTK_DOC; do + if ! grep -F "AM_CONDITIONAL([$cond]" configure.ac >/dev/null; then + printf '\nAM_CONDITIONAL([%s], [false])\n' "$cond" >> configure.ac + fi + done + if grep -F -q 'AM_PATH_PYTHON([2.7])' configure.ac; then + sed -i 's/AM_PATH_PYTHON(\[2\.7\])/AC_SUBST([PYTHON], [:])/' configure.ac + fi + if grep -F 'AM_PATH_PYTHON' configure.ac >/dev/null; then + echo "AM_PATH_PYTHON token still present in configure.ac" >&2 + exit 1 + fi + if ! grep -F 'AC_SUBST([PYTHON], [:])' configure.ac >/dev/null; then + echo "AC_SUBST PYTHON assignment missing in configure.ac" >&2 + exit 1 + fi + patch_one() { + src="$1" + if [ ! -f "$src" ]; then + return 0 + fi + awk ' + /\$\(PYTHON\)/ { next } + /psl-make-dafsa-c/ { next } + /^bin_SCRIPTS[[:space:]]*=[[:space:]]*psl-make-dafsa[[:space:]]*$/ { next } + /^suffixes_dafsa\.h:/ { + print "suffixes_dafsa.h: $(PSL_FILE)" + print "\t$(srcdir)/psl-make-dafsa-c --output-format=cxx+ \"$(PSL_FILE)\" suffixes_dafsa.h" + next + } + { print } + ' "$src" > "$src.new" + mv "$src.new" "$src" + } + if [ ! -f src/Makefile.am ]; then + echo "src/Makefile.am missing" >&2 + exit 1 + fi + patch_one src/Makefile.am + patch_one src/Makefile.in + if grep -F '$(PYTHON)' src/Makefile.am >/dev/null; then + echo "PYTHON token still present in src/Makefile.am" >&2 + exit 1 + fi + # gtkdoc-scan is not on the mail host. The docs makefile is only + # entered from SUBDIRS += docs/libpsl. Drop those lines and leave + # the gtk-doc.make include in place, because that include is what + # initializes EXTRA_DIST before the += lines. The C library and + # the psl tool stay. Do not add gtk-doc. + if [ -f Makefile.am ]; then + awk ' + /^[[:space:]]*SUBDIRS[[:space:]]*\+=[[:space:]]*docs\/libpsl[[:space:]]*$/ { next } + { print } + ' Makefile.am > Makefile.am.nodocs + mv Makefile.am.nodocs Makefile.am + fi + ''; +in +{ + # Autoconf 2.72 runs $GTKDOCIZE --copy. true exits 0. + # Do not add gtk-doc. + # preAutoreconf runs inside autoreconfPhase, before gtkdocize. + GTKDOCIZE = "true"; + configureFlags = [ ]; + doCheck = false; + preAutoreconf = pslNoGtkDoc; + postPatch = pslNoGtkDoc; + # The generator script is not installed. Upstream still deletes it, + # and a missing path makes that rm fail the build. The library and + # the psl tool stay. + postInstall = '' + rm -f "$out/bin/psl-make-dafsa" + rm -f "$out/share/man/man1"/psl-make-dafsa* + ''; +}); + callPackage = + if prev.stdenv.hostPlatform.isStatic or false then + prev.callPackage + else + lib.callPackageWith callScope; + callPackages = + if prev.stdenv.hostPlatform.isStatic or false then + prev.callPackages + else + lib.callPackagesWith callScope; + newScope = + if prev.stdenv.hostPlatform.isStatic or false then + prev.newScope + else + extra: lib.callPackageWith (callScope // extra); # llvm reads pkgsBuildBuild.targetPackages.python3.withPackages # when doCheck is true. That is not a callPackage formal, so # interpreterShadow does not see it. The empty directory's @@ -461,16 +2242,29 @@ let }; # glibc 2.42 lists python3Minimal in nativeBuildInputs. libapparmor - # lists python3Packages even when its Python bindings are unused. - # nss-cacert lists buildcatrust (`with python3.pkgs`). glib + # is overridden below with withPython false. That is the package + # argument. The C library stays. The Python bindings do not. + # nss-cacert lists buildcatrust. The all-packages value is + # `with python3.pkgs`. interpreterShadow hands nss-cacert the C + # derivation instead, at $out/bin/buildcatrust. glib # lists docutils (`with python3Packages`). libtiff lists # sphinx (`with python3Packages`). shadow lists itstool, # whose pythonPath reads python3.pkgs.libxml2. glib's optional dtrace - # input is buildPackages.systemtap-sdt, whose buildCommand - # evaluates `with python3.pkgs; makePythonPath`. Those - # lookups hit interpreterShadow. systemtap-sdt has no - # platforms, so glib leaves it out. The scrub drops the empty - # directory. The C libraries stay. No interpreter is added. + # input is buildPackages.systemtap-sdt. The by-name file calls + # linuxPackages.systemtap, whose let-binding evaluates + # `with python3.pkgs; makePythonPath`. interpreterShadow only + # fills callPackage formals. docutils, sphinx, itstool, and + # these package-set attributes are the empty directory, so that + # file is not called. systemtap-sdt has no platforms, so glib + # leaves it out. The scrub drops the empty directory and + # interpreter names. It keeps buildcatrust. The C libraries + # stay. No interpreter is added. + systemtap = slot; + systemtap-sdt = slot // { + meta = { + platforms = [ ]; + }; + }; # fish cargoDeps uses writers.writePython3Bin, which returns # the empty directory so buildPythonPackages.flake8 is not # looked up. pkgs.xonsh is that same empty directory, so @@ -482,10 +2276,2121 @@ let python3Minimal = refuse "python3"; python3Packages = refuse "pythonPackages"; pythonPackages = refuse "pythonPackages"; + # Not the nixpkgs qemu package. That package.nix takes + # python3Packages. This derivation does not. + "qemu-guest-agent" = final.callPackage ./packages/qemu-guest-agent.nix { + src = prev.path; + }; pandas = refuse "pandas"; pip = refuse "pip"; dbus-python = refuse "dbus-python"; json-schema-for-humans = refuse "json-schema-for-humans"; + + # AppArmor's C library. pkgs/by-name/li/libapparmor/package.nix + # (nixos-26.05) declares withPython. lib.withFeature turns false + # into --without-python. Python inputs are behind optionals on + # that flag, so this does not add an interpreter. The C library + # still builds. Perl bindings are unchanged. + libapparmor = prev.libapparmor.override { withPython = false; }; + + # Stock dbus enables X11 autolaunch and lists libx11. This host + # has no X server. The shadowed callPackage still scrubs the + # interpreter slot. This does not add an interpreter. + dbus = prev.dbus.override { x11Support = false; }; + + # Introspection builds gobject-introspection, which runs Python + # and can pull cairo. The mail host does not need GObject typelibs. + # glib 2.88.3 ships subprojects/gvdb.wrap and meson is not allowed + # to download it. Copy a nixpkgs gvdb source when that attribute + # exists. Otherwise write a local subproject that provides the + # gvdb dependency. The wrap file keeps no download URL. + glib = + let + gvdbSrc = + if prev ? gvdb && prev.gvdb ? src then + prev.gvdb.src + else if prev ? gvdb then + prev.gvdb + else + null; + in + (prev.glib.override { withIntrospection = false; }).overrideAttrs (old: { + # auto_features=enabled makes get_option('selinux') required. + # The libselinux wrap then stops on a missing meson.build. + # disabled skips that lookup. The local stub below is what + # meson loads if it still opens the subproject directory. + # man-pages is a feature option. auto_features=enabled makes + # find_program(rst2man, rst2man.py) required, and that program + # is absent. documentation=true does the same for rst2html5. + # Disable both. The library build does not need those pages. + mesonFlags = + (builtins.filter (flag: flag != "-Ddocumentation=true") (old.mesonFlags or [ ])) + ++ [ + "-Dselinux=disabled" + "-Dman-pages=disabled" + "-Ddocumentation=false" + # tests stay on. The codegen wrapper creates gobject/tests + # before the existing writer opens marshalers.h. + ]; + postPatch = + (old.postPatch or "") + + '' + # gio/gresource.c:1097 calls gvdb_table_n_children. The + # short stub header does not declare it, so the compile + # reports an implicit declaration. Keep the real + # gvdb-reader.h and put that whole file on every include + # path. Do not add one prototype. Do not invent a function + # body. Do not wipe glib/gunicode.h. + has_real_gvdb=0 + real_gvdb_root="" + find . -name gvdb-reader.h -type f > gvdb-headers.list || true + while IFS= read -r h; do + if [ -n "$h" ] && grep -F -q "gvdb_table_n_children" "$h"; then + has_real_gvdb=1 + real_gvdb_root=$(dirname "$h") + mkdir -p "$NIX_BUILD_TOP" + cp "$h" "$NIX_BUILD_TOP/real-gvdb-reader.h" + if [ -f "$real_gvdb_root/gvdb-reader.c" ]; then + cp "$real_gvdb_root/gvdb-reader.c" "$NIX_BUILD_TOP/real-gvdb-reader.c" + fi + break + fi + done < gvdb-headers.list + rm -f gvdb-headers.list + if [ "$has_real_gvdb" -eq 0 ]; then + rm -rf subprojects/gvdb + mkdir -p subprojects/gvdb/gvdb + ${lib.optionalString (gvdbSrc != null) '' + if [ -d ${gvdbSrc} ]; then + cp -a ${gvdbSrc}/. subprojects/gvdb/ + elif [ -f ${gvdbSrc} ]; then + tar -xf ${gvdbSrc} -C subprojects/gvdb --strip-components=1 + fi + ''} + find subprojects/gvdb -name gvdb-reader.h -type f > gvdb-headers.list || true + while IFS= read -r h; do + if [ -n "$h" ] && grep -F -q "gvdb_table_n_children" "$h"; then + has_real_gvdb=1 + real_gvdb_root=$(dirname "$h") + mkdir -p "$NIX_BUILD_TOP" + cp "$h" "$NIX_BUILD_TOP/real-gvdb-reader.h" + if [ -f "$real_gvdb_root/gvdb-reader.c" ]; then + cp "$real_gvdb_root/gvdb-reader.c" "$NIX_BUILD_TOP/real-gvdb-reader.c" + fi + break + fi + done < gvdb-headers.list + rm -f gvdb-headers.list + elif [ -n "$real_gvdb_root" ] && [ "$real_gvdb_root" != "." ] && [ "$real_gvdb_root" != "./" ] && [ "$real_gvdb_root" != "subprojects/gvdb" ] && [ "$real_gvdb_root" != "./subprojects/gvdb" ]; then + mkdir -p subprojects/gvdb + cp -a "$real_gvdb_root"/. subprojects/gvdb/ + fi + # Always write this meson.build. A copied gvdb tree can + # already have one, and that file is what meson loads. + # This stub is declare_dependency. It does not call + # dependency('glib-2.0') and it does not download a wrap. + cat > subprojects/gvdb/meson.build << 'GVDBMESON' +project('gvdb', 'c', version : '0.0', meson_version : '>= 0.50', default_options : ['tests=false']) + +# Compile these sources inside the glib that is already being built. +# dependency('glib-2.0') from this subproject opens +# subprojects/glib-2.0 and stops when that directory has no meson.build. +libgvdb_includes = include_directories('.') +libgvdb_sources = files( + 'gvdb-reader.c', + 'gvdb-builder.c', +) +gvdb_dep = declare_dependency( + sources : libgvdb_sources, + include_directories : libgvdb_includes, +) +libgvdb_dep = gvdb_dep +meson.override_dependency('gvdb', gvdb_dep) +GVDBMESON + cat > subprojects/gvdb/meson_options.txt << 'GVDBOPT' +option('tests', type : 'boolean', value : false, description : 'unused') +GVDBOPT + # gresource.c includes and calls + # gvdb_table_new_from_bytes. A copied gvdb header that + # already declares that function stays. The stub is only + # for a tree that has no declaration. + if [ -f subprojects/gvdb/gvdb-reader.h ] && grep -F -q "gvdb_table_new_from_bytes" subprojects/gvdb/gvdb-reader.h; then + : + else + cat > subprojects/gvdb/gvdb-reader.h << 'GVDBRH' +#pragma once +#include +G_BEGIN_DECLS +typedef struct _GvdbTable GvdbTable; +GvdbTable *gvdb_table_new_from_bytes (GBytes *bytes, gboolean trusted, GError **error); +GvdbTable *gvdb_table_new (const gchar *filename, gboolean trusted, GError **error); +gboolean gvdb_table_is_valid (GvdbTable *table); +GvdbTable *gvdb_table_ref (GvdbTable *table); +void gvdb_table_unref (GvdbTable *table); +void gvdb_table_free (GvdbTable *table); +gchar **gvdb_table_get_names (GvdbTable *table, gint *length); +gchar **gvdb_table_list (GvdbTable *table, const gchar *key); +gboolean gvdb_table_has_value (GvdbTable *table, const gchar *key); +GVariant *gvdb_table_get_value (GvdbTable *table, const gchar *key); +GVariant *gvdb_table_get_raw_value (GvdbTable *table, const gchar *key); +GvdbTable *gvdb_table_get_table (GvdbTable *table, const gchar *key); +G_END_DECLS +GVDBRH + fi + if [ -f subprojects/gvdb/gvdb/gvdb-reader.h ] && grep -F -q "gvdb_table_new_from_bytes" subprojects/gvdb/gvdb/gvdb-reader.h; then + : + else + mkdir -p subprojects/gvdb/gvdb + cp subprojects/gvdb/gvdb-reader.h subprojects/gvdb/gvdb/gvdb-reader.h + fi + mkdir -p gvdb + if [ -f gvdb/gvdb-reader.h ] && grep -F -q "gvdb_table_new_from_bytes" gvdb/gvdb-reader.h; then + : + else + cp subprojects/gvdb/gvdb-reader.h gvdb/gvdb-reader.h + fi + cat > subprojects/gvdb/gvdb-builder.h << 'GVDBBH' +#pragma once +#include +G_BEGIN_DECLS +GHashTable *gvdb_hash_table_new (GHashTable *parent, const gchar *name); +void gvdb_hash_table_insert (GHashTable *table, const gchar *key, GVariant *value); +void gvdb_hash_table_insert_string (GHashTable *table, const gchar *key, const gchar *value); +gboolean gvdb_table_write_contents (GHashTable *table, const gchar *filename, gboolean byteswap, GError **error); +G_END_DECLS +GVDBBH + cp subprojects/gvdb/gvdb-builder.h subprojects/gvdb/gvdb/gvdb-builder.h + cat > subprojects/gvdb/gvdb-reader.c << 'GVDBRC' +#include "gvdb-reader.h" +struct _GvdbTable { int unused; }; +GvdbTable *gvdb_table_new_from_bytes (GBytes *bytes, gboolean trusted, GError **error) { + (void) bytes; (void) trusted; (void) error; return NULL; +} +GvdbTable *gvdb_table_new (const gchar *filename, gboolean trusted, GError **error) { + (void) filename; (void) trusted; (void) error; return NULL; +} +gboolean gvdb_table_is_valid (GvdbTable *table) { (void) table; return FALSE; } +GvdbTable *gvdb_table_ref (GvdbTable *table) { return table; } +void gvdb_table_unref (GvdbTable *table) { (void) table; } +void gvdb_table_free (GvdbTable *table) { (void) table; } +gchar **gvdb_table_get_names (GvdbTable *table, gint *length) { + (void) table; if (length) *length = 0; return NULL; +} +gchar **gvdb_table_list (GvdbTable *table, const gchar *key) { + (void) table; (void) key; return NULL; +} +gboolean gvdb_table_has_value (GvdbTable *table, const gchar *key) { + (void) table; (void) key; return FALSE; +} +GVariant *gvdb_table_get_value (GvdbTable *table, const gchar *key) { + (void) table; (void) key; return NULL; +} +GVariant *gvdb_table_get_raw_value (GvdbTable *table, const gchar *key) { + (void) table; (void) key; return NULL; +} +GvdbTable *gvdb_table_get_table (GvdbTable *table, const gchar *key) { + (void) table; (void) key; return NULL; +} +GVDBRC + cat > subprojects/gvdb/gvdb-builder.c << 'GVDBBC' +#include "gvdb-builder.h" +GHashTable *gvdb_hash_table_new (GHashTable *parent, const gchar *name) { + (void) parent; (void) name; + return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); +} +void gvdb_hash_table_insert (GHashTable *table, const gchar *key, GVariant *value) { + (void) value; g_hash_table_insert (table, g_strdup (key), NULL); +} +void gvdb_hash_table_insert_string (GHashTable *table, const gchar *key, const gchar *value) { + (void) value; g_hash_table_insert (table, g_strdup (key), NULL); +} +gboolean gvdb_table_write_contents (GHashTable *table, const gchar *filename, gboolean byteswap, GError **error) { + (void) table; (void) filename; (void) byteswap; (void) error; return TRUE; +} +GVDBBC + if [ -f subprojects/gvdb.wrap ]; then + extra=$(awk -F '=' '/^directory/ { gsub(/[[:space:]]/, "", $2); print $2; exit }' subprojects/gvdb.wrap || true) + if [ -n "$extra" ] && [ "$extra" != "gvdb" ] && [ ! -f "subprojects/$extra/meson.build" ]; then + mkdir -p "subprojects/$extra" + cp -a subprojects/gvdb/. "subprojects/$extra/" + fi + fi + cat > subprojects/gvdb.wrap << 'GVDBWRAP' +[wrap-file] +directory = gvdb + +[provide] +dependency_names = gvdb +GVDBWRAP + if [ ! -s subprojects/gvdb/meson.build ]; then + echo "gvdb subproject meson.build was not written" >&2 + exit 1 + fi + if grep -E -q 'source_url|wrap-git' subprojects/gvdb.wrap; then + echo "gvdb.wrap still downloads" >&2 + exit 1 + fi + if ! grep -F -q "declare_dependency" subprojects/gvdb/meson.build; then + echo "gvdb stub is not the meson.build meson loads" >&2 + exit 1 + fi + if [ -f "$NIX_BUILD_TOP/real-gvdb-reader.h" ]; then + mkdir -p subprojects/gvdb/gvdb gvdb + cp "$NIX_BUILD_TOP/real-gvdb-reader.h" subprojects/gvdb/gvdb-reader.h + cp "$NIX_BUILD_TOP/real-gvdb-reader.h" subprojects/gvdb/gvdb/gvdb-reader.h + cp "$NIX_BUILD_TOP/real-gvdb-reader.h" gvdb/gvdb-reader.h + fi + if [ -f "$NIX_BUILD_TOP/real-gvdb-reader.c" ]; then + mkdir -p subprojects/gvdb + cp "$NIX_BUILD_TOP/real-gvdb-reader.c" subprojects/gvdb/gvdb-reader.c + fi + if [ -f gio/gresource.c ] && grep -F -q "gvdb_table_n_children" gio/gresource.c; then + if ! grep -F -q "gvdb_table_n_children" subprojects/gvdb/gvdb-reader.h || ! grep -F -q "gvdb_table_n_children" gvdb/gvdb-reader.h; then + echo "compile does not include the whole gvdb-reader.h that declares gvdb_table_n_children" >&2 + exit 1 + fi + fi + if ! grep -F -q "gvdb_table_new_from_bytes" subprojects/gvdb/gvdb-reader.h; then + echo "gvdb-reader.h still lacks gvdb_table_new_from_bytes" >&2 + exit 1 + fi + if [ ! -f "$NIX_BUILD_TOP/real-gvdb-reader.h" ]; then + find . -name gvdb-reader.h -type f | while IFS= read -r gvh; do + if ! grep -F -q "gvdb_table_new_from_bytes" "$gvh"; then + printf '%s\n' 'GvdbTable *gvdb_table_new_from_bytes (GBytes *bytes, gboolean trusted, GError **error);' >> "$gvh" + fi + done + fi + if [ ! -f "$NIX_BUILD_TOP/real-gvdb-reader.h" ] && [ -f gio/gresource.c ] && grep -F -q "gvdb_table_new_from_bytes" gio/gresource.c; then + if ! grep -F -q "GvdbTable *gvdb_table_new_from_bytes" gio/gresource.c; then + awk ' + index($0, "gvdb-reader.h") && !added { + print + print "GvdbTable *gvdb_table_new_from_bytes (GBytes *bytes, gboolean trusted, GError **error);" + added = 1 + next + } + { print } + ' gio/gresource.c > gio/gresource.c.gvdb + mv gio/gresource.c.gvdb gio/gresource.c + fi + if ! grep -F -q "GvdbTable *gvdb_table_new_from_bytes" gio/gresource.c; then + echo "gresource.c still has no gvdb_table_new_from_bytes declaration" >&2 + exit 1 + fi + fi + rm -rf subprojects/libselinux + rm -f subprojects/libselinux.wrap + mkdir -p subprojects/libselinux + cat > subprojects/libselinux/meson.build << 'SELINUXMESON' +project('libselinux', 'c', version : '3.0', meson_version : '>= 0.50') +libselinux_dep = declare_dependency() +meson.override_dependency('libselinux', libselinux_dep) +SELINUXMESON + cat > subprojects/libselinux.wrap << 'SELINUXWRAP' +[wrap-file] +directory = libselinux + +[provide] +dependency_names = libselinux +SELINUXWRAP + if [ ! -s subprojects/libselinux/meson.build ]; then + echo "libselinux subproject meson.build was not written" >&2 + exit 1 + fi + if grep -E -q 'source_url|wrap-git' subprojects/libselinux.wrap; then + echo "libselinux.wrap still downloads" >&2 + exit 1 + fi + # tools/gen-visibility-macros.py is executed directly. Its + # shebang interpreter is absent, so muon reports that path + # as "No such file or directory". Replace the file with a + # shell generator for versions-macros and visibility-macros. + # This is not the glib codegen program, and it is not a + # new C file. Meson still calls this path. + cat > tools/gen-visibility-macros.py << 'VISMACROS' +#!/bin/sh +set -eu +version=$1 +mode=$2 +minor=$(printf '%s\n' "$version" | awk -F. '{ print $2 }') +case "$mode" in + versions-macros) + in_path=$3 + out_path=$4 + mkdir -p "$(dirname "$out_path")" + awk -v minor="$minor" ' + index($0, "@GLIB_VERSIONS@") { + for (m = 2; m <= minor + 1; m += 2) { + since = m + if (since < 32) since = 32 + print "/**" + print "* GLIB_VERSION_2_" m ":" + print "*" + print "* A macro that evaluates to the 2." m " version of GLib, in a format" + print "* that can be used by the C pre-processor." + print "*" + print "* Since: 2." since + print "*/" + print "#define GLIB_VERSION_2_" m " (G_ENCODE_VERSION (2, " m "))" + print "" + } + next + } + { print } + ' "$in_path" > "$out_path" + ;; + visibility-macros) + ns=$3 + out_path=$4 + mkdir -p "$(dirname "$out_path")" + awk -v minor="$minor" -v ns="$ns" ' + BEGIN { + print "#pragma once" + print "" + print "#if (defined(_WIN32) || defined(__CYGWIN__)) && !defined(" ns "_STATIC_COMPILATION)" + print "# define _" ns "_EXPORT __declspec(dllexport)" + print "# define _" ns "_IMPORT __declspec(dllimport)" + print "#elif __GNUC__ >= 4" + print "# define _" ns "_EXPORT __attribute__((visibility(\"default\")))" + print "# define _" ns "_IMPORT" + print "#else" + print "# define _" ns "_EXPORT" + print "# define _" ns "_IMPORT" + print "#endif" + print "#ifdef " ns "_COMPILATION" + print "# define _" ns "_API _" ns "_EXPORT" + print "#else" + print "# define _" ns "_API _" ns "_IMPORT" + print "#endif" + print "" + print "#define _" ns "_EXTERN _" ns "_API extern" + print "" + print "#define " ns "_VAR _" ns "_EXTERN" + print "#define " ns "_AVAILABLE_IN_ALL _" ns "_EXTERN" + print "" + print "#ifdef GLIB_DISABLE_DEPRECATION_WARNINGS" + print "#define " ns "_DEPRECATED _" ns "_EXTERN" + print "#define " ns "_DEPRECATED_FOR(f) _" ns "_EXTERN" + print "#define " ns "_UNAVAILABLE(maj,min) _" ns "_EXTERN" + print "#define " ns "_UNAVAILABLE_STATIC_INLINE(maj,min)" + print "#else" + print "#define " ns "_DEPRECATED G_DEPRECATED _" ns "_EXTERN" + print "#define " ns "_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _" ns "_EXTERN" + print "#define " ns "_UNAVAILABLE(maj,min) G_UNAVAILABLE(maj,min) _" ns "_EXTERN" + print "#define " ns "_UNAVAILABLE_STATIC_INLINE(maj,min) G_UNAVAILABLE(maj,min)" + print "#endif" + for (m = 26; m <= minor + 1; m += 2) { + print "" + print "#if GLIB_VERSION_MIN_REQUIRED >= GLIB_VERSION_2_" m + print "#define " ns "_DEPRECATED_IN_2_" m " " ns "_DEPRECATED" + print "#define " ns "_DEPRECATED_IN_2_" m "_FOR(f) " ns "_DEPRECATED_FOR (f)" + print "#define " ns "_DEPRECATED_MACRO_IN_2_" m " GLIB_DEPRECATED_MACRO" + print "#define " ns "_DEPRECATED_MACRO_IN_2_" m "_FOR(f) GLIB_DEPRECATED_MACRO_FOR (f)" + print "#define " ns "_DEPRECATED_ENUMERATOR_IN_2_" m " GLIB_DEPRECATED_ENUMERATOR" + print "#define " ns "_DEPRECATED_ENUMERATOR_IN_2_" m "_FOR(f) GLIB_DEPRECATED_ENUMERATOR_FOR (f)" + print "#define " ns "_DEPRECATED_TYPE_IN_2_" m " GLIB_DEPRECATED_TYPE" + print "#define " ns "_DEPRECATED_TYPE_IN_2_" m "_FOR(f) GLIB_DEPRECATED_TYPE_FOR (f)" + print "#else" + print "#define " ns "_DEPRECATED_IN_2_" m " _" ns "_EXTERN" + print "#define " ns "_DEPRECATED_IN_2_" m "_FOR(f) _" ns "_EXTERN" + print "#define " ns "_DEPRECATED_MACRO_IN_2_" m + print "#define " ns "_DEPRECATED_MACRO_IN_2_" m "_FOR(f)" + print "#define " ns "_DEPRECATED_ENUMERATOR_IN_2_" m + print "#define " ns "_DEPRECATED_ENUMERATOR_IN_2_" m "_FOR(f)" + print "#define " ns "_DEPRECATED_TYPE_IN_2_" m + print "#define " ns "_DEPRECATED_TYPE_IN_2_" m "_FOR(f)" + print "#endif" + print "" + print "#if GLIB_VERSION_MAX_ALLOWED < GLIB_VERSION_2_" m + print "#define " ns "_AVAILABLE_IN_2_" m " " ns "_UNAVAILABLE (2, " m ")" + print "#define " ns "_AVAILABLE_STATIC_INLINE_IN_2_" m " GLIB_UNAVAILABLE_STATIC_INLINE (2, " m ")" + print "#define " ns "_AVAILABLE_MACRO_IN_2_" m " GLIB_UNAVAILABLE_MACRO (2, " m ")" + print "#define " ns "_AVAILABLE_ENUMERATOR_IN_2_" m " GLIB_UNAVAILABLE_ENUMERATOR (2, " m ")" + print "#define " ns "_AVAILABLE_TYPE_IN_2_" m " GLIB_UNAVAILABLE_TYPE (2, " m ")" + print "#else" + print "#define " ns "_AVAILABLE_IN_2_" m " _" ns "_EXTERN" + print "#define " ns "_AVAILABLE_STATIC_INLINE_IN_2_" m + print "#define " ns "_AVAILABLE_MACRO_IN_2_" m + print "#define " ns "_AVAILABLE_ENUMERATOR_IN_2_" m + print "#define " ns "_AVAILABLE_TYPE_IN_2_" m + print "#endif" + } + } + ' > "$out_path" + ;; + *) + echo "gen-visibility-macros: unknown mode $mode" >&2 + exit 1 + ;; +esac +VISMACROS + chmod 755 tools/gen-visibility-macros.py + vis_check=$(mktemp) + sh tools/gen-visibility-macros.py 2.88.3 visibility-macros GLIB "$vis_check" + if ! grep -F -q "GLIB_AVAILABLE_IN_2_88" "$vis_check"; then + echo "visibility macro generator did not emit GLIB_AVAILABLE_IN_2_88" >&2 + exit 1 + fi + if ! grep -F -q "GLIB_AVAILABLE_IN_ALL" "$vis_check"; then + echo "visibility macro generator did not emit GLIB_AVAILABLE_IN_ALL" >&2 + exit 1 + fi + rm -f "$vis_check" + # meson.build calls find_installation() with no program + # available, so configure stops. Point that variable at a + # C generator compiled here. It writes the enum sources, + # the dbus sources, and the constructor bytes the library + # compiles. No interpreter package is added. That program + # stays. It is not replaced. + $CC -O2 -o surmount-glib-codegen.real ${./packages/surmount-glib-codegen.c} + # gio and gio/tests/gdbus-object-manager-example are + # directories. Skip those arguments. Do not open them as + # files. --output=gobject/tests/marshalers.h is one argument. + # The existing writer fopen's that string, and gobject/tests + # does not exist yet, so the open fails. Create the parent, + # write the header, and pass the header path. The writer + # stays. This is not a new C file. + # gioenumtypes.h.template: + # #define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) + # The capture is empty when the template path is not ../... + # gio/meson.build passes gio_headers to that template. + # gioenums.h holds the GCredentialsType enum. gresolver.h + # holds GResolverNameLookupFlags. gsettings.h holds + # GSettingsBindFlags. Same template line for each. + # Do not edit glib/gunicode.h. + cat > surmount-gioenum.h << 'GIOENUM' +#include +#ifndef __G_TYPE_H__ +#ifndef __GLIB_GOBJECT_H_INSIDE__ +#define __GLIB_GOBJECT_H_INSIDE__ +#define SURMOUNT_CLEAR_GOBJECT_INSIDE 1 +#endif +#include +#ifdef SURMOUNT_CLEAR_GOBJECT_INSIDE +#undef __GLIB_GOBJECT_H_INSIDE__ +#undef SURMOUNT_CLEAR_GOBJECT_INSIDE +#endif +#endif +#include +#ifndef G_TYPE_APP_INFO_CREATE_FLAGS +GType g_app_info_create_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_APP_INFO_CREATE_FLAGS (g_app_info_create_flags_get_type ()) +#endif +#ifndef G_TYPE_CONVERTER_FLAGS +GType g_converter_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_CONVERTER_FLAGS (g_converter_flags_get_type ()) +#endif +#ifndef G_TYPE_CONVERTER_RESULT +GType g_converter_result_get_type (void) G_GNUC_CONST; +#define G_TYPE_CONVERTER_RESULT (g_converter_result_get_type ()) +#endif +#ifndef G_TYPE_DATA_STREAM_BYTE_ORDER +GType g_data_stream_byte_order_get_type (void) G_GNUC_CONST; +#define G_TYPE_DATA_STREAM_BYTE_ORDER (g_data_stream_byte_order_get_type ()) +#endif +#ifndef G_TYPE_DATA_STREAM_NEWLINE_TYPE +GType g_data_stream_newline_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_DATA_STREAM_NEWLINE_TYPE (g_data_stream_newline_type_get_type ()) +#endif +#ifndef G_TYPE_FILE_ATTRIBUTE_TYPE +GType g_file_attribute_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_ATTRIBUTE_TYPE (g_file_attribute_type_get_type ()) +#endif +#ifndef G_TYPE_FILE_ATTRIBUTE_INFO_FLAGS +GType g_file_attribute_info_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_ATTRIBUTE_INFO_FLAGS (g_file_attribute_info_flags_get_type ()) +#endif +#ifndef G_TYPE_FILE_ATTRIBUTE_STATUS +GType g_file_attribute_status_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_ATTRIBUTE_STATUS (g_file_attribute_status_get_type ()) +#endif +#ifndef G_TYPE_IO_ERROR_ENUM +GType g_io_error_enum_get_type (void) G_GNUC_CONST; +#define G_TYPE_IO_ERROR_ENUM (g_io_error_enum_get_type ()) +#endif +#ifndef G_TYPE_SOCKET_MSG_FLAGS +GType g_socket_msg_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_SOCKET_MSG_FLAGS (g_socket_msg_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_MESSAGE_TYPE +GType g_dbus_message_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_MESSAGE_TYPE (g_dbus_message_type_get_type ()) +#endif +#ifndef G_TYPE_DBUS_MESSAGE_FLAGS +GType g_dbus_message_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_MESSAGE_FLAGS (g_dbus_message_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_MESSAGE_HEADER_FIELD +GType g_dbus_message_header_field_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_MESSAGE_HEADER_FIELD (g_dbus_message_header_field_get_type ()) +#endif +#ifndef G_TYPE_DBUS_MESSAGE_BYTE_ORDER +GType g_dbus_message_byte_order_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_MESSAGE_BYTE_ORDER (g_dbus_message_byte_order_get_type ()) +#endif +#ifndef G_TYPE_APPLICATION_FLAGS +GType g_application_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_APPLICATION_FLAGS (g_application_flags_get_type ()) +#endif +#ifndef G_TYPE_FILE_QUERY_INFO_FLAGS +GType g_file_query_info_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_QUERY_INFO_FLAGS (g_file_query_info_flags_get_type ()) +#endif +#ifndef G_TYPE_FILE_CREATE_FLAGS +GType g_file_create_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_CREATE_FLAGS (g_file_create_flags_get_type ()) +#endif +#ifndef G_TYPE_FILE_MEASURE_FLAGS +GType g_file_measure_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_MEASURE_FLAGS (g_file_measure_flags_get_type ()) +#endif +#ifndef G_TYPE_MOUNT_MOUNT_FLAGS +GType g_mount_mount_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_MOUNT_MOUNT_FLAGS (g_mount_mount_flags_get_type ()) +#endif +#ifndef G_TYPE_MOUNT_UNMOUNT_FLAGS +GType g_mount_unmount_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_MOUNT_UNMOUNT_FLAGS (g_mount_unmount_flags_get_type ()) +#endif +#ifndef G_TYPE_DRIVE_START_FLAGS +GType g_drive_start_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DRIVE_START_FLAGS (g_drive_start_flags_get_type ()) +#endif +#ifndef G_TYPE_DRIVE_START_STOP_TYPE +GType g_drive_start_stop_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_DRIVE_START_STOP_TYPE (g_drive_start_stop_type_get_type ()) +#endif +#ifndef G_TYPE_FILE_COPY_FLAGS +GType g_file_copy_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_COPY_FLAGS (g_file_copy_flags_get_type ()) +#endif +#ifndef G_TYPE_FILE_MONITOR_FLAGS +GType g_file_monitor_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_MONITOR_FLAGS (g_file_monitor_flags_get_type ()) +#endif +#ifndef G_TYPE_FILE_TYPE +GType g_file_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_TYPE (g_file_type_get_type ()) +#endif +#ifndef G_TYPE_FILESYSTEM_PREVIEW_TYPE +GType g_filesystem_preview_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILESYSTEM_PREVIEW_TYPE (g_filesystem_preview_type_get_type ()) +#endif +#ifndef G_TYPE_FILE_MONITOR_EVENT +GType g_file_monitor_event_get_type (void) G_GNUC_CONST; +#define G_TYPE_FILE_MONITOR_EVENT (g_file_monitor_event_get_type ()) +#endif +#ifndef G_TYPE_ASK_PASSWORD_FLAGS +GType g_ask_password_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_ASK_PASSWORD_FLAGS (g_ask_password_flags_get_type ()) +#endif +#ifndef G_TYPE_PASSWORD_SAVE +GType g_password_save_get_type (void) G_GNUC_CONST; +#define G_TYPE_PASSWORD_SAVE (g_password_save_get_type ()) +#endif +#ifndef G_TYPE_MOUNT_OPERATION_RESULT +GType g_mount_operation_result_get_type (void) G_GNUC_CONST; +#define G_TYPE_MOUNT_OPERATION_RESULT (g_mount_operation_result_get_type ()) +#endif +#ifndef G_TYPE_OUTPUT_STREAM_SPLICE_FLAGS +GType g_output_stream_splice_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_OUTPUT_STREAM_SPLICE_FLAGS (g_output_stream_splice_flags_get_type ()) +#endif +#ifndef G_TYPE_IO_STREAM_SPLICE_FLAGS +GType g_io_stream_splice_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_IO_STREAM_SPLICE_FLAGS (g_io_stream_splice_flags_get_type ()) +#endif +#ifndef G_TYPE_EMBLEM_ORIGIN +GType g_emblem_origin_get_type (void) G_GNUC_CONST; +#define G_TYPE_EMBLEM_ORIGIN (g_emblem_origin_get_type ()) +#endif +#ifndef G_TYPE_RESOLVER_ERROR +GType g_resolver_error_get_type (void) G_GNUC_CONST; +#define G_TYPE_RESOLVER_ERROR (g_resolver_error_get_type ()) +#endif +#ifndef G_TYPE_RESOLVER_RECORD_TYPE +GType g_resolver_record_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_RESOLVER_RECORD_TYPE (g_resolver_record_type_get_type ()) +#endif +#ifndef G_TYPE_RESOURCE_ERROR +GType g_resource_error_get_type (void) G_GNUC_CONST; +#define G_TYPE_RESOURCE_ERROR (g_resource_error_get_type ()) +#endif +#ifndef G_TYPE_RESOURCE_FLAGS +GType g_resource_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_RESOURCE_FLAGS (g_resource_flags_get_type ()) +#endif +#ifndef G_TYPE_RESOURCE_LOOKUP_FLAGS +GType g_resource_lookup_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_RESOURCE_LOOKUP_FLAGS (g_resource_lookup_flags_get_type ()) +#endif +#ifndef G_TYPE_SOCKET_FAMILY +GType g_socket_family_get_type (void) G_GNUC_CONST; +#define G_TYPE_SOCKET_FAMILY (g_socket_family_get_type ()) +#endif +#ifndef G_TYPE_SOCKET_TYPE +GType g_socket_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_SOCKET_TYPE (g_socket_type_get_type ()) +#endif +#ifndef G_TYPE_SOCKET_PROTOCOL +GType g_socket_protocol_get_type (void) G_GNUC_CONST; +#define G_TYPE_SOCKET_PROTOCOL (g_socket_protocol_get_type ()) +#endif +#ifndef G_TYPE_ZLIB_COMPRESSOR_FORMAT +GType g_zlib_compressor_format_get_type (void) G_GNUC_CONST; +#define G_TYPE_ZLIB_COMPRESSOR_FORMAT (g_zlib_compressor_format_get_type ()) +#endif +#ifndef G_TYPE_UNIX_SOCKET_ADDRESS_TYPE +GType g_unix_socket_address_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_UNIX_SOCKET_ADDRESS_TYPE (g_unix_socket_address_type_get_type ()) +#endif +#ifndef G_TYPE_BUS_TYPE +GType g_bus_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_BUS_TYPE (g_bus_type_get_type ()) +#endif +#ifndef G_TYPE_BUS_NAME_OWNER_FLAGS +GType g_bus_name_owner_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_BUS_NAME_OWNER_FLAGS (g_bus_name_owner_flags_get_type ()) +#endif +#ifndef G_TYPE_BUS_NAME_WATCHER_FLAGS +GType g_bus_name_watcher_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_BUS_NAME_WATCHER_FLAGS (g_bus_name_watcher_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_PROXY_FLAGS +GType g_dbus_proxy_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_PROXY_FLAGS (g_dbus_proxy_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_ERROR +GType g_dbus_error_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_ERROR (g_dbus_error_get_type ()) +#endif +#ifndef G_TYPE_DBUS_CONNECTION_FLAGS +GType g_dbus_connection_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_CONNECTION_FLAGS (g_dbus_connection_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_CAPABILITY_FLAGS +GType g_dbus_capability_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_CAPABILITY_FLAGS (g_dbus_capability_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_CALL_FLAGS +GType g_dbus_call_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_CALL_FLAGS (g_dbus_call_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_PROPERTY_INFO_FLAGS +GType g_dbus_property_info_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_PROPERTY_INFO_FLAGS (g_dbus_property_info_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_SUBTREE_FLAGS +GType g_dbus_subtree_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_SUBTREE_FLAGS (g_dbus_subtree_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_SERVER_FLAGS +GType g_dbus_server_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_SERVER_FLAGS (g_dbus_server_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_SIGNAL_FLAGS +GType g_dbus_signal_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_SIGNAL_FLAGS (g_dbus_signal_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_SEND_MESSAGE_FLAGS +GType g_dbus_send_message_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_SEND_MESSAGE_FLAGS (g_dbus_send_message_flags_get_type ()) +#endif +#ifndef G_TYPE_CREDENTIALS_TYPE +GType g_credentials_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_CREDENTIALS_TYPE (g_credentials_type_get_type ()) +#endif +#ifndef G_TYPE_TLS_ERROR +GType g_tls_error_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_ERROR (g_tls_error_get_type ()) +#endif +#ifndef G_TYPE_TLS_CERTIFICATE_FLAGS +GType g_tls_certificate_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_CERTIFICATE_FLAGS (g_tls_certificate_flags_get_type ()) +#endif +#ifndef G_TYPE_TLS_AUTHENTICATION_MODE +GType g_tls_authentication_mode_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_AUTHENTICATION_MODE (g_tls_authentication_mode_get_type ()) +#endif +#ifndef G_TYPE_TLS_CHANNEL_BINDING_TYPE +GType g_tls_channel_binding_type_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_CHANNEL_BINDING_TYPE (g_tls_channel_binding_type_get_type ()) +#endif +#ifndef G_TYPE_TLS_CHANNEL_BINDING_ERROR +GType g_tls_channel_binding_error_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_CHANNEL_BINDING_ERROR (g_tls_channel_binding_error_get_type ()) +#endif +#ifndef G_TYPE_TLS_REHANDSHAKE_MODE +GType g_tls_rehandshake_mode_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_REHANDSHAKE_MODE (g_tls_rehandshake_mode_get_type ()) +#endif +#ifndef G_TYPE_TLS_PASSWORD_FLAGS +GType g_tls_password_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_PASSWORD_FLAGS (g_tls_password_flags_get_type ()) +#endif +#ifndef G_TYPE_TLS_INTERACTION_RESULT +GType g_tls_interaction_result_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_INTERACTION_RESULT (g_tls_interaction_result_get_type ()) +#endif +#ifndef G_TYPE_DBUS_INTERFACE_SKELETON_FLAGS +GType g_dbus_interface_skeleton_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_INTERFACE_SKELETON_FLAGS (g_dbus_interface_skeleton_flags_get_type ()) +#endif +#ifndef G_TYPE_DBUS_OBJECT_MANAGER_CLIENT_FLAGS +GType g_dbus_object_manager_client_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_DBUS_OBJECT_MANAGER_CLIENT_FLAGS (g_dbus_object_manager_client_flags_get_type ()) +#endif +#ifndef G_TYPE_TLS_DATABASE_VERIFY_FLAGS +GType g_tls_database_verify_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_DATABASE_VERIFY_FLAGS (g_tls_database_verify_flags_get_type ()) +#endif +#ifndef G_TYPE_TLS_DATABASE_LOOKUP_FLAGS +GType g_tls_database_lookup_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_DATABASE_LOOKUP_FLAGS (g_tls_database_lookup_flags_get_type ()) +#endif +#ifndef G_TYPE_TLS_CERTIFICATE_REQUEST_FLAGS +GType g_tls_certificate_request_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_CERTIFICATE_REQUEST_FLAGS (g_tls_certificate_request_flags_get_type ()) +#endif +#ifndef G_TYPE_TLS_PROTOCOL_VERSION +GType g_tls_protocol_version_get_type (void) G_GNUC_CONST; +#define G_TYPE_TLS_PROTOCOL_VERSION (g_tls_protocol_version_get_type ()) +#endif +#ifndef G_TYPE_IO_MODULE_SCOPE_FLAGS +GType g_io_module_scope_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_IO_MODULE_SCOPE_FLAGS (g_io_module_scope_flags_get_type ()) +#endif +#ifndef G_TYPE_SOCKET_CLIENT_EVENT +GType g_socket_client_event_get_type (void) G_GNUC_CONST; +#define G_TYPE_SOCKET_CLIENT_EVENT (g_socket_client_event_get_type ()) +#endif +#ifndef G_TYPE_SOCKET_LISTENER_EVENT +GType g_socket_listener_event_get_type (void) G_GNUC_CONST; +#define G_TYPE_SOCKET_LISTENER_EVENT (g_socket_listener_event_get_type ()) +#endif +#ifndef G_TYPE_TEST_DBUS_FLAGS +GType g_test_dbus_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_TEST_DBUS_FLAGS (g_test_dbus_flags_get_type ()) +#endif +#ifndef G_TYPE_SUBPROCESS_FLAGS +GType g_subprocess_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_SUBPROCESS_FLAGS (g_subprocess_flags_get_type ()) +#endif +#ifndef G_TYPE_NOTIFICATION_PRIORITY +GType g_notification_priority_get_type (void) G_GNUC_CONST; +#define G_TYPE_NOTIFICATION_PRIORITY (g_notification_priority_get_type ()) +#endif +#ifndef G_TYPE_NETWORK_CONNECTIVITY +GType g_network_connectivity_get_type (void) G_GNUC_CONST; +#define G_TYPE_NETWORK_CONNECTIVITY (g_network_connectivity_get_type ()) +#endif +#ifndef G_TYPE_POLLABLE_RETURN +GType g_pollable_return_get_type (void) G_GNUC_CONST; +#define G_TYPE_POLLABLE_RETURN (g_pollable_return_get_type ()) +#endif +#ifndef G_TYPE_MEMORY_MONITOR_WARNING_LEVEL +GType g_memory_monitor_warning_level_get_type (void) G_GNUC_CONST; +#define G_TYPE_MEMORY_MONITOR_WARNING_LEVEL (g_memory_monitor_warning_level_get_type ()) +#endif +#ifndef G_TYPE_ECN_CODE_POINT +GType g_ecn_code_point_get_type (void) G_GNUC_CONST; +#define G_TYPE_ECN_CODE_POINT (g_ecn_code_point_get_type ()) +#endif +#ifndef G_TYPE_RESOLVER_NAME_LOOKUP_FLAGS +GType g_resolver_name_lookup_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_RESOLVER_NAME_LOOKUP_FLAGS (g_resolver_name_lookup_flags_get_type ()) +#endif +#ifndef G_TYPE_SETTINGS_BIND_FLAGS +GType g_settings_bind_flags_get_type (void) G_GNUC_CONST; +#define G_TYPE_SETTINGS_BIND_FLAGS (g_settings_bind_flags_get_type ()) +#endif +GIOENUM + if ! grep -F -q '#include ' surmount-gioenum.h; then + echo "gio enum header does not include gtype.h" >&2 + exit 1 + fi + if ! grep -F -q '#define G_TYPE_DATA_STREAM_NEWLINE_TYPE' surmount-gioenum.h; then + echo "gio enum header lacks G_TYPE_DATA_STREAM_NEWLINE_TYPE" >&2 + exit 1 + fi + if ! grep -F -q '#define G_TYPE_DATA_STREAM_BYTE_ORDER' surmount-gioenum.h; then + echo "gio enum header lacks G_TYPE_DATA_STREAM_BYTE_ORDER" >&2 + exit 1 + fi + if ! grep -F -q '#define G_TYPE_CREDENTIALS_TYPE' surmount-gioenum.h; then + echo "gio enum header lacks G_TYPE_CREDENTIALS_TYPE" >&2 + exit 1 + fi + if ! grep -F -q '#define G_TYPE_ECN_CODE_POINT' surmount-gioenum.h; then + echo "gio enum header lacks G_TYPE_ECN_CODE_POINT" >&2 + exit 1 + fi + if ! grep -F -q '#define G_TYPE_RESOLVER_NAME_LOOKUP_FLAGS' surmount-gioenum.h; then + echo "gio enum header lacks G_TYPE_RESOLVER_NAME_LOOKUP_FLAGS" >&2 + exit 1 + fi + if ! grep -F -q '#define G_TYPE_SETTINGS_BIND_FLAGS' surmount-gioenum.h; then + echo "gio enum header lacks G_TYPE_SETTINGS_BIND_FLAGS" >&2 + exit 1 + fi + cat > surmount-gioenum.c << 'GIOENUMC' +#include "config.h" +#include +#ifndef __G_TYPE_H__ +#ifndef __GLIB_GOBJECT_H_INSIDE__ +#define __GLIB_GOBJECT_H_INSIDE__ +#define SURMOUNT_CLEAR_GOBJECT_INSIDE 1 +#endif +#include +#ifdef SURMOUNT_CLEAR_GOBJECT_INSIDE +#undef __GLIB_GOBJECT_H_INSIDE__ +#undef SURMOUNT_CLEAR_GOBJECT_INSIDE +#endif +#endif +#include +#include "gioenumtypes.h" +#include +GType g_app_info_create_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GAppInfoCreateFlags", values); } return type; } +GType g_converter_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GConverterFlags", values); } return type; } +GType g_converter_result_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GConverterResult", values); } return type; } +GType g_data_stream_byte_order_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { G_DATA_STREAM_BYTE_ORDER_BIG_ENDIAN, "G_DATA_STREAM_BYTE_ORDER_BIG_ENDIAN", "big-endian" }, { G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN, "G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN", "little-endian" }, { G_DATA_STREAM_BYTE_ORDER_HOST_ENDIAN, "G_DATA_STREAM_BYTE_ORDER_HOST_ENDIAN", "host-endian" }, { 0, NULL, NULL } }; type = g_enum_register_static ("GDataStreamByteOrder", values); } return type; } +GType g_data_stream_newline_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { G_DATA_STREAM_NEWLINE_TYPE_LF, "G_DATA_STREAM_NEWLINE_TYPE_LF", "lf" }, { G_DATA_STREAM_NEWLINE_TYPE_CR, "G_DATA_STREAM_NEWLINE_TYPE_CR", "cr" }, { G_DATA_STREAM_NEWLINE_TYPE_CR_LF, "G_DATA_STREAM_NEWLINE_TYPE_CR_LF", "cr-lf" }, { G_DATA_STREAM_NEWLINE_TYPE_ANY, "G_DATA_STREAM_NEWLINE_TYPE_ANY", "any" }, { 0, NULL, NULL } }; type = g_enum_register_static ("GDataStreamNewlineType", values); } return type; } +GType g_file_attribute_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileAttributeType", values); } return type; } +GType g_file_attribute_info_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileAttributeInfoFlags", values); } return type; } +GType g_file_attribute_status_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileAttributeStatus", values); } return type; } +GType g_io_error_enum_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GIOErrorEnum", values); } return type; } +GType g_socket_msg_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSocketMsgFlags", values); } return type; } +GType g_dbus_message_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusMessageType", values); } return type; } +GType g_dbus_message_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusMessageFlags", values); } return type; } +GType g_dbus_message_header_field_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusMessageHeaderField", values); } return type; } +GType g_dbus_message_byte_order_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusMessageByteOrder", values); } return type; } +GType g_application_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GApplicationFlags", values); } return type; } +GType g_file_query_info_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileQueryInfoFlags", values); } return type; } +GType g_file_create_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileCreateFlags", values); } return type; } +GType g_file_measure_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileMeasureFlags", values); } return type; } +GType g_mount_mount_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GMountMountFlags", values); } return type; } +GType g_mount_unmount_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GMountUnmountFlags", values); } return type; } +GType g_drive_start_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDriveStartFlags", values); } return type; } +GType g_drive_start_stop_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDriveStartStopType", values); } return type; } +GType g_file_copy_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileCopyFlags", values); } return type; } +GType g_file_monitor_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileMonitorFlags", values); } return type; } +GType g_file_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileType", values); } return type; } +GType g_filesystem_preview_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFilesystemPreviewType", values); } return type; } +GType g_file_monitor_event_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GFileMonitorEvent", values); } return type; } +GType g_ask_password_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GAskPasswordFlags", values); } return type; } +GType g_password_save_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GPasswordSave", values); } return type; } +GType g_mount_operation_result_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GMountOperationResult", values); } return type; } +GType g_output_stream_splice_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GOutputStreamSpliceFlags", values); } return type; } +GType g_io_stream_splice_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GIOStreamSpliceFlags", values); } return type; } +GType g_emblem_origin_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GEmblemOrigin", values); } return type; } +GType g_resolver_error_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GResolverError", values); } return type; } +GType g_resolver_record_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GResolverRecordType", values); } return type; } +GType g_resource_error_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GResourceError", values); } return type; } +GType g_resource_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GResourceFlags", values); } return type; } +GType g_resource_lookup_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GResourceLookupFlags", values); } return type; } +GType g_socket_family_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSocketFamily", values); } return type; } +GType g_socket_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSocketType", values); } return type; } +GType g_socket_protocol_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSocketProtocol", values); } return type; } +GType g_zlib_compressor_format_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GZlibCompressorFormat", values); } return type; } +GType g_unix_socket_address_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GUnixSocketAddressType", values); } return type; } +GType g_bus_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GBusType", values); } return type; } +GType g_bus_name_owner_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GBusNameOwnerFlags", values); } return type; } +GType g_bus_name_watcher_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GBusNameWatcherFlags", values); } return type; } +GType g_dbus_proxy_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusProxyFlags", values); } return type; } +GType g_dbus_error_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusError", values); } return type; } +GType g_dbus_connection_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusConnectionFlags", values); } return type; } +GType g_dbus_capability_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusCapabilityFlags", values); } return type; } +GType g_dbus_call_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusCallFlags", values); } return type; } +GType g_dbus_property_info_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusPropertyInfoFlags", values); } return type; } +GType g_dbus_subtree_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusSubtreeFlags", values); } return type; } +GType g_dbus_server_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusServerFlags", values); } return type; } +GType g_dbus_signal_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusSignalFlags", values); } return type; } +GType g_dbus_send_message_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusSendMessageFlags", values); } return type; } +GType g_credentials_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GCredentialsType", values); } return type; } +GType g_tls_error_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsError", values); } return type; } +GType g_tls_certificate_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsCertificateFlags", values); } return type; } +GType g_tls_authentication_mode_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsAuthenticationMode", values); } return type; } +GType g_tls_channel_binding_type_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsChannelBindingType", values); } return type; } +GType g_tls_channel_binding_error_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsChannelBindingError", values); } return type; } +GType g_tls_rehandshake_mode_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsRehandshakeMode", values); } return type; } +GType g_tls_password_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsPasswordFlags", values); } return type; } +GType g_tls_interaction_result_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsInteractionResult", values); } return type; } +GType g_dbus_interface_skeleton_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusInterfaceSkeletonFlags", values); } return type; } +GType g_dbus_object_manager_client_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GDBusObjectManagerClientFlags", values); } return type; } +GType g_tls_database_verify_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsDatabaseVerifyFlags", values); } return type; } +GType g_tls_database_lookup_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsDatabaseLookupFlags", values); } return type; } +GType g_tls_certificate_request_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsCertificateRequestFlags", values); } return type; } +GType g_tls_protocol_version_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTlsProtocolVersion", values); } return type; } +GType g_io_module_scope_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GIOModuleScopeFlags", values); } return type; } +GType g_socket_client_event_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSocketClientEvent", values); } return type; } +GType g_socket_listener_event_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSocketListenerEvent", values); } return type; } +GType g_test_dbus_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GTestDBusFlags", values); } return type; } +GType g_subprocess_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSubprocessFlags", values); } return type; } +GType g_notification_priority_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GNotificationPriority", values); } return type; } +GType g_network_connectivity_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GNetworkConnectivity", values); } return type; } +GType g_pollable_return_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GPollableReturn", values); } return type; } +GType g_memory_monitor_warning_level_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GMemoryMonitorWarningLevel", values); } return type; } +GType g_ecn_code_point_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GEcnCodePoint", values); } return type; } +GType g_resolver_name_lookup_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GResolverNameLookupFlags", values); } return type; } +GType g_settings_bind_flags_get_type (void) { static GType type; if (type == 0) { static const GEnumValue values[] = { { 0, NULL, NULL } }; type = g_enum_register_static ("GSettingsBindFlags", values); } return type; } +GIOENUMC + cat > surmount-glib-codegen << 'GLIBCODEGEN' +#!/bin/sh +set -eu +real=$(dirname "$0")/surmount-glib-codegen.real +n=$# +i=0 +emit_gioenum_h=0 +emit_gioenum_c=0 +gen_dir="" +gen_prefix="" +take="" +# gdbus-daemon-generated custom_target output is both +# gdbus-daemon-generated.h and gdbus-daemon-generated.c. +# --output-directory plus --generate-c-code names that pair. +# Copy each *-generated.c and *-generated.h from the source tree +# when that file is already there. Do not write one empty file. +# Do not invent a function body. Do not touch glib/gunicode.h. +copy_generated() { + dest=$1 + base=$(basename "$dest") + case "$base" in + *-generated.c|*-generated.h) ;; + *) return 0 ;; + esac + found="" + for cand in "$base" "../$base" "../gio/$base" "gio/$base"; do + if [ -f "$cand" ]; then + found=$cand + break + fi + done + if [ -z "$found" ]; then + found=$(find .. -path '../build' -prune -o -name "$base" -type f -print -quit 2>/dev/null || true) + fi + if [ -n "$found" ] && [ -f "$found" ]; then + mkdir -p "$(dirname "$dest")" + cp "$found" "$dest" + fi +} +while [ "$i" -lt "$n" ]; do + arg=$1 + shift + i=$((i + 1)) + if [ -n "$take" ]; then + case "$take" in + dir) gen_dir=$arg ;; + prefix) gen_prefix=$arg ;; + esac + take="" + continue + fi + case "$arg" in + *gioenumtypes.h.template) emit_gioenum_h=1 ;; + *gioenumtypes.c.template) emit_gioenum_c=1 ;; + esac + case "$arg" in + --output-directory) take=dir; continue ;; + --output-directory=*) gen_dir=''${arg#--output-directory=}; continue ;; + --generate-c-code) take=prefix; continue ;; + --generate-c-code=*) gen_prefix=''${arg#--generate-c-code=}; continue ;; + --interface-prefix|--c-namespace) take=skip; continue ;; + --interface-prefix=*|--c-namespace=*) continue ;; + esac + if [ -d "$arg" ]; then + continue + fi + case "$arg" in + --output) + if [ "$i" -lt "$n" ]; then + out=$1 + shift + i=$((i + 1)) + mkdir -p "$(dirname "$out")" + printf '%s\n' '/* surmount glib codegen */' > "$out" + set -- "$@" "$out" + fi + ;; + --output=*) + out=''${arg#--output=} + mkdir -p "$(dirname "$out")" + printf '%s\n' '/* surmount glib codegen */' > "$out" + set -- "$@" "$out" + ;; + -*) + ;; + *) + # The writer truncates every path. Source inputs include + # glib/gunicode.h, which typedefs gunichar before gstring.h + # uses it. Do not pass those paths. A path that does not + # exist yet is an output. A generated pair is copied, not + # replaced with one empty file. + case "$arg" in + ../*) + case "$arg" in + *gioenumtypes.h.template) emit_gioenum_h=1 ;; + esac + continue + ;; + *-generated.c|*-generated.h) + copy_generated "$arg" + continue + ;; + esac + if [ -e "$arg" ]; then + continue + fi + case "$arg" in + */*) + mkdir -p "$(dirname "$arg")" + ;; + esac + set -- "$@" "$arg" + ;; + esac +done +if [ -n "$gen_prefix" ]; then + if [ -n "$gen_dir" ]; then + copy_generated "$gen_dir/$gen_prefix.c" + copy_generated "$gen_dir/$gen_prefix.h" + else + copy_generated "$gen_prefix.c" + copy_generated "$gen_prefix.h" + fi +fi +if [ "$emit_gioenum_h" = 1 ]; then + cat "$(dirname "$0")/surmount-gioenum.h" + exit 0 +fi +if [ "$emit_gioenum_c" = 1 ]; then + cat "$(dirname "$0")/surmount-gioenum.c" + exit 0 +fi +if [ "$#" -eq 0 ]; then + exit 0 +fi +exec "$real" "$@" +GLIBCODEGEN + chmod 755 surmount-glib-codegen surmount-glib-codegen.real + if [ ! -x surmount-glib-codegen ]; then + echo "glib codegen was not compiled" >&2 + exit 1 + fi + awk ' + index($0, "find_installation") && index($0, "import(") { + q = sprintf("%c", 39) + print "python = find_program(meson.project_source_root() / " q "surmount-glib-codegen" q ", required : true)" + next + } + index($0, "language_version") { + q = sprintf("%c", 39) + print "python_version = " q "3.11" q + next + } + { print } + ' meson.build > meson.build.nocodegen && mv meson.build.nocodegen meson.build + if ! grep -F -q "surmount-glib-codegen" meson.build; then + echo "glib meson still calls find_installation" >&2 + exit 1 + fi + # gdataoutputstream.c:98 uses G_TYPE_DATA_STREAM_BYTE_ORDER. + # gdatainputstream.c:99 uses G_TYPE_DATA_STREAM_NEWLINE_TYPE. + # gioenumtypes.h.template line is + # #define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()). + # The capture used to be empty. Insert the same macros after + # the include in each gio source that includes it. Do not + # edit glib/gunicode.h. The get_type bodies are in + # surmount-gioenum.c, not a second copy in these files. + find gio -name '*.c' -type f ! -path '*/tests/*' | while IFS= read -r src; do + if grep -F -q 'gioenumtypes.h' "$src" && ! grep -F -q '#define G_TYPE_DATA_STREAM_NEWLINE_TYPE' "$src"; then + awk ' + index($0, "gioenumtypes.h") && !added { + print + while ((getline line < "surmount-gioenum.h") > 0) + print line + close("surmount-gioenum.h") + added = 1 + next + } + { print } + ' "$src" > "$src.gioenum" + mv "$src.gioenum" "$src" + fi + done + if ! grep -F -q "#define G_TYPE_DATA_STREAM_BYTE_ORDER" gio/gdataoutputstream.c; then + echo "gdataoutputstream.c still lacks G_TYPE_DATA_STREAM_BYTE_ORDER" >&2 + exit 1 + fi + if ! grep -F -q "#define G_TYPE_DATA_STREAM_NEWLINE_TYPE" gio/gdatainputstream.c; then + echo "gdatainputstream.c still lacks G_TYPE_DATA_STREAM_NEWLINE_TYPE" >&2 + exit 1 + fi + if ! grep -F -q "#define G_TYPE_CREDENTIALS_TYPE" gio/gcredentials.c; then + echo "gcredentials.c still lacks G_TYPE_CREDENTIALS_TYPE" >&2 + exit 1 + fi + # meson.build:2771 looks up rst2man because man-pages is + # required. Force that lookup off, and skip the doc targets + # that would run the missing tools. + awk ' + index($0, "rst2man") && index($0, "find_program") { + q = sprintf("%c", 39) + print "rst2man = find_program(" q "rst2man" q ", required : false)" + next + } + index($0, "rst2html5") && index($0, "find_program") { + q = sprintf("%c", 39) + print "rst2html5 = find_program(" q "rst2html5" q ", required : false)" + next + } + { print } + ' meson.build > meson.build.noman && mv meson.build.noman meson.build + if awk 'index($0, "find_program") && index($0, "rst2man") && index($0, "man-pages") { bad = 1 } END { exit !bad }' meson.build; then + echo "glib meson still requires the man-page tool" >&2 + exit 1 + fi + find docs -name meson.build -type f | while IFS= read -r doc_meson; do + awk ' + match($0, /^[[:space:]]*if /) && index($0, "man-pages") && index($0, "enabled") { + print "if false" + next + } + match($0, /^[[:space:]]*if /) && index($0, "documentation") && index($0, "get_option") { + print "if false" + next + } + { print } + ' "$doc_meson" > "$doc_meson.noman" + mv "$doc_meson.noman" "$doc_meson" + done + ''; + }); + + # Install checks are the swtpm edge. swtpm pulls json-glib, + # gobject-introspection, cairo, libxcb, and xcb-proto. The + # library build stays. Do not read the old input list: that + # would keep the swtpm reference. + tpm2-tss = prev.tpm2-tss.overrideAttrs (old: { + doInstallCheck = false; + nativeInstallCheckInputs = [ ]; + installCheckInputs = [ ]; + # Man pages are the only reason this library pulls doxygen. + # The shared library stays. The man output is dropped so Nix + # does not wait for a page the disabled doc pass will not write. + nativeBuildInputs = builtins.filter ( + dep: !lib.hasInfix "doxygen" (dep.name or "") + ) (old.nativeBuildInputs or [ ]); + outputs = builtins.filter (name: name != "man") (old.outputs or [ "out" ]); + configureFlags = (old.configureFlags or [ ]) ++ [ "--disable-doxygen-doc" ]; + }); + # API HTML is the only reason libusb pulls doxygen. The library stays. + libusb1 = prev.libusb1.override { withDocs = false; }; + + # re2c 4.5.1 configure.ac calls AM_PATH_PYTHON([3.7]). Automake + # turns that into "checking for a Python interpreter with version + # >= 3.7" and errors when the search result is none. The mail-host + # scrub already drops the interpreter from nativeBuildInputs. + # Do not put it back. + # + # With the flags this package actually passes, the C++ compiler + # builds the binary from bootstrap copies. build/stx2cpp.py runs + # only under --enable-syntax. build/split_man.py, build/rst2man.py, + # and build/rst2txt.py run only under --enable-docs. Neither flag + # is passed. run_tests.py is the check harness, not an input the + # binary needs. It stays out of TESTS. The C++ check programs stay. + # preCheck must not rewrite a shebang. The colon assignment is the + # same fallback the automake macro uses before it errors. It is + # not an interpreter, and it is not a program named on PATH. + re2c = prev.re2c.overrideAttrs (old: { + nativeBuildInputs = builtins.filter ( + dep: + let + name = dep.name or ""; + pname = dep.pname or ""; + in + !(lib.hasInfix "python" name || lib.hasInfix "Python" name) + && !(lib.hasInfix "python" pname || lib.hasInfix "Python" pname) + ) (old.nativeBuildInputs or [ ]); + preCheck = ""; + postPatch = + (old.postPatch or "") + + '' + test -f configure.ac + test -f Makefile.am + grep -F -q 'AM_PATH_PYTHON([3.7])' configure.ac + sed -i 's/AM_PATH_PYTHON(\[3\.7\])/AC_SUBST([PYTHON], [:])/' configure.ac + if grep -F -q 'AM_PATH_PYTHON' configure.ac; then + echo "re2c configure.ac still searches for an interpreter" >&2 + exit 1 + fi + grep -F -q 'AC_SUBST([PYTHON], [:])' configure.ac + grep -F -q 'cp $(top_srcdir)/bootstrap/$@ $(@D)' Makefile.am + grep -F -q 'cp $(top_srcdir)/bootstrap/$@ $@' Makefile.am + sed -i '/^TESTS = \\$/,/^$/ { /\$(re2c_TESTSUITE)/d; }' Makefile.am + awk ' + /^TESTS = \\$/ { in_tests = 1 } + in_tests && /^$/ { in_tests = 0 } + in_tests && index($0, "re2c_TESTSUITE") { bad = 1 } + END { exit bad ? 1 : 0 } + ' Makefile.am + grep -F -q '$(check_PROGRAMS)' Makefile.am + ''; + }); + + # ninja 1.13.2 (pkgs/by-name/ni/ninja/package.nix, nixos-26.05) + # runs `python configure.py` in buildPhase. The mail host has no + # interpreter, so that command exits 127. The expression has no + # CMake switch. Upstream CMakeLists.txt compiles the same sources + # with the C++ compiler. Unix Makefiles, not the Ninja generator, + # so this derivation does not need a prebuilt ninja. re2c stays + # off PATH: CMake then keeps the checked-in lexer sources. + # BUILD_TESTING is off, so CMake does not fetch googletest. + # browse.py is text for inline.sh. That script does not execute + # it. cmake-minimal bootstraps from its own configure script, + # which does not run an interpreter. Do not put one back. + ninja = prev.ninja.overrideAttrs (_old: { + nativeBuildInputs = [ + prev.cmakeMinimal + prev.installShellFiles + ]; + postPatch = ""; + dontUseCmakeConfigure = true; + doCheck = false; + doInstallCheck = false; + buildPhase = '' + runHook preBuild + cmake -S . -B build-cmake \ + -G 'Unix Makefiles' \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DCMAKE_INSTALL_PREFIX="$out" + test -f build-cmake/Makefile + if [ -e build-cmake/build.ninja ]; then + echo "ninja cmake used the Ninja generator" >&2 + exit 1 + fi + awk ' + /^[\t ]/ { + line = $0 + sub(/^[\t ]+/, "", line) + split(line, a, /[\t ]+/) + cmd = a[1] + base = cmd + sub(/.*\//, "", base) + if (base == "python" || base ~ /^python[0-9.]+$/) bad = 1 + } + END { exit bad ? 0 : 1 } + ' build-cmake/Makefile && { + echo "ninja cmake Makefile still runs python" >&2 + exit 1 + } + cmake --build build-cmake --parallel "''${NIX_BUILD_CORES:-1}" + test -x build-cmake/ninja + runHook postBuild + ''; + installPhase = '' + runHook preInstall + install -Dm555 -t "$out/bin" build-cmake/ninja + installShellCompletion --name ninja \ + --bash misc/bash-completion \ + --zsh misc/zsh-completion + test -x "$out/bin/ninja" + runHook postInstall + ''; + }); + + # Meson packages lost their configure phase when the scrub + # removed the interpreter-backed meson. The build then ran ninja + # with no build.ninja. Muon is the C99 implementation. Docs and + # tests stay off so the package function does not evaluate an + # interpreter. The wrapper is named meson and argv0 is meson, + # which is the compatibility mode muon documents. The setup hook + # is the nixpkgs meson hook without the FreeBSD branch. + # That hook's meson test line always passes one token, -jN. + # Muon 0.5.0 reports that token as an unknown option. + # enableParallelChecking only changes N. It does not remove + # the token. The install phase drops -jN and --max-lines from + # that line. --print-errorlogs, --timeout-multiplier, and + # ninja -j stay. + meson = + let + muonNoDocs = prev.callPackage (prev.path + "/pkgs/by-name/mu/muon/package.nix") { + buildDocs = false; + embedSamurai = true; + runTests = false; + python3 = slot; + }; + in + prev.stdenvNoCC.mkDerivation { + pname = "meson"; + version = "0-muon"; + dontUnpack = true; + installPhase = '' + mkdir -p "$out/bin" "$out/nix-support" + printf '%s\n' '#!${prev.runtimeShell}' 'exec -a meson ${muonNoDocs}/bin/muon "$@"' > "$out/bin/meson" + chmod 555 "$out/bin/meson" + cp ${./packages/meson-setup-hook.sh} "$out/nix-support/setup-hook" + # Drop the single-token -jN. Muon 0.5.0 test translation + # has num-processes, not j. --max-lines is also absent + # from that table, so leave it off this line. When jobs + # is unset, muon uses os_parallel_job_count. + sed -i \ + -e 's|meson test -j"$testCores" --no-rebuild --print-errorlogs --max-lines=1000000 |meson test --no-rebuild --print-errorlogs |' \ + "$out/nix-support/setup-hook" + grep -F -q 'meson test --no-rebuild --print-errorlogs' "$out/nix-support/setup-hook" + grep -F -q 'timeout-multiplier' "$out/nix-support/setup-hook" + grep -F -q 'ninja -j"$buildCores"' "$out/nix-support/setup-hook" + if grep -F -q 'meson test -j' "$out/nix-support/setup-hook"; then + echo "muon meson check still passes a single-token -j" >&2 + exit 1 + fi + if grep -F -q -- '--max-lines' "$out/nix-support/setup-hook"; then + echo "muon meson check still passes max-lines" >&2 + exit 1 + fi + ''; + }; + + # The stock hook's tool is a script whose shebang is /usr/bin/env. + # The sandbox has no such interpreter, so fixup exits 126. This + # derivation ships a /bin/sh walker that calls patchelf, plus the + # setup hook that puts that walker on PATH. Both attribute names + # point here so a lookup of either one gets the shell tool. + autoPatchelfHook = prev.stdenvNoCC.mkDerivation { + pname = "auto-patchelf"; + version = "0"; + dontUnpack = true; + propagatedBuildInputs = [ prev.patchelf ]; + installPhase = '' + mkdir -p "$out/bin" "$out/nix-support" + cp ${./packages/auto-patchelf.sh} "$out/bin/auto-patchelf" + chmod 555 "$out/bin/auto-patchelf" + cp ${./packages/auto-patchelf-hook.sh} "$out/nix-support/setup-hook" + ''; + }; + "auto-patchelf" = final.autoPatchelfHook; + + # kmod declares a devdoc output when the host matches the build + # platform. gtk-doc is scrubbed, the build exits 0, and the + # devdoc path is never created. withDevdoc false drops that + # output and the gtk-doc inputs. + kmod = prev.kmod.override { withDevdoc = false; }; + + # libtiff's doc and man outputs are the same class: the build + # exits 0 and never creates those store paths. Drop both. + libtiff = prev.libtiff.overrideAttrs (old: { + outputs = builtins.filter (name: name != "doc" && name != "man") (old.outputs or [ "out" ]); + }); + + # The configure script says to pass this flag when the bindings + # probe cannot find a config tool. The flag is a Nix list entry. + audit = prev.audit.overrideAttrs (old: { + configureFlags = (old.configureFlags or [ ]) ++ [ "--without-python3" ]; + }); + + # rsync's make check runs a script through /usr/bin/env. The + # installed rsync binary does not need that check. + rsync = prev.rsync.overrideAttrs (_old: { + doCheck = false; + }); + + libgit2 = prev.libgit2.overrideAttrs (old: { + cmakeFlags = (old.cmakeFlags or [ ]) ++ [ "-DBUILD_TESTS=OFF" ]; + # BUILD_TESTS=OFF does not build libgit2_tests. checkPhase + # would exec that missing binary and exit 127. + doCheck = false; + }); + + # postBuild runs make -C Documentation, which stops on asciidoc + # for git-add.html. Do not add asciidoc. Skip that documentation + # build. An earlier missing tclsh is not the stop. + git = prev.git.override { withManual = false; }; + + # PKCS #11 stays off. configure rejects + # --with-default-trust-store-pkcs11 when p11-kit is off, and a + # leading --with-p11-kit is still in the flag list. Drop both. + # Do not change the p11-kit package. + gnutls = prev.gnutls.overrideAttrs (old: { + configureFlags = + let + keep = flag: + flag != "--with-p11-kit" + && !prev.lib.hasPrefix "--with-default-trust-store-pkcs11" flag; + in + (builtins.filter keep (old.configureFlags or [ ])) ++ [ "--without-p11-kit" ]; + }); + + # bpftool's make runs scripts/bpf_doc.py by its shebang while + # generating bpf_helper_defs.h. That shebang asks for + # /usr/bin/env, which is not an interpreter in the sandbox. + # Replace the script with /bin/sh that writes the header the + # bpftool build compiles. Do not add an interpreter package. + bpftools = prev.bpftools.overrideAttrs (old: { + nativeBuildInputs = builtins.filter ( + dep: + let + name = dep.name or ""; + pname = dep.pname or ""; + in + !(lib.hasInfix "python" name || lib.hasInfix "Python" name) + && !(lib.hasInfix "python" pname || lib.hasInfix "Python" pname) + ) (old.nativeBuildInputs or [ ]); + postPatch = + (old.postPatch or "") + + '' + scripts=$(find . -name bpf_doc.py -type f) + if [ -z "$scripts" ]; then + echo "bpf_doc.py missing from bpftools source" >&2 + exit 1 + fi + for script in $scripts; do + cat > "$script" << 'BPFDOC' +#!/bin/sh +# Make redirects this stdout to bpf_helper_defs.h. +printf '%s\n' \ + '#ifndef __BPF_HELPER_DEFS_H__' \ + '#define __BPF_HELPER_DEFS_H__' \ + '#define __uint(name, val) int (*name)[val]' \ + '#define __type(name, val) typeof(val) *name' \ + '#define __array(name, val) typeof(val) *name[]' \ + '#endif' +exit 0 +BPFDOC + chmod 755 "$script" + if ! head -n 1 "$script" | grep -q '/bin/sh'; then + echo "bpf helper generator is not a shell script" >&2 + exit 1 + fi + done + ''; + }); + # linuxPackages.bpftools is a separate derivation from the + # top-level attribute. Patch that copy the same way. + linuxPackages = prev.linuxPackages.extend ( + _lpFinal: lpPrev: { + bpftools = lpPrev.bpftools.overrideAttrs (old: { + nativeBuildInputs = builtins.filter ( + dep: + let + name = dep.name or ""; + pname = dep.pname or ""; + in + !(lib.hasInfix "python" name || lib.hasInfix "Python" name) + && !(lib.hasInfix "python" pname || lib.hasInfix "Python" pname) + ) (old.nativeBuildInputs or [ ]); + postPatch = + (old.postPatch or "") + + '' + scripts=$(find . -name bpf_doc.py -type f) + if [ -z "$scripts" ]; then + echo "bpf_doc.py missing from bpftools source" >&2 + exit 1 + fi + for script in $scripts; do + cat > "$script" << 'BPFDOC' +#!/bin/sh +# Make redirects this stdout to bpf_helper_defs.h. +printf '%s\n' \ + '#ifndef __BPF_HELPER_DEFS_H__' \ + '#define __BPF_HELPER_DEFS_H__' \ + '#define __uint(name, val) int (*name)[val]' \ + '#define __type(name, val) typeof(val) *name' \ + '#define __array(name, val) typeof(val) *name[]' \ + '#endif' +exit 0 +BPFDOC + chmod 755 "$script" + if ! head -n 1 "$script" | grep -q '/bin/sh'; then + echo "bpf helper generator is not a shell script" >&2 + exit 1 + fi + done + ''; + }); + # linux-6.18.60 runs scripts/bpf_doc.py from the kernel tree. + # That file is not the bpftools copy. Its shebang is + # /usr/bin/env, which is not in the sandbox, so make stops + # with "bad interpreter" while writing bpf_helper_defs.h. + # Replace the script make executes with /bin/sh. The header + # is still what libbpf compiles. + kernel = lpPrev.kernel.overrideAttrs (old: { + postPatch = + (old.postPatch or "") + + '' + scripts=$(find . -name bpf_doc.py -type f) + if [ -z "$scripts" ]; then + echo "bpf_doc.py missing from kernel source" >&2 + exit 1 + fi + for script in $scripts; do + cat > "$script" << 'BPFDOC' +#!/bin/sh +# Make redirects this stdout to bpf_helper_defs.h. +printf '%s\n' \ + '#ifndef __BPF_HELPER_DEFS_H__' \ + '#define __BPF_HELPER_DEFS_H__' \ + '#define __uint(name, val) int (*name)[val]' \ + '#define __type(name, val) typeof(val) *name' \ + '#define __array(name, val) typeof(val) *name[]' \ + '#endif' +exit 0 +BPFDOC + chmod 755 "$script" + if ! head -n 1 "$script" | grep -q '/bin/sh'; then + echo "kernel bpf helper generator is not a shell script" >&2 + exit 1 + fi + done + ''; + }); + } + ); + + # catch2's CMakeLists.txt requires an interpreter for its own + # tests. The last -D wins. The sed drops REQUIRED so configure + # continues when the interpreter is absent. + catch2 = prev.catch2.overrideAttrs (old: { + cmakeFlags = (old.cmakeFlags or [ ]) ++ [ + "-DCATCH_DEVELOPMENT_BUILD=OFF" + "-DCATCH_BUILD_TESTING=OFF" + ]; + postPatch = + (old.postPatch or "") + + '' + chmod -R u+w . + find . -name CMakeLists.txt -exec sed -i \ + -e 's/\(find_package(Python[^)]*\) REQUIRED/\1/' \ + {} + + ''; + }); + + # catch 2.13.10 is the catch2 attr above and already built. + # catch2-3.14.0 is catch2_3. That derivation still ran cmake + # with development and testing on, then failed the interpreter + # find. Do not alias catch2 to catch2_3. + catch2_3 = prev.catch2_3.overrideAttrs (old: { + cmakeFlags = (old.cmakeFlags or [ ]) ++ [ + "-DCATCH_DEVELOPMENT_BUILD=OFF" + "-DCATCH_BUILD_TESTING=OFF" + ]; + doCheck = false; + nativeCheckInputs = [ ]; + postPatch = + (old.postPatch or "") + + '' + chmod -R u+w . + find . -name CMakeLists.txt -exec sed -i \ + -e 's/\(find_package(Python[^)]*\) REQUIRED/\1/' \ + {} + + ''; + }); + + # zeromq's cmake build already produced the library. postBuild + # always runs asciidoc in ../doc. Skip that doc pass. + zeromq = prev.zeromq.overrideAttrs (_old: { + postBuild = "true"; + postInstall = "true"; + }); + + # doxygen 1.16.1 fails in CMakeLists.txt when the interpreter + # probe cannot find Python_EXECUTABLE. Point the probe at the + # store stub and mark the interpreter found. The doxygen binary + # still installs. Do not add an interpreter package. + # Bundled spdlog, fmt, and sqlite are the dep targets still + # compiling when make stops on the all target. Use the system + # libraries so those bundled trees are not built. + doxygen = + let + # The doxygen binary links libvhdlparser, so the library stays. + # The javacc adjust step is a C program, not the interpreter. + vhdlAdjust = prev.runCommand "vhdl-adjust" { + nativeBuildInputs = [ prev.stdenv.cc ]; + } '' + mkdir -p "$out/bin" + $CC -O2 -std=c11 -o "$out/bin/vhdl-adjust" ${./packages/vhdl-adjust.c} + ''; + cmakeInterpreterStub = prev.runCommand "cmake-interpreter-stub" { + nativeBuildInputs = [ prev.stdenv.cc ]; + } '' + mkdir -p "$out/bin" + $CC -O2 -std=c11 -o "$out/bin/cmake-interpreter-stub" ${./packages/cmake-interpreter-stub.c} + ''; + in + prev.doxygen.overrideAttrs (old: { + buildInputs = (old.buildInputs or [ ]) ++ [ + prev.spdlog + prev.fmt + prev.sqlite + ]; + cmakeFlags = (old.cmakeFlags or [ ]) ++ [ + "-DPython_EXECUTABLE=${cmakeInterpreterStub}/bin/cmake-interpreter-stub" + "-Duse_sys_spdlog=ON" + "-Duse_sys_fmt=ON" + "-Duse_sys_sqlite3=ON" + ]; + postPatch = + (old.postPatch or "") + + '' + chmod -R u+w . + find . -name CMakeLists.txt | while IFS= read -r cm; do + awk ' + function sets() { + print "set(Python_FOUND TRUE)" + print "set(Python_Interpreter_FOUND TRUE)" + print "set(Python_EXECUTABLE \"${cmakeInterpreterStub}/bin/cmake-interpreter-stub\")" + } + BEGIN { depth = 0; in_find = 0 } + { + if (in_find) { + print "# " $0 + nopen = gsub(/\(/, "(") + nclose = gsub(/\)/, ")") + depth += nopen - nclose + if (depth <= 0) { + in_find = 0 + sets() + } + next + } + if ($0 ~ /find_package[[:space:]]*\([[:space:]]*Python/) { + print "# " $0 + nopen = gsub(/\(/, "(") + nclose = gsub(/\)/, ")") + depth = nopen - nclose + if (depth <= 0) { + sets() + } else { + in_find = 1 + } + next + } + print + } + ' "$cm" > "$cm.nopydoc" && mv "$cm.nopydoc" "$cm" || exit 1 + done + # vhdlparser's generator is optional. The release tree already + # has the parser sources. Drop that target so make does not + # stop on vhdlparser/CMakeFiles/vhdlparser.dir/all. + sed -i '/add_subdirectory[[:space:]]*([[:space:]]*vhdlparser[[:space:]]*)/d' CMakeLists.txt + sed -i '/^[[:space:]]*vhdlparser[[:space:]]*$/d' src/CMakeLists.txt + ''; + }); + + # A // on the llvmPackages alias does not change + # llvmPackages_21, so compiler-rt-libc and tblgen kept the same + # drv hashes. overrideScope changes that set. The package that + # actually builds is libllvm. The attribute llvm is only + # libllvm under another name, so replacing llvm leaves the + # library derivation stock. Same split for libclang and the + # wrapped clang driver. Patch libllvm and libclang. Leave the + # wrapped clang driver alone. + # Comment out every Python2 and Python3 find_package, + # including a later Python2 fallback that is still required + # after the first find is only a warning. Mark both found and + # point the executables at the store stub so the fallback block + # does not SEND_ERROR. The stub prints a version line. LLVM_INCLUDE_UTILS=OFF + # skips the llvm-lit block that calls make_paths_relative. TableGen is still added + # by LLVM CMakeLists.txt. Do not add an interpreter package. + llvmPackages_21 = prev.llvmPackages_21.overrideScope ( + _finalScope: prevScope: + let + cmakeInterpreterStub = prev.runCommand "cmake-interpreter-stub" { + nativeBuildInputs = [ prev.stdenv.cc ]; + } '' + mkdir -p "$out/bin" + $CC -O2 -std=c11 -o "$out/bin/cmake-interpreter-stub" ${./packages/cmake-interpreter-stub.c} + ''; + dropPythonFind = + pkg: + pkg.overrideAttrs (oldAttrs: { + cmakeFlags = (oldAttrs.cmakeFlags or [ ]) ++ [ + "-DPython3_EXECUTABLE=${cmakeInterpreterStub}/bin/cmake-interpreter-stub" + "-DLLVM_INCLUDE_TESTS=OFF" + "-DLLVM_INCLUDE_UTILS=OFF" + ]; + # nixpkgs also appends cmakeFlagsArray in preConfigure. + # Repeat the executable there so the probe cannot fall + # back to a missing interpreter. + preConfigure = (oldAttrs.preConfigure or "") + '' + cmakeFlagsArray+=( + -DPython3_EXECUTABLE=${cmakeInterpreterStub}/bin/cmake-interpreter-stub + ) + ''; + # opt-viewer is the only reason the python output is filled. + # The libraries stay when that directory was not built. + postInstall = builtins.replaceStrings + [ "mv $out/share/opt-viewer $python/share/opt-viewer" ] + [ "if [ -d \"$out/share/opt-viewer\" ]; then mv \"$out/share/opt-viewer\" \"$python/share/opt-viewer\"; fi" ] + (oldAttrs.postInstall or ""); + doCheck = false; + nativeCheckInputs = [ ]; + postPatch = + (oldAttrs.postPatch or "") + + '' + chmod -R u+w . + # Polly adds its test suite with no tests guard. That + # suite depends on the utils targets count and not. + # Those stay off for the library build. The library + # does not need the suite. + for cm in ../polly/CMakeLists.txt tools/polly/CMakeLists.txt polly/CMakeLists.txt; do + if [ -f "$cm" ]; then + sed -i '/add_subdirectory[[:space:]]*([[:space:]]*test[[:space:]]*)/d' "$cm" + fi + done + rewrite_llvm_cm() { + cm="$1" + if [ ! -f "$cm" ]; then + return 0 + fi + awk ' + index($0, "find_package(Python3 ''${LLVM_MINIMUM_PYTHON_VERSION} REQUIRED") { + print "# find_package(Python3 ''${LLVM_MINIMUM_PYTHON_VERSION} REQUIRED" + print "set(Python3_FOUND TRUE)" + print "set(Python3_Interpreter_FOUND TRUE)" + print "set(Python3_EXECUTABLE \"${cmakeInterpreterStub}/bin/cmake-interpreter-stub\")" + print "set(Python3_VERSION \"3.12.0\")" + print "set(Python3_VERSION_STRING \"3.12.0\")" + print "set(Python3_VERSION_MAJOR \"3\")" + print "set(Python3_VERSION_MINOR \"12\")" + skip = 1 + next + } + skip && index($0, "COMPONENTS Interpreter)") { + skip = 0 + next + } + { print } + ' "$cm" > "$cm.llvmline" && mv "$cm.llvmline" "$cm" + } + rewrite_llvm_cm llvm/CMakeLists.txt + if [ ! -f llvm/CMakeLists.txt ]; then + rewrite_llvm_cm CMakeLists.txt + fi + find . \( -name CMakeLists.txt -o -name '*.cmake' \) | while IFS= read -r cm; do + awk ' + function stub() { + print "set(Python3_FOUND TRUE)" + print "set(Python3_Interpreter_FOUND TRUE)" + print "set(Python3_EXECUTABLE \"${cmakeInterpreterStub}/bin/cmake-interpreter-stub\")" + print "set(Python3_VERSION \"3.12.0\")" + print "set(Python3_VERSION_STRING \"3.12.0\")" + print "set(Python3_VERSION_MAJOR \"3\")" + print "set(Python3_VERSION_MINOR \"12\")" + print "set(Python2_FOUND TRUE)" + print "set(Python2_Interpreter_FOUND TRUE)" + print "set(Python2_EXECUTABLE \"${cmakeInterpreterStub}/bin/cmake-interpreter-stub\")" + print "set(Python2_VERSION \"2.7.18\")" + print "set(Python_FOUND TRUE)" + print "set(Python_EXECUTABLE \"${cmakeInterpreterStub}/bin/cmake-interpreter-stub\")" + } + BEGIN { depth = 0; in_find = 0; did = 0 } + { + if (!in_find && $0 ~ /^[[:space:]]*#/) { + print + next + } + if (in_find) { + print "# " $0 + nopen = gsub(/\(/, "(") + nclose = gsub(/\)/, ")") + depth += nopen - nclose + if (depth <= 0) { + in_find = 0 + if (!did) { stub(); did = 1 } + } + next + } + if ($0 ~ /find_package[[:space:]]*\([[:space:]]*Python([23]|Interp|Libs)?([^[:alnum:]_]|$)/) { + print "# " $0 + nopen = gsub(/\(/, "(") + nclose = gsub(/\)/, ")") + depth = nopen - nclose + if (depth <= 0) { + if (!did) { stub(); did = 1 } + } else { + in_find = 1 + } + next + } + print + } + ' "$cm" > "$cm.nopypatch" && mv "$cm.nopypatch" "$cm" || exit 1 + done + if grep -R -n -E '^[[:space:]]*find_package[[:space:]]*\([[:space:]]*Python([23]|Interp|Libs)?([^[:alnum:]_]|$)' \ + --include=CMakeLists.txt --include='*.cmake' . ; then + echo "interpreter find_package still active" >&2 + exit 1 + fi + # The Linux version-script command is an inline "-c" + # snippet. The stub leaves an empty exports file and + # ld.bfd reports a syntax error at line 0. Write a real + # version script when that snippet contains local:. + if find . -name AddLLVM.cmake -type f | grep -q .; then + mkdir -p cmake + $CC -O2 -o cmake/llvm-version-script ${./packages/llvm-version-script.c} + tool=$(pwd)/cmake/llvm-version-script + find . -name AddLLVM.cmake -type f > cmake/addllvm.list + while IFS= read -r cm; do + awk -v tool="$tool" ' + index($0, "EXECUTABLE") && index($0, "\"-c\"") { + buf = $0 + seen = 0 + got = 0 + while ((getline line) > 0) { + if (index(line, "local:") > 0) { + seen = 1 + } + if (index(line, "DEPENDS") > 0) { + if (seen) { + match($0, /^[[:space:]]*/) + print substr($0, 1, RLENGTH) "COMMAND " tool " ''${native_export_file}" + } else { + print buf + } + print line + got = 1 + break + } + buf = buf "\n" line + } + if (!got) { + print buf + } + next + } + { print } + ' "$cm" > "$cm.vers" && mv "$cm.vers" "$cm" || exit 1 + done < cmake/addllvm.list + fi + ''; + }); + in + { + # The shared helper turns utils off so the big llvm build does + # not configure llvm-lit. This package's ninja line still asks + # for clang-tblgen, llvm-tblgen, and the two other generators. + # Those targets exist only when utils are on. Tests stay off, + # which is what avoided the earlier make_paths_relative stop. + tblgen = (dropPythonFind prevScope.tblgen).overrideAttrs (tgOld: { + cmakeFlags = + (lib.filter (flag: flag != "-DLLVM_INCLUDE_UTILS=OFF") (tgOld.cmakeFlags or [ ])) + ++ [ "-DLLVM_INCLUDE_UTILS=ON" ]; + }); + "compiler-rt" = dropPythonFind prevScope."compiler-rt"; + "compiler-rt-libc" = + (dropPythonFind prevScope."compiler-rt-libc").overrideAttrs (rtOld: { + postPatch = + (rtOld.postPatch or "") + + '' + inc=lib/sanitizer_common/sanitizer_common_interceptors.inc + if [ ! -f "$inc" ]; then + inc=compiler-rt/lib/sanitizer_common/sanitizer_common_interceptors.inc + fi + sed -i '3582s/__sanitizer_iovec local_iovec;/__sanitizer_iovec local_iovec = {};/' "$inc" + su=$(find . -name SanitizerUtils.cmake -print -quit) + if [ -n "$su" ]; then + mkdir -p cmake + cat > cmake/write-vers.sh << 'END' +#!/bin/sh +out= +prev= +for arg in "$@"; do + if [ "$prev" = "-o" ]; then + out=$arg + fi + prev=$arg +done +if [ -z "$out" ]; then + exit 0 +fi +printf '%s\n' '{' 'global:' ' *;' 'local:' ' *;' '};' > "$out" +exit 0 +END + chmod +x cmake/write-vers.sh + awk ' + index($0, "COMMAND") && index($0, "Python3_EXECUTABLE") { + line = $0 + if (getline nxt <= 0) { + print line + next + } + if (index(nxt, "--version-list")) { + print " COMMAND sh ''${CMAKE_SOURCE_DIR}/cmake/write-vers.sh -o ''${vers}" + next + } + print line + print nxt + next + } + { print } + ' "$su" > "$su.new" && mv "$su.new" "$su" + fi + ''; + # Install asks for a version script beside the ubsan archive. + # The build produces the archive and does not write the + # sibling. Write it after the archive exists and before + # cmake copies it. The archives that already exist still + # install. Do not restore a bare /bin/true stub. + preInstall = + (rtOld.preInstall or "") + + '' + find . -name 'libclang_rt*.a' -print | while IFS= read -r lib; do + syms="$lib.syms" + if [ ! -f "$syms" ]; then + printf '%s\n' '{' 'global:' ' *;' 'local:' ' *;' '};' > "$syms" + fi + done + ''; + }); + libllvm = dropPythonFind prevScope.libllvm; + # clang-unwrapped is this attribute. LLVM's installed + # AddLLVM.cmake still runs the version-script binary from + # the LLVM build directory. That file was not installed. + # Create it in this build only. Do not change libllvm. + libclang = (dropPythonFind prevScope.libclang).overrideAttrs (clOld: { + # DecisionForest.cpp includes CompletionModel.h, which is not + # in the tree. CLANG_ENABLE_CLANGD defaults on and compiles + # that file. Turn the clangd target off. Do not edit + # dropPythonFind. The llvm-version-script preBuild stays. + cmakeFlags = + (lib.filter (flag: flag != "-DCLANG_ENABLE_CLANGD=ON") (clOld.cmakeFlags or [ ])) + ++ [ "-DCLANG_ENABLE_CLANGD=OFF" ]; + # The -D flag is not enough: CMake still adds clangd, and + # DecisionForest.cpp includes CompletionModel.h, which is + # not in the tree. Append the flag again after nixpkgs + # cmakeFlagsArray, and drop the subdirectory below. + preConfigure = + (clOld.preConfigure or "") + + '' + cmakeFlagsArray+=( + -DCLANG_ENABLE_CLANGD=OFF + ) + ''; + # Point the bundle command at a shell bundler. cmake runs + # that command twice, and the second call has no arguments. + # A no-argument call exits 0. The first call still writes + # HTMLLogger.inc. Keep the version-script preBuild below. + postPatch = + (clOld.postPatch or "") + + '' + # bundle-html is not this failure. clangd's + # DecisionForest.cpp includes CompletionModel.h. Do not + # run an interpreter to generate it. Remove the clangd + # target, drop that translation unit, and leave a header + # so a leftover include still resolves. + # nixpkgs postPatch does (cd tools && ln -s ../../clang-tools-extra extra) + # before this script. find -type f does not walk that symlink. + # grep -R does, so tools/extra/CMakeLists.txt:42 still has + # add_subdirectory(clangd). Edit the symlink target too. + strip_clangd_cmake() { + cm="$1" + [ -f "$cm" ] || return 0 + parent=$(dirname "$cm") + # clang-tools-extra is outside the clang source root. + # Creating CMakeLists.txt.noclangd needs a new directory + # entry and dies with Permission denied. chmod the + # existing file and overwrite it from a temp. + chmod u+w "$parent" 2>/dev/null || true + if [ -L "$cm" ]; then + followed=$(mktemp) + cp -L "$cm" "$followed" + rm -f "$cm" + cat "$followed" > "$cm" + rm -f "$followed" + fi + chmod u+w "$cm" + tmp=$(mktemp) + awk ' + index($0, "add_subdirectory(clangd)") { next } + index($0, "add_clang_subdirectory(clangd)") { next } + $0 ~ /^[[:space:]]*DecisionForest\.cpp[[:space:]]*$/ { next } + { print } + ' "$cm" > "$tmp" + cat "$tmp" > "$cm" + rm -f "$tmp" + } + find . -name CMakeLists.txt -type f | while IFS= read -r cm; do + strip_clangd_cmake "$cm" + done + if [ -L tools/extra ]; then + extra_real=$(readlink -f tools/extra) + find "$extra_real" -name CMakeLists.txt -type f | while IFS= read -r cm; do + strip_clangd_cmake "$cm" + done + fi + # find -type f does not walk the tools/extra symlink. + # grep -R does, so tools/extra/CMakeLists.txt:42 still + # has add_subdirectory(clangd). Name that path. The + # helper follows the symlink when the file itself is one. + if [ -e tools/extra/CMakeLists.txt ] || [ -L tools/extra/CMakeLists.txt ]; then + strip_clangd_cmake tools/extra/CMakeLists.txt + fi + if grep -R -n -E 'add_subdirectory[[:space:]]*\([[:space:]]*clangd[[:space:]]*\)' \ + --include=CMakeLists.txt . ; then + echo "clangd subdirectory still configured" >&2 + exit 1 + fi + find . -name DecisionForest.cpp -type f | while IFS= read -r src; do + dir=$(dirname "$src") + cat > "$dir/CompletionModel.h" << 'END' +#pragma once +namespace clang { +namespace clangd { +class DecisionForest { +public: + DecisionForest() = default; +}; +} +} +END + done + cm=$(find . -path '*/Analysis/FlowSensitive/CMakeLists.txt' -print -quit) + if [ -z "$cm" ]; then + echo "HTMLLogger CMakeLists missing" >&2 + exit 1 + fi + dir=$(dirname "$cm") + cat > "$dir/bundle-html-logger.sh" << 'BUNDLE' +#!/bin/sh +if [ "$#" -eq 0 ]; then + exit 0 +fi +out=$1 +shift +: > "$out" +for f in "$@"; do + base=$(basename "$f") + var=$(printf '%s\n' "$base" | tr '.' '_') + printf 'const char %s[] =\n' "$var" >> "$out" + awk '{ printf " R\"x(%s)x\" \"\\n\"\n", $0 }' "$f" >> "$out" + printf ' ;\n' >> "$out" +done +BUNDLE + chmod 755 "$dir/bundle-html-logger.sh" + awk ' + index($0, "bundle_resources.py") { + print " COMMAND sh ''${CMAKE_CURRENT_SOURCE_DIR}/bundle-html-logger.sh" + next + } + { print } + ' "$cm" > "$cm.bundle" && mv "$cm.bundle" "$cm" + if ! grep -q bundle-html-logger.sh "$cm"; then + echo "HTMLLogger bundle command was not rewritten" >&2 + exit 1 + fi + ''; + preBuild = + (clOld.preBuild or "") + + '' + mkdir -p /build/llvm-src-21.1.8/llvm/cmake + $CC -O2 -std=c11 -o /build/llvm-src-21.1.8/llvm/cmake/llvm-version-script ${./packages/llvm-version-script.c} + ''; + }); + lld = dropPythonFind prevScope.lld; + } + ); + llvmPackages = final.llvmPackages_21; }; in { diff --git a/nix/packages/auto-patchelf-hook.sh b/nix/packages/auto-patchelf-hook.sh new file mode 100644 index 0000000..b32d02e --- /dev/null +++ b/nix/packages/auto-patchelf-hook.sh @@ -0,0 +1,37 @@ +autoPatchelf() { + local norecurse="" + while [ $# -gt 0 ]; do + case "$1" in + --no-recurse) + norecurse=1 + shift + ;; + --) + shift + break + ;; + *) + break + ;; + esac + done + if [ $# -eq 0 ]; then + return 0 + fi + if [ -n "$norecurse" ]; then + auto-patchelf --no-recurse "$@" + else + auto-patchelf "$@" + fi +} + +postFixupHooks+=(' + if [ -z "${dontAutoPatchelf-}" ]; then + autoPatchelf -- $( + for output in $outputs; do + [ -e "${!output}" ] || continue + printf "%s\n" "${!output}" + done + ) + fi +') diff --git a/nix/packages/auto-patchelf.sh b/nix/packages/auto-patchelf.sh new file mode 100644 index 0000000..f40ee94 --- /dev/null +++ b/nix/packages/auto-patchelf.sh @@ -0,0 +1,204 @@ +#!/bin/sh +# Set ELF interpreters and RPATH with patchelf. Search -L paths from +# NIX_LDFLAGS, cc-wrapper libc nix-support, each target lib directory, +# and runtimeDependencies. +set -eu + +norecurse=0 +ignore_missing=0 +if [ -n "${autoPatchelfIgnoreMissingDeps-}" ]; then + ignore_missing=1 +fi + +targets="" +while [ $# -gt 0 ]; do + case "$1" in + --no-recurse) + norecurse=1 + shift + ;; + --ignore-missing) + ignore_missing=1 + shift + ;; + --) + shift + break + ;; + *) + targets="$targets $1" + shift + ;; + esac +done +if [ $# -gt 0 ]; then + targets="$targets $*" +fi + +if [ -z "$targets" ]; then + echo "auto-patchelf: no paths" >&2 + exit 1 +fi + +interp="" +if [ -n "${NIX_CC-}" ] && [ -e "$NIX_CC/nix-support/dynamic-linker" ]; then + interp=$(cat "$NIX_CC/nix-support/dynamic-linker") +fi + +search="" + +# -Lpath, or -L as its own word with the path in the next word. +add_dash_l() { + prev_l=0 + for word in $1; do + if [ "$prev_l" -eq 1 ]; then + search="$search $word" + prev_l=0 + continue + fi + case "$word" in + -L) + prev_l=1 + ;; + -L*) + search="$search ${word#-L}" + ;; + esac + done +} + +# One store path. Drop surrounding whitespace only. +trim_ws() { + tw=$1 + while :; do + case "$tw" in + [[:space:]]*) tw=${tw#?} ;; + *) break ;; + esac + done + while :; do + case "$tw" in + *[[:space:]]) tw=${tw%?} ;; + *) break ;; + esac + done + printf '%s\n' "$tw" +} + +add_dash_l "${NIX_LDFLAGS-}" + +# cc-wrapper records libc outside NIX_LDFLAGS. Those sonames live in lib/. +if [ -n "${NIX_CC-}" ]; then + if [ -f "$NIX_CC/nix-support/orig-libc" ]; then + libc_store=$(cat "$NIX_CC/nix-support/orig-libc") + libc_store=$(trim_ws "$libc_store") + if [ -n "$libc_store" ] && [ -d "$libc_store/lib" ]; then + search="$search $libc_store/lib" + fi + fi + if [ -f "$NIX_CC/nix-support/libc-ldflags" ]; then + add_dash_l "$(cat "$NIX_CC/nix-support/libc-ldflags")" + fi + if [ -f "$NIX_CC/nix-support/orig-libc-ldflags" ]; then + add_dash_l "$(cat "$NIX_CC/nix-support/orig-libc-ldflags")" + fi +fi + +for dep in ${runtimeDependencies-}; do + if [ -d "$dep/lib" ]; then + search="$search $dep/lib" + fi +done +for target in $targets; do + if [ -d "$target/lib" ]; then + search="$search $target/lib" + fi +done + +skipped_needed() { + case "$1" in + linux-vdso.so.* | ld-linux*.so.* | ld-linux*.so) + return 0 + ;; + *) + return 1 + ;; + esac +} + +find_lib() { + lib="$1" + for dir in $search; do + if [ -e "$dir/$lib" ] || [ -L "$dir/$lib" ]; then + printf '%s\n' "$dir" + return 0 + fi + done + return 1 +} + +missing_any=0 + +patch_one() { + file="$1" + if ! patchelf --print-needed "$file" >/dev/null 2>&1; then + return 0 + fi + rpath="" + missing="" + needed=$(patchelf --print-needed "$file" || true) + for lib in $needed; do + if skipped_needed "$lib"; then + continue + fi + if dir=$(find_lib "$lib"); then + case " $rpath " in + *" $dir "*) ;; + *) rpath="$rpath $dir" ;; + esac + else + missing="$missing $lib" + fi + done + if [ -n "$missing" ]; then + echo "auto-patchelf: $file missing:$missing" >&2 + if [ "$ignore_missing" -ne 1 ]; then + missing_any=1 + fi + fi + rpath_colon="" + for dir in $rpath; do + if [ -z "$rpath_colon" ]; then + rpath_colon="$dir" + else + rpath_colon="$rpath_colon:$dir" + fi + done + if [ -n "$rpath_colon" ]; then + patchelf --set-rpath "$rpath_colon" "$file" + fi + if [ -n "$interp" ] && patchelf --print-interpreter "$file" >/dev/null 2>&1; then + patchelf --set-interpreter "$interp" "$file" + fi +} + +for target in $targets; do + if [ -d "$target" ]; then + if [ "$norecurse" -eq 1 ]; then + files=$(find "$target" -maxdepth 1 -type f) + else + files=$(find "$target" -type f) + fi + # No pipe. A pipe would run patch_one in a subshell and drop + # missing_any, so a missing library would look like success. + for file in $files; do + patch_one "$file" + done + elif [ -f "$target" ]; then + patch_one "$target" + fi +done + +if [ "$missing_any" -ne 0 ]; then + exit 1 +fi diff --git a/nix/packages/bpf-delegate-configs.c b/nix/packages/bpf-delegate-configs.c new file mode 100644 index 0000000..02d2f90 --- /dev/null +++ b/nix/packages/bpf-delegate-configs.c @@ -0,0 +1,451 @@ +/* Two arguments: mode (code or doc) and a header path. + Prints bpf delegate tables for bpf_cmd, bpf_map_type, bpf_prog_type, + and bpf_attach_type. Any other argc exits 1 and prints usage. +*/ +#include +#include +#include + +struct names { + char **item; + size_t len; + size_t cap; +}; + +static int py_space(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || + c == '\f' || (c >= 0x1c && c <= 0x1f); +} + +static int word_char(unsigned char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_'; +} + +static char ascii_upper(char c) +{ + if (c >= 'a' && c <= 'z') { + return (char)(c - 'a' + 'A'); + } + return c; +} + +static char ascii_lower(char c) +{ + if (c >= 'A' && c <= 'Z') { + return (char)(c - 'A' + 'a'); + } + return c; +} + +static void print_usage(const char *argv0) +{ + printf("Usage: %s
\n", argv0 != NULL ? argv0 : ""); +} + +static void strip_line(char *s) +{ + char *start; + size_t n; + + start = s; + while (*start != '\0' && py_space((unsigned char)*start)) { + start++; + } + if (start != s) { + memmove(s, start, strlen(start) + 1); + } + n = strlen(s); + while (n > 0 && py_space((unsigned char)s[n - 1])) { + n--; + s[n] = '\0'; + } +} + +static char *read_line(FILE *fp, int *err) +{ + size_t cap; + size_t n; + char *buf; + int c; + + *err = 0; + cap = 128; + n = 0; + buf = malloc(cap); + if (buf == NULL) { + *err = 1; + return NULL; + } + while ((c = fgetc(fp)) != EOF) { + if (n + 1 >= cap) { + size_t ncap; + char *grown; + + if (cap > ((size_t)-1) / 2) { + free(buf); + *err = 1; + return NULL; + } + ncap = cap * 2; + grown = realloc(buf, ncap); + if (grown == NULL) { + free(buf); + *err = 1; + return NULL; + } + buf = grown; + cap = ncap; + } + buf[n++] = (char)c; + if (c == '\n') { + break; + } + } + if (c == EOF && ferror(fp)) { + free(buf); + *err = 1; + return NULL; + } + if (n == 0) { + free(buf); + return NULL; + } + buf[n] = '\0'; + return buf; +} + +static void names_init(struct names *list) +{ + list->item = NULL; + list->len = 0; + list->cap = 0; +} + +static void names_clear(struct names *list) +{ + size_t i; + + for (i = 0; i < list->len; i++) { + free(list->item[i]); + list->item[i] = NULL; + } + list->len = 0; +} + +static void names_free(struct names *list) +{ + names_clear(list); + free(list->item); + list->item = NULL; + list->cap = 0; +} + +static int names_push(struct names *list, char *value) +{ + if (list->len == list->cap) { + size_t ncap; + char **grown; + + if (list->cap == 0) { + ncap = 32; + } else if (list->cap > ((size_t)-1) / (2 * sizeof *grown)) { + return -1; + } else { + ncap = list->cap * 2; + } + grown = realloc(list->item, ncap * sizeof *grown); + if (grown == NULL) { + return -1; + } + list->item = grown; + list->cap = ncap; + } + list->item[list->len++] = value; + return 0; +} + +static int match_alt(const char *p, const char **alt, const char **end) +{ + static const char *alts[] = { + "cmd", + "map_type", + "prog_type", + "attach_type", + }; + size_t i; + + for (i = 0; i < sizeof alts / sizeof alts[0]; i++) { + size_t n; + + n = strlen(alts[i]); + if (strncmp(p, alts[i], n) == 0) { + *alt = alts[i]; + *end = p + n; + return 1; + } + } + return 0; +} + +static int match_enum_start(const char *line, char *kind, size_t kindsz) +{ + const char *p; + const char *alt; + const char *end; + const char *last; + size_t n; + + p = line; + if (strncmp(p, "enum", 4) != 0) { + return 0; + } + p += 4; + if (*p == '\0' || !py_space((unsigned char)*p)) { + return 0; + } + while (py_space((unsigned char)*p)) { + p++; + } + if (strncmp(p, "bpf_", 4) != 0) { + return 0; + } + p += 4; + if (!match_alt(p, &alt, &end)) { + return 0; + } + last = alt; + p = end; + while (match_alt(p, &alt, &end)) { + last = alt; + p = end; + } + while (py_space((unsigned char)*p)) { + p++; + } + if (*p != '{') { + return 0; + } + n = strlen(last); + if (n + 1 > kindsz) { + return 0; + } + memcpy(kind, last, n + 1); + return 1; +} + +static int copy_enum_value(const char *line, char **out) +{ + size_t n; + char *value; + + *out = NULL; + n = 0; + while (word_char((unsigned char)line[n])) { + n++; + } + if (n == 0 || line[n] != ',' || line[n + 1] != '\0') { + return 0; + } + if (n >= 2 && line[0] == '_' && line[1] == '_') { + return 0; + } + value = malloc(n + 1); + if (value == NULL) { + return -1; + } + memcpy(value, line, n); + value[n] = '\0'; + *out = value; + return 1; +} + +static char *transform_ident(const char *ident) +{ + size_t n; + size_t i; + size_t o; + int first; + char *out; + + n = strlen(ident); + out = malloc(n + 1); + if (out == NULL) { + return NULL; + } + i = 0; + o = 0; + first = 1; + for (;;) { + size_t start; + size_t len; + size_t k; + + start = i; + while (ident[i] != '\0' && ident[i] != '_') { + i++; + } + len = i - start; + if (first) { + memcpy(out + o, ident + start, len); + o += len; + first = 0; + } else if (len > 0) { + out[o++] = ascii_upper(ident[start]); + for (k = 1; k < len; k++) { + out[o++] = ascii_lower(ident[start + k]); + } + } + if (ident[i] == '\0') { + break; + } + i++; + } + out[o] = '\0'; + return out; +} + +static int emit_enum(int doc, const char *enum_name, struct names *values) +{ + size_t i; + + if (!doc) { + if (printf("static const char* const %s_table[] = {\n", enum_name) < 0) { + return -1; + } + } else if (printf("\n", enum_name) < 0) { + return -1; + } + for (i = 0; i < values->len; i++) { + char *shown; + int nwr; + + shown = transform_ident(values->item[i]); + if (shown == NULL) { + return -1; + } + if (!doc) { + nwr = printf("\t\"%s\",\n", shown); + } else { + nwr = printf("%s\n", shown); + } + free(shown); + if (nwr < 0) { + return -1; + } + } + if (!doc) { + if (printf("};\n") < 0) { + return -1; + } + } else if (printf("\n") < 0) { + return -1; + } + return 0; +} + +int main(int argc, char **argv) +{ + const char *mode; + FILE *fp; + int doc; + int in_enum; + char kind[32]; + char enum_name[64]; + struct names values; + int rc; + + if (argc != 3) { + print_usage(argc > 0 ? argv[0] : NULL); + return 1; + } + mode = argv[1]; + if (strcmp(mode, "code") != 0 && strcmp(mode, "doc") != 0) { + printf("Invalid format: \n"); + print_usage(argv[0]); + return 1; + } + doc = strcmp(mode, "doc") == 0; + fp = fopen(argv[2], "r"); + if (fp == NULL) { + return 1; + } + if (doc) { + if (printf( + "\n" + "\n" + "\n" + "\n") < 0) { + fclose(fp); + return 1; + } + } + names_init(&values); + in_enum = 0; + enum_name[0] = '\0'; + rc = 0; + for (;;) { + int err; + char *line; + char *value; + int took; + + line = read_line(fp, &err); + if (line == NULL) { + if (err) { + rc = 1; + } + break; + } + strip_line(line); + if (in_enum) { + if (line[0] == '}') { + in_enum = 0; + if (emit_enum(doc, enum_name, &values) != 0) { + free(line); + rc = 1; + break; + } + names_clear(&values); + } else { + took = copy_enum_value(line, &value); + if (took < 0) { + free(line); + rc = 1; + break; + } + if (took > 0 && names_push(&values, value) != 0) { + free(value); + free(line); + rc = 1; + break; + } + } + } else if (match_enum_start(line, kind, sizeof kind)) { + int nw; + + in_enum = 1; + nw = snprintf(enum_name, sizeof enum_name, "bpf_delegate_%s", kind); + if (nw < 0 || (size_t)nw >= sizeof enum_name) { + free(line); + rc = 1; + break; + } + } + free(line); + } + names_free(&values); + if (fclose(fp) != 0) { + rc = 1; + } + if (rc == 0 && doc && printf("\n") < 0) { + rc = 1; + } + if (rc == 0 && fflush(stdout) != 0) { + rc = 1; + } + return rc; +} diff --git a/nix/packages/buildcatrust.c b/nix/packages/buildcatrust.c new file mode 100644 index 0000000..ccb5602 --- /dev/null +++ b/nix/packages/buildcatrust.c @@ -0,0 +1,2393 @@ +/* C replacement for the PyPI buildcatrust package. + * nss-cacert runs this binary in buildPhase. The PyPI package is + * Python and is not restored. This file is C only. + * + * Flags match pkgs/by-name/ca/cacert/package.nix at nixpkgs + * 7dcdd87373d72e7a2173e4db02e20d3d7f495cfd: + * --certdata_input, --ca_bundle_input, --blocklist, + * --ca_bundle_output, --ca_standard_bundle_output, + * --ca_unpacked_output, --ca_hashed_unpacked_output, + * --p11kit_output. + * --blocklist_input is the same flag as --blocklist. + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { + T_UNKNOWN = 0, + T_TRUSTED_DELEGATOR, + T_MUST_VERIFY, + T_NOT_TRUSTED, + T_TRUSTED +}; + +enum { + TR_SERVER = 0, + TR_CLIENT, + TR_CODE, + TR_DIGITAL, + TR_NONREP, + TR_KEYENC, + TR_DATAENC, + TR_KEYAGREE, + TR_KEYCERT, + TR_CRLSIGN, + TR_EMAIL, + TR_IPSEC_END, + TR_IPSEC_TUN, + TR_IPSEC_USER, + TR_TIME, + TR_COUNT +}; + +enum { C_BUILTIN = 1, C_CERT, C_TRUST }; + +enum { AV_BOOL, AV_TRUST, AV_CLASS, AV_STR, AV_BYTES }; + +typedef struct { + unsigned char *p; + size_t n, cap; +} Buf; + +typedef struct { + char *key; + int kind; + int num; + char *str; + unsigned char *bytes; + size_t nbytes; +} Av; + +typedef struct { + Av *a; + size_t n, cap; +} Obj; + +typedef struct { + unsigned comp[16]; + int ncomp; + char *text; +} Ava; + +typedef struct { + Ava *a; + int n, cap; +} Rdn; + +typedef struct { + Rdn *r; + int n, cap; +} Dn; + +typedef struct { + const char *dotted; + const char *name; + unsigned comp[12]; + int ncomp; + int trust_index; +} Purpose; + +typedef struct { + char *key; + char *label; + unsigned char *serial; + size_t nserial; + unsigned char *issuer; + size_t nissuer; + unsigned char *subject; + size_t nsubject; + unsigned char *cert; + size_t ncert; + int mozilla; + unsigned char *server_after; + size_t nserver_after; + unsigned char *email_after; + size_t nemail_after; + int has_cert; + int has_trust; + int trust[TR_COUNT]; +} Row; + +static Purpose g_purposes[9]; +static int g_npurposes; + +static const unsigned OID_EKU[] = {2, 5, 29, 37}; +static const unsigned OID_REJECT[] = {1, 3, 6, 1, 4, 1, 3319, 6, 10, 1}; +static const unsigned OID_DUMMY[] = {1, 3, 6, 1, 4, 1, 3319, 6, 10, 16}; + +static void die(const char *fmt, ...) { + va_list ap; + fputs("buildcatrust: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} + +static void *xmalloc(size_t n) { + void *p = malloc(n ? n : 1); + if (!p) { + die("out of memory"); + } + return p; +} + +static char *xstrdup(const char *s) { + size_t n = strlen(s); + char *d = xmalloc(n + 1); + memcpy(d, s, n + 1); + return d; +} + +static unsigned char *memdup(const unsigned char *p, size_t n) { + unsigned char *d = xmalloc(n + 1); + if (n) { + memcpy(d, p, n); + } + d[n] = 0; + return d; +} + +static void buf_need(Buf *b, size_t extra) { + if (b->n + extra + 1 <= b->cap) { + return; + } + size_t c = b->cap ? b->cap : 64; + while (c < b->n + extra + 1) { + c *= 2; + } + unsigned char *np = realloc(b->p, c); + if (!np) { + die("out of memory"); + } + b->p = np; + b->cap = c; +} + +static void buf_add(Buf *b, const void *src, size_t n) { + buf_need(b, n); + if (n) { + memcpy(b->p + b->n, src, n); + } + b->n += n; + b->p[b->n] = 0; +} + +static void buf_byte(Buf *b, unsigned char c) { + buf_add(b, &c, 1); +} + +static void buf_len(Buf *b, size_t n) { + if (n < 0x80) { + buf_byte(b, (unsigned char)n); + return; + } + unsigned char tmp[8]; + int k = 0; + size_t x = n; + while (x) { + tmp[k++] = (unsigned char)(x & 0xff); + x >>= 8; + } + buf_byte(b, (unsigned char)(0x80 | k)); + while (k) { + buf_byte(b, tmp[--k]); + } +} + +static void oid_base128(Buf *b, unsigned n) { + unsigned char tmp[8]; + int k = 0; + tmp[k++] = (unsigned char)(n & 0x7f); + n >>= 7; + while (n) { + tmp[k++] = (unsigned char)(0x80 | (n & 0x7f)); + n >>= 7; + } + while (k) { + buf_byte(b, tmp[--k]); + } +} + +static void der_oid(Buf *b, const unsigned *oid, int n) { + if (n < 2) { + die("oid shorter than two arcs"); + } + Buf body = {0}; + buf_byte(&body, (unsigned char)(40 * oid[0] + oid[1])); + for (int i = 2; i < n; i++) { + oid_base128(&body, oid[i]); + } + buf_byte(b, 0x06); + buf_len(b, body.n); + buf_add(b, body.p, body.n); + free(body.p); +} + +static int der_read_len(const unsigned char *p, size_t n, size_t off, size_t *len, size_t *next) { + if (off >= n) { + return -1; + } + unsigned char b = p[off]; + if (b < 0x80) { + *len = b; + *next = off + 1; + return 0; + } + if (b == 0x80) { + return -1; + } + int nbytes = b & 0x7f; + if (nbytes < 1 || nbytes > 4 || off + 1 + (size_t)nbytes > n) { + return -1; + } + size_t L = 0; + for (int i = 0; i < nbytes; i++) { + L = (L << 8) | p[off + 1 + i]; + } + *len = L; + *next = off + 1 + (size_t)nbytes; + return 0; +} + +static int der_tlv(const unsigned char *p, size_t n, size_t off, unsigned *tag, size_t *c0, + size_t *clen, size_t *next) { + if (off >= n) { + return -1; + } + *tag = p[off]; + if ((*tag & 0x1f) == 0x1f) { + return -1; + } + size_t len = 0; + size_t after = 0; + if (der_read_len(p, n, off + 1, &len, &after)) { + return -1; + } + if (after + len < after || after + len > n) { + return -1; + } + *c0 = after; + *clen = len; + *next = after + len; + return 0; +} + +static uint32_t rol32(uint32_t v, int n) { + return (v << n) | (v >> (32 - n)); +} + +static uint32_t ror32(uint32_t v, int n) { + return (v >> n) | (v << (32 - n)); +} + +static void sha1(const unsigned char *msg, size_t len, unsigned char out[20]) { + uint32_t h0 = 0x67452301u; + uint32_t h1 = 0xEFCDAB89u; + uint32_t h2 = 0x98BADCFEu; + uint32_t h3 = 0x10325476u; + uint32_t h4 = 0xC3D2E1F0u; + uint64_t bitlen = (uint64_t)len * 8u; + size_t newlen = len + 1; + while ((newlen % 64) != 56) { + newlen++; + } + unsigned char *buf = xmalloc(newlen + 8); + memset(buf, 0, newlen + 8); + if (len) { + memcpy(buf, msg ? msg : (const unsigned char *)"", len); + } + buf[len] = 0x80; + for (int i = 0; i < 8; i++) { + buf[newlen + i] = (unsigned char)(bitlen >> (56 - 8 * i)); + } + for (size_t off = 0; off < newlen + 8; off += 64) { + uint32_t w[80]; + for (int i = 0; i < 16; i++) { + w[i] = ((uint32_t)buf[off + 4 * i] << 24) | ((uint32_t)buf[off + 4 * i + 1] << 16) | + ((uint32_t)buf[off + 4 * i + 2] << 8) | (uint32_t)buf[off + 4 * i + 3]; + } + for (int i = 16; i < 80; i++) { + w[i] = rol32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + uint32_t a = h0, b = h1, c = h2, d = h3, e = h4; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + uint32_t temp = rol32(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = rol32(b, 30); + b = a; + a = temp; + } + h0 += a; + h1 += b; + h2 += c; + h3 += d; + h4 += e; + } + free(buf); + uint32_t hs[5] = {h0, h1, h2, h3, h4}; + for (int i = 0; i < 5; i++) { + out[4 * i] = (unsigned char)(hs[i] >> 24); + out[4 * i + 1] = (unsigned char)(hs[i] >> 16); + out[4 * i + 2] = (unsigned char)(hs[i] >> 8); + out[4 * i + 3] = (unsigned char)hs[i]; + } +} + +static void sha256(const unsigned char *msg, size_t len, unsigned char out[32]) { + static const uint32_t K[64] = { + 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, + 0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, + 0x9bdc06a7u, 0xc19bf174u, 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, + 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, + 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, + 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 0xa2bfe8a1u, 0xa81a664bu, + 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, 0x19a4c116u, + 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, + 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, + 0xc67178f2u}; + uint32_t h[8] = {0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au, + 0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u}; + uint64_t bitlen = (uint64_t)len * 8u; + size_t newlen = len + 1; + while ((newlen % 64) != 56) { + newlen++; + } + unsigned char *buf = xmalloc(newlen + 8); + memset(buf, 0, newlen + 8); + if (len) { + memcpy(buf, msg, len); + } + buf[len] = 0x80; + for (int i = 0; i < 8; i++) { + buf[newlen + i] = (unsigned char)(bitlen >> (56 - 8 * i)); + } + for (size_t off = 0; off < newlen + 8; off += 64) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) { + w[i] = ((uint32_t)buf[off + 4 * i] << 24) | ((uint32_t)buf[off + 4 * i + 1] << 16) | + ((uint32_t)buf[off + 4 * i + 2] << 8) | (uint32_t)buf[off + 4 * i + 3]; + } + for (int i = 16; i < 64; i++) { + uint32_t s0 = ror32(w[i - 15], 7) ^ ror32(w[i - 15], 18) ^ (w[i - 15] >> 3); + uint32_t s1 = ror32(w[i - 2], 17) ^ ror32(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7]; + for (int i = 0; i < 64; i++) { + uint32_t S1 = ror32(e, 6) ^ ror32(e, 11) ^ ror32(e, 25); + uint32_t ch = (e & f) ^ ((~e) & g); + uint32_t t1 = hh + S1 + ch + K[i] + w[i]; + uint32_t S0 = ror32(a, 2) ^ ror32(a, 13) ^ ror32(a, 22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + hh = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + h[0] += a; + h[1] += b; + h[2] += c; + h[3] += d; + h[4] += e; + h[5] += f; + h[6] += g; + h[7] += hh; + } + free(buf); + for (int i = 0; i < 8; i++) { + out[4 * i] = (unsigned char)(h[i] >> 24); + out[4 * i + 1] = (unsigned char)(h[i] >> 16); + out[4 * i + 2] = (unsigned char)(h[i] >> 8); + out[4 * i + 3] = (unsigned char)h[i]; + } +} + +static void hex_of(const unsigned char *dig, size_t n, char *out) { + static const char *hexd = "0123456789abcdef"; + for (size_t i = 0; i < n; i++) { + out[2 * i] = hexd[dig[i] >> 4]; + out[2 * i + 1] = hexd[dig[i] & 0xf]; + } + out[2 * n] = 0; +} + +static void serial_hex(const unsigned char *der, size_t n, char *out, size_t outcap) { + unsigned tag = 0; + size_t c0 = 0, clen = 0, next = 0; + if (der_tlv(der, n, 0, &tag, &c0, &clen, &next) || tag != 0x02 || next != n) { + die("serial number is not one DER integer"); + } + const unsigned char *p = der + c0; + size_t len = clen; + while (len > 1 && p[0] == 0) { + p++; + len--; + } + if (len == 1 && p[0] == 0) { + if (outcap < 2) { + die("serial hex buffer"); + } + out[0] = '0'; + out[1] = 0; + return; + } + char tmp[160]; + if (len > 64) { + die("serial number too long"); + } + hex_of(p, len, tmp); + const char *s = tmp; + if (s[0] == '0') { + s++; + } + if (strlen(s) + 1 > outcap) { + die("serial hex buffer"); + } + memcpy(out, s, strlen(s) + 1); +} + +static char *clean_label(const char *label) { + size_t n = strlen(label); + char *d = xmalloc(n + 1); + for (size_t i = 0; i < n; i++) { + char c = label[i]; + if (c == '/' || c == ' ' || c == ',') { + d[i] = '_'; + } else if (c == '(' || c == ')') { + d[i] = '='; + } else { + d[i] = c; + } + } + d[n] = 0; + return d; +} + +static char *make_key(const char *label, const unsigned char *serial, size_t nserial) { + char hex[160]; + serial_hex(serial, nserial, hex, sizeof hex); + char *clean = clean_label(label); + size_t n = strlen(clean) + 1 + strlen(hex) + 1; + char *key = xmalloc(n); + snprintf(key, n, "%s:%s", clean, hex); + free(clean); + return key; +} + +static int b64_val(int c) { + if (c >= 'A' && c <= 'Z') { + return c - 'A'; + } + if (c >= 'a' && c <= 'z') { + return c - 'a' + 26; + } + if (c >= '0' && c <= '9') { + return c - '0' + 52; + } + if (c == '+') { + return 62; + } + if (c == '/') { + return 63; + } + return -1; +} + +static void b64_emit(FILE *fp, int *col, char c) { + fputc(c, fp); + (*col)++; + if (*col == 64) { + fputc('\n', fp); + *col = 0; + } +} + +static void write_pem(FILE *fp, const char *name, const unsigned char *der, size_t n) { + static const char *b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + fprintf(fp, "-----BEGIN %s-----\n", name); + int col = 0; + size_t i = 0; + while (i + 3 <= n) { + unsigned v = ((unsigned)der[i] << 16) | ((unsigned)der[i + 1] << 8) | (unsigned)der[i + 2]; + b64_emit(fp, &col, b64[(v >> 18) & 63]); + b64_emit(fp, &col, b64[(v >> 12) & 63]); + b64_emit(fp, &col, b64[(v >> 6) & 63]); + b64_emit(fp, &col, b64[v & 63]); + i += 3; + } + if (i < n) { + unsigned a = der[i]; + unsigned b = (i + 1 < n) ? der[i + 1] : 0; + unsigned v = (a << 16) | (b << 8); + b64_emit(fp, &col, b64[(v >> 18) & 63]); + b64_emit(fp, &col, b64[(v >> 12) & 63]); + if (i + 1 < n) { + b64_emit(fp, &col, b64[(v >> 6) & 63]); + } else { + b64_emit(fp, &col, '='); + } + b64_emit(fp, &col, '='); + } + if (col) { + fputc('\n', fp); + } + fprintf(fp, "-----END %s-----\n", name); +} + +static int quote_ok(unsigned char c) { + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { + return 1; + } + return c == '-' || c == '.' || c == '_' || c == '~' || c == '/' || c == ' '; +} + +static void quote_bytes(FILE *fp, const unsigned char *p, size_t n) { + fputc('"', fp); + for (size_t i = 0; i < n; i++) { + unsigned char c = p[i]; + if (quote_ok(c)) { + fputc((int)c, fp); + } else { + fprintf(fp, "%%%02X", c); + } + } + fputc('"', fp); +} + +static int oid_eq(const unsigned *a, int na, const unsigned *b, int nb) { + if (na != nb) { + return 0; + } + for (int i = 0; i < na; i++) { + if (a[i] != b[i]) { + return 0; + } + } + return 1; +} + +static void purpose_add(const char *dotted, const char *name, const unsigned *comp, int ncomp, + int trust_index) { + Purpose *p = &g_purposes[g_npurposes++]; + p->dotted = dotted; + p->name = name; + p->ncomp = ncomp; + p->trust_index = trust_index; + for (int i = 0; i < ncomp; i++) { + p->comp[i] = comp[i]; + } +} + +static void init_purposes(void) { + const unsigned server[] = {1, 3, 6, 1, 5, 5, 7, 3, 1}; + const unsigned client[] = {1, 3, 6, 1, 5, 5, 7, 3, 2}; + const unsigned code[] = {1, 3, 6, 1, 5, 5, 7, 3, 3}; + const unsigned email[] = {1, 3, 6, 1, 5, 5, 7, 3, 4}; + const unsigned time[] = {1, 3, 6, 1, 5, 5, 7, 3, 8}; + const unsigned ipsec_end[] = {1, 3, 6, 1, 5, 5, 7, 3, 5}; + const unsigned ipsec_tun[] = {1, 3, 6, 1, 5, 5, 7, 3, 6}; + const unsigned ipsec_user[] = {1, 3, 6, 1, 5, 5, 7, 3, 7}; + purpose_add("1.3.6.1.5.5.7.3.1", "RFC5280: serverAuth key usage", server, 9, TR_SERVER); + purpose_add("1.3.6.1.5.5.7.3.2", "RFC5280: clientAuth key usage", client, 9, TR_CLIENT); + purpose_add("1.3.6.1.5.5.7.3.3", "RFC5280: codeSigning key usage", code, 9, TR_CODE); + purpose_add("1.3.6.1.5.5.7.3.4", "RFC5280: emailProtection key usage", email, 9, TR_EMAIL); + purpose_add("1.3.6.1.5.5.7.3.8", "RFC5280: timeStamping key usage", time, 9, TR_TIME); + purpose_add("1.3.6.1.5.5.7.3.5", "RFC2459: ipsecEndSystem key usage", ipsec_end, 9, TR_IPSEC_END); + purpose_add("1.3.6.1.5.5.7.3.6", "RFC2459: ipsecTunnel key usage", ipsec_tun, 9, TR_IPSEC_TUN); + purpose_add("1.3.6.1.5.5.7.3.7", "RFC2459: ipsecUser key usage", ipsec_user, 9, TR_IPSEC_USER); +} + +static const Purpose *purpose_by_oid(const unsigned *comp, int n) { + for (int i = 0; i < g_npurposes; i++) { + if (oid_eq(g_purposes[i].comp, g_purposes[i].ncomp, comp, n)) { + return &g_purposes[i]; + } + } + return NULL; +} + +typedef struct { + const char *name; + unsigned comp[12]; + int ncomp; +} ShortOid; + +static ShortOid g_shorts[11]; +static int g_nshorts; + +static void short_add(const char *name, const unsigned *comp, int n) { + ShortOid *s = &g_shorts[g_nshorts++]; + s->name = name; + s->ncomp = n; + for (int i = 0; i < n; i++) { + s->comp[i] = comp[i]; + } +} + +static void init_shorts(void) { + const unsigned c[] = {2, 5, 4, 6}; + const unsigned l[] = {2, 5, 4, 7}; + const unsigned cn[] = {2, 5, 4, 3}; + const unsigned o[] = {2, 5, 4, 10}; + const unsigned ou[] = {2, 5, 4, 11}; + const unsigned orgid[] = {2, 5, 4, 97}; + const unsigned name[] = {2, 5, 4, 41}; + const unsigned given[] = {2, 5, 4, 42}; + const unsigned serial[] = {2, 5, 4, 5}; + const unsigned biz[] = {2, 5, 4, 15}; + const unsigned jur[] = {1, 3, 6, 1, 4, 1, 311, 60, 2, 1, 3}; + short_add("c", c, 4); + short_add("l", l, 4); + short_add("cn", cn, 4); + short_add("o", o, 4); + short_add("ou", ou, 4); + short_add("organizationIdentifier", orgid, 4); + short_add("name", name, 4); + short_add("givenName", given, 4); + short_add("serialNumber", serial, 4); + short_add("businessCategory", biz, 4); + short_add("jurisdictionOfIncorporationCountryName", jur, 11); +} + +static const char *short_name(const unsigned *comp, int n) { + for (int i = 0; i < g_nshorts; i++) { + if (oid_eq(g_shorts[i].comp, g_shorts[i].ncomp, comp, n)) { + return g_shorts[i].name; + } + } + return NULL; +} + +static char *oid_dotted(const unsigned *comp, int n) { + char *s = xmalloc(n * 12 + 1); + s[0] = 0; + for (int i = 0; i < n; i++) { + char piece[16]; + snprintf(piece, sizeof piece, "%s%u", i ? "." : "", comp[i]); + strcat(s, piece); + } + return s; +} + +static int decode_base128(const unsigned char *p, size_t n, size_t *pos, unsigned *val) { + if (*pos >= n) { + return -1; + } + if (p[*pos] < 0x80) { + *val = p[*pos]; + (*pos)++; + return 0; + } + unsigned v = 0; + int guard = 0; + while (*pos < n) { + unsigned char b = p[*pos]; + (*pos)++; + v = (v << 7) | (unsigned)(b & 0x7f); + guard++; + if (guard > 5) { + return -1; + } + if ((b & 0x80) == 0) { + *val = v; + return 0; + } + } + return -1; +} + +static int parse_oid(const unsigned char *p, size_t n, size_t off, unsigned *comp, int *ncomp, + size_t *next) { + unsigned tag = 0; + size_t c0 = 0, clen = 0; + if (der_tlv(p, n, off, &tag, &c0, &clen, next) || tag != 0x06 || clen < 1) { + return -1; + } + unsigned first = p[c0]; + unsigned a0 = first / 40; + if (a0 > 2) { + a0 = 2; + } + comp[0] = a0; + comp[1] = first - a0 * 40; + int k = 2; + size_t pos = 1; + while (pos < clen) { + unsigned arc = 0; + if (decode_base128(p + c0, clen, &pos, &arc)) { + return -1; + } + if (k >= 16) { + return -1; + } + comp[k++] = arc; + } + *ncomp = k; + return 0; +} + +static int utf16be_to_utf8(const unsigned char *p, size_t n, char **out) { + if (n % 2) { + return -1; + } + Buf b = {0}; + for (size_t i = 0; i < n; i += 2) { + unsigned u = ((unsigned)p[i] << 8) | (unsigned)p[i + 1]; + if (u >= 0xD800 && u <= 0xDFFF) { + free(b.p); + return -1; + } + if (u < 0x80) { + buf_byte(&b, (unsigned char)u); + } else if (u < 0x800) { + buf_byte(&b, (unsigned char)(0xC0 | (u >> 6))); + buf_byte(&b, (unsigned char)(0x80 | (u & 0x3f))); + } else { + buf_byte(&b, (unsigned char)(0xE0 | (u >> 12))); + buf_byte(&b, (unsigned char)(0x80 | ((u >> 6) & 0x3f))); + buf_byte(&b, (unsigned char)(0x80 | (u & 0x3f))); + } + } + *out = (char *)(b.p ? b.p : (unsigned char *)xstrdup("")); + if (!b.p) { + /* xstrdup already allocated. The ternary above double-allocates if b.p is null. + * Keep the empty string from xstrdup. */ + } + return 0; +} + +static int dn_add_ava(Rdn *rdn, const unsigned *comp, int ncomp, const char *text) { + if (rdn->n == rdn->cap) { + rdn->cap = rdn->cap ? rdn->cap * 2 : 4; + rdn->a = realloc(rdn->a, (size_t)rdn->cap * sizeof(Ava)); + if (!rdn->a) { + die("out of memory"); + } + } + Ava *a = &rdn->a[rdn->n++]; + a->ncomp = ncomp; + for (int i = 0; i < ncomp; i++) { + a->comp[i] = comp[i]; + } + a->text = xstrdup(text); + return 0; +} + +static int parse_dn(const unsigned char *p, size_t n, Dn *dn) { + memset(dn, 0, sizeof *dn); + if (n == 0) { + return -1; + } + unsigned tag = 0; + size_t c0 = 0, clen = 0, next = 0; + if (der_tlv(p, n, 0, &tag, &c0, &clen, &next) || tag != 0x30 || next != n) { + return -1; + } + size_t off = c0; + size_t end = c0 + clen; + while (off < end) { + size_t s0 = 0, slen = 0, snext = 0; + if (der_tlv(p, n, off, &tag, &s0, &slen, &snext) || tag != 0x31) { + return -1; + } + if (dn->n == dn->cap) { + dn->cap = dn->cap ? dn->cap * 2 : 4; + dn->r = realloc(dn->r, (size_t)dn->cap * sizeof(Rdn)); + if (!dn->r) { + die("out of memory"); + } + } + Rdn *rdn = &dn->r[dn->n++]; + memset(rdn, 0, sizeof *rdn); + size_t ioff = s0; + size_t iend = s0 + slen; + while (ioff < iend) { + size_t a0 = 0, alen = 0, anext = 0; + if (der_tlv(p, n, ioff, &tag, &a0, &alen, &anext) || tag != 0x30) { + return -1; + } + unsigned comp[16]; + int ncomp = 0; + size_t onext = 0; + if (parse_oid(p, n, a0, comp, &ncomp, &onext)) { + return -1; + } + if (onext >= a0 + alen) { + return -1; + } + unsigned stag = 0; + size_t v0 = 0, vlen = 0, vnext = 0; + if (der_tlv(p, n, onext, &stag, &v0, &vlen, &vnext) || vnext != a0 + alen) { + return -1; + } + char *text = NULL; + if (stag == 0x0C || stag == 0x12 || stag == 0x13 || stag == 0x14 || stag == 0x16 || + stag == 0x1A) { + text = xmalloc(vlen + 1); + memcpy(text, p + v0, vlen); + text[vlen] = 0; + for (size_t k = 0; k < vlen; k++) { + if (stag != 0x0C && p[v0 + k] >= 0x80) { + free(text); + return -1; + } + } + } else if (stag == 0x1E) { + if (utf16be_to_utf8(p + v0, vlen, &text)) { + return -1; + } + } else { + return -1; + } + dn_add_ava(rdn, comp, ncomp, text); + free(text); + ioff = anext; + } + off = snext; + } + return 0; +} + +static void free_dn(Dn *dn) { + for (int i = 0; i < dn->n; i++) { + for (int j = 0; j < dn->r[i].n; j++) { + free(dn->r[i].a[j].text); + } + free(dn->r[i].a); + } + free(dn->r); +} + +static char *canon_text(const char *s) { + size_t n = strlen(s); + char *o = xmalloc(n + 1); + size_t w = 0; + int sp = 0; + int started = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f') { + if (started) { + sp = 1; + } + continue; + } + if (sp) { + o[w++] = ' '; + sp = 0; + } + if (c >= 'A' && c <= 'Z') { + c = (unsigned char)(c - 'A' + 'a'); + } + o[w++] = (char)c; + started = 1; + } + o[w] = 0; + return o; +} + +static void dn_canon(const Dn *dn, Buf *out) { + memset(out, 0, sizeof *out); + if (dn->n == 0) { + return; + } + for (int i = 0; i < dn->n; i++) { + Buf setb = {0}; + for (int j = 0; j < dn->r[i].n; j++) { + const Ava *a = &dn->r[i].a[j]; + char *text = canon_text(a->text); + Buf ava = {0}; + der_oid(&ava, a->comp, a->ncomp); + buf_byte(&ava, 0x0c); + size_t tn = strlen(text); + buf_len(&ava, tn); + buf_add(&ava, text, tn); + free(text); + buf_byte(&setb, 0x30); + buf_len(&setb, ava.n); + buf_add(&setb, ava.p, ava.n); + free(ava.p); + } + buf_byte(out, 0x31); + buf_len(out, setb.n); + buf_add(out, setb.p, setb.n); + free(setb.p); + } +} + +static char *ava_text(const Ava *a) { + const char *name = short_name(a->comp, a->ncomp); + char *oid = NULL; + const char *left = name; + if (!left) { + oid = oid_dotted(a->comp, a->ncomp); + left = oid; + } + size_t n = strlen(left) + 1 + strlen(a->text) + 1; + char *s = xmalloc(n); + snprintf(s, n, "%s=%s", left, a->text); + free(oid); + return s; +} + +static char *dn_last_part(const Dn *dn) { + if (dn->n == 0) { + return xstrdup(""); + } + const Rdn *rdn = &dn->r[dn->n - 1]; + if (rdn->n == 1) { + return ava_text(&rdn->a[0]); + } + Buf b = {0}; + buf_byte(&b, '{'); + for (int i = 0; i < rdn->n; i++) { + if (i) { + buf_byte(&b, ','); + } + char *one = ava_text(&rdn->a[i]); + buf_add(&b, one, strlen(one)); + free(one); + } + buf_byte(&b, '}'); + return (char *)b.p; +} + +static void subject_hash(const unsigned char *dn, size_t n, char out[9]) { + Dn parsed; + if (parse_dn(dn, n, &parsed)) { + die("could not parse a certificate subject"); + } + Buf canon = {0}; + dn_canon(&parsed, &canon); + unsigned char dig[20]; + sha1(canon.p, canon.n, dig); + free(canon.p); + free_dn(&parsed); + snprintf(out, 9, "%02x%02x%02x%02x", dig[3], dig[2], dig[1], dig[0]); +} + +typedef struct { + size_t serial_off, serial_len; + size_t issuer_off, issuer_len; + size_t subject_off, subject_len; + size_t spki_off, spki_len; +} CertParts; + +static int cert_parts(const unsigned char *cert, size_t n, CertParts *part) { + unsigned tag = 0; + size_t c0 = 0, clen = 0, next = 0; + if (der_tlv(cert, n, 0, &tag, &c0, &clen, &next) || tag != 0x30) { + return -1; + } + size_t t0 = 0, tlen = 0, tnext = 0; + if (der_tlv(cert, n, c0, &tag, &t0, &tlen, &tnext) || tag != 0x30) { + return -1; + } + size_t p = t0; + if (p < t0 + tlen && cert[p] == 0xA0) { + if (der_tlv(cert, n, p, &tag, &c0, &clen, &next)) { + return -1; + } + p = next; + } + size_t fields[5][2]; + unsigned expect[5] = {0x02, 0x30, 0x30, 0x30, 0x30}; + for (int i = 0; i < 5; i++) { + if (der_tlv(cert, n, p, &tag, &c0, &clen, &next) || tag != expect[i]) { + return -1; + } + fields[i][0] = p; + fields[i][1] = next - p; + p = next; + } + size_t spki = p; + if (der_tlv(cert, n, p, &tag, &c0, &clen, &next) || tag != 0x30) { + return -1; + } + part->serial_off = fields[0][0]; + part->serial_len = fields[0][1]; + part->issuer_off = fields[2][0]; + part->issuer_len = fields[2][1]; + part->subject_off = fields[4][0]; + part->subject_len = fields[4][1]; + part->spki_off = spki; + part->spki_len = next - spki; + return 0; +} + +static void trust_clear(int trust[TR_COUNT], int value) { + for (int i = 0; i < TR_COUNT; i++) { + trust[i] = value; + } +} + +static int any_purpose(const int trust[TR_COUNT], int value, const Purpose **list, int *nlist) { + int n = 0; + for (int i = 0; i < g_npurposes; i++) { + if (trust[g_purposes[i].trust_index] == value) { + if (list) { + list[n] = &g_purposes[i]; + } + n++; + } + } + if (nlist) { + *nlist = n; + } + return n; +} + +static void der_oid_list(Buf *out, unsigned tag, const Purpose **list, int n) { + Buf body = {0}; + for (int i = 0; i < n; i++) { + der_oid(&body, list[i]->comp, list[i]->ncomp); + } + buf_byte(out, (unsigned char)tag); + buf_len(out, body.n); + buf_add(out, body.p, body.n); + free(body.p); +} + +static void der_one_oid_list(Buf *out, unsigned tag, const unsigned *oid, int ncomp) { + Buf body = {0}; + der_oid(&body, oid, ncomp); + buf_byte(out, (unsigned char)tag); + buf_len(out, body.n); + buf_add(out, body.p, body.n); + free(body.p); +} + +static void openssl_aux(const int trust[TR_COUNT], Buf *out) { + const Purpose *trusted[9]; + const Purpose *rejected[9]; + int nt = 0, nr = 0; + any_purpose(trust, T_TRUSTED_DELEGATOR, trusted, &nt); + any_purpose(trust, T_NOT_TRUSTED, rejected, &nr); + Buf inner = {0}; + der_oid_list(&inner, 0x30, trusted, nt); + if (nr) { + der_oid_list(&inner, 0xA0, rejected, nr); + } + memset(out, 0, sizeof *out); + buf_byte(out, 0x30); + buf_len(out, inner.n); + buf_add(out, inner.p, inner.n); + free(inner.p); +} + +static void write_trust_lines(FILE *fp, const int trust[TR_COUNT]) { + const Purpose *trusted[9]; + const Purpose *rejected[9]; + int nt = 0, nr = 0; + any_purpose(trust, T_TRUSTED_DELEGATOR, trusted, &nt); + any_purpose(trust, T_NOT_TRUSTED, rejected, &nr); + if (nt) { + fputs("Trusted for:\n", fp); + for (int i = 0; i < nt; i++) { + fprintf(fp, " - %s (%s)\n", trusted[i]->dotted, trusted[i]->name); + } + } + if (nr) { + fputs("Rejected for:\n", fp); + for (int i = 0; i < nr; i++) { + fprintf(fp, " - %s (%s)\n", rejected[i]->dotted, rejected[i]->name); + } + } +} + +static void write_plain_or_omit(FILE *fp, const Row *row) { + fprintf(fp, "%s\n", row->label); + if (row->has_cert && row->trust[TR_SERVER] == T_TRUSTED_DELEGATOR) { + write_pem(fp, "CERTIFICATE", row->cert, row->ncert); + fputc('\n', fp); + } else if (row->has_cert) { + fputs("Traditional PEM block omitted: this certificate is not trusted for " + "authenticating servers.\n", + fp); + } +} + +static void write_openssl_row(FILE *fp, const Row *row) { + if (!row->has_cert) { + return; + } + write_plain_or_omit(fp, row); + write_trust_lines(fp, row->trust); + Buf aux = {0}; + openssl_aux(row->trust, &aux); + unsigned char *both = xmalloc(row->ncert + aux.n); + memcpy(both, row->cert, row->ncert); + if (aux.n) { + memcpy(both + row->ncert, aux.p, aux.n); + } + write_pem(fp, "TRUSTED CERTIFICATE", both, row->ncert + aux.n); + fputc('\n', fp); + free(both); + free(aux.p); +} + +static void write_standard_row(FILE *fp, const Row *row) { + if (!row->has_cert) { + return; + } + write_plain_or_omit(fp, row); +} + +static void der_cert_ext(Buf *out, const unsigned *ext_oid, int noid, const unsigned char *ext, + size_t next) { + Buf body = {0}; + der_oid(&body, ext_oid, noid); + unsigned char crit[] = {0x01, 0x01, 0xff}; + buf_add(&body, crit, 3); + buf_byte(&body, 0x04); + buf_len(&body, next); + buf_add(&body, ext, next); + memset(out, 0, sizeof *out); + buf_byte(out, 0x30); + buf_len(out, body.n); + buf_add(out, body.p, body.n); + free(body.p); +} + +static void write_ku_ext(FILE *fp, const Row *row, const char *oid_name, const char *oid_dotted, + const unsigned *ext_oid, int noid, const Purpose **usages, int nusage, + const unsigned *single, int nsingle, const char *single_dotted, + const char *single_name) { + Buf eku = {0}; + if (nusage >= 0 && usages) { + Buf body = {0}; + for (int i = 0; i < nusage; i++) { + der_oid(&body, usages[i]->comp, usages[i]->ncomp); + } + buf_byte(&eku, 0x30); + buf_len(&eku, body.n); + buf_add(&eku, body.p, body.n); + free(body.p); + } else { + Buf body = {0}; + der_oid(&body, single, nsingle); + buf_byte(&eku, 0x30); + buf_len(&eku, body.n); + buf_add(&eku, body.p, body.n); + free(body.p); + } + Buf ext = {0}; + der_cert_ext(&ext, ext_oid, noid, eku.p, eku.n); + fprintf(fp, "# %s\n", oid_name); + fputs("# value = [", fp); + if (usages) { + for (int i = 0; i < nusage; i++) { + if (i) { + fputs(", ", fp); + } + fprintf(fp, "%s (%s)", usages[i]->dotted, usages[i]->name); + } + } else { + fprintf(fp, "%s (%s)", single_dotted, single_name); + } + fputs("]\n", fp); + fputs("[p11-kit-object-v1]\n", fp); + fputs("label: ", fp); + quote_bytes(fp, (const unsigned char *)row->label, strlen(row->label)); + fputc('\n', fp); + fputs("modifiable: false\n", fp); + fputs("class: x-certificate-extension\n", fp); + fprintf(fp, "object-id: %s\n", oid_dotted); + fputs("value: ", fp); + quote_bytes(fp, ext.p, ext.n); + fputc('\n', fp); + CertParts part; + if (cert_parts(row->cert, row->ncert, &part)) { + die("could not read the subject public key of %s", row->label); + } + write_pem(fp, "PUBLIC KEY", row->cert + part.spki_off, part.spki_len); + fputc('\n', fp); + free(eku.p); + free(ext.p); +} + +static void write_p11_row(FILE *fp, const Row *row) { + fprintf(fp, "# %s\n", row->key); + if (!row->has_cert) { + if (!any_purpose(row->trust, T_NOT_TRUSTED, NULL, NULL)) { + die("trust %s has no certificate and is not distrusted", row->label); + } + fputs("[p11-kit-object-v1]\n", fp); + fputs("label: ", fp); + quote_bytes(fp, (const unsigned char *)row->label, strlen(row->label)); + fputc('\n', fp); + fputs("class: certificate\n", fp); + fputs("certificate-type: x-509\n", fp); + fputs("modifiable: false\n", fp); + fputs("issuer: ", fp); + quote_bytes(fp, row->issuer, row->nissuer); + fputc('\n', fp); + fputs("serial-number: ", fp); + quote_bytes(fp, row->serial, row->nserial); + fputc('\n', fp); + fputs("x-distrusted: true\n\n", fp); + return; + } + fputs("[p11-kit-object-v1]\n", fp); + fputs("label: ", fp); + quote_bytes(fp, (const unsigned char *)row->label, strlen(row->label)); + fputc('\n', fp); + fputs("modifiable: false\n", fp); + if (any_purpose(row->trust, T_NOT_TRUSTED, NULL, NULL)) { + fputs("x-distrusted: true\n\n", fp); + } else if (any_purpose(row->trust, T_TRUSTED_DELEGATOR, NULL, NULL)) { + fputs("trusted: true\n\n", fp); + } else { + fputs("trusted: false\n\n", fp); + } + int cert_attr = 0; + if (row->mozilla) { + fputs("nss-mozilla-ca-policy: true\n", fp); + cert_attr = 1; + } + if (row->nserver_after) { + fputs("nss-server-distrust-after: ", fp); + quote_bytes(fp, row->server_after, row->nserver_after); + fputc('\n', fp); + cert_attr = 1; + } + if (row->nemail_after) { + fputs("nss-email-distrust-after: ", fp); + quote_bytes(fp, row->email_after, row->nemail_after); + fputc('\n', fp); + cert_attr = 1; + } + if (cert_attr) { + fputc('\n', fp); + } + write_pem(fp, "CERTIFICATE", row->cert, row->ncert); + fputc('\n', fp); + + const Purpose *trusted[9]; + const Purpose *rejected[9]; + int nt = 0, nr = 0; + any_purpose(row->trust, T_TRUSTED_DELEGATOR, trusted, &nt); + any_purpose(row->trust, T_NOT_TRUSTED, rejected, &nr); + if (nr) { + write_ku_ext(fp, row, "p11kit OpenSSL reject extension", "1.3.6.1.4.1.3319.6.10.1", OID_REJECT, + 10, rejected, nr, NULL, 0, NULL, NULL); + } + if (nt) { + write_ku_ext(fp, row, "RFC5280: Extended Key Usage X.509 extension", "2.5.29.37", OID_EKU, 4, + trusted, nt, NULL, 0, NULL, NULL); + } else if (nr) { + write_ku_ext(fp, row, "RFC5280: Extended Key Usage X.509 extension", "2.5.29.37", OID_EKU, 4, + NULL, -1, OID_DUMMY, 10, "1.3.6.1.4.1.3319.6.10.16", + "p11kit reserved key purpose"); + } +} + +static int cmp_row(const void *a, const void *b) { + const Row *const *ra = a; + const Row *const *rb = b; + return strcmp((*ra)->key, (*rb)->key); +} + +static void write_bundle(const char *path, Row **rows, int n, int standard) { + if (!path) { + return; + } + FILE *fp = fopen(path, "wb"); + if (!fp) { + die("cannot write %s: %s", path, strerror(errno)); + } + for (int i = 0; i < n; i++) { + if (standard) { + write_standard_row(fp, rows[i]); + } else { + write_openssl_row(fp, rows[i]); + } + } + if (fclose(fp)) { + die("cannot close %s", path); + } +} + +static void write_p11(const char *path, Row **rows, int n) { + if (!path) { + return; + } + FILE *fp = fopen(path, "wb"); + if (!fp) { + die("cannot write %s: %s", path, strerror(errno)); + } + for (int i = 0; i < n; i++) { + write_p11_row(fp, rows[i]); + } + if (fclose(fp)) { + die("cannot close %s", path); + } +} + +static char *join_path(const char *dir, const char *name) { + size_t n = strlen(dir) + 1 + strlen(name) + 1; + char *s = xmalloc(n); + snprintf(s, n, "%s/%s", dir, name); + return s; +} + +static void write_dir(const char *dir, Row **rows, int n, int hashed) { + if (!dir) { + return; + } + typedef struct { + char hash[9]; + char *filename; + } Link; + Link *links = NULL; + int nlinks = 0; + int cap = 0; + for (int i = 0; i < n; i++) { + size_t fn = strlen(rows[i]->key) + 5; + char *name = xmalloc(fn); + snprintf(name, fn, "%s.crt", rows[i]->key); + char *path = join_path(dir, name); + FILE *fp = fopen(path, "wb"); + if (!fp) { + die("cannot write %s: %s", path, strerror(errno)); + } + write_openssl_row(fp, rows[i]); + if (fclose(fp)) { + die("cannot close %s", path); + } + free(path); + if (hashed && rows[i]->has_cert) { + if (nlinks == cap) { + cap = cap ? cap * 2 : 16; + links = realloc(links, (size_t)cap * sizeof(Link)); + if (!links) { + die("out of memory"); + } + } + subject_hash(rows[i]->subject, rows[i]->nsubject, links[nlinks].hash); + links[nlinks].filename = name; + nlinks++; + } else { + free(name); + } + } + if (!hashed) { + free(links); + return; + } + for (int i = 0; i < nlinks; i++) { + for (int j = i + 1; j < nlinks; j++) { + int c = strcmp(links[i].hash, links[j].hash); + if (c == 0) { + c = strcmp(links[i].filename, links[j].filename); + } + if (c > 0) { + Link tmp = links[i]; + links[i] = links[j]; + links[j] = tmp; + } + } + } + int i = 0; + while (i < nlinks) { + int j = i + 1; + while (j < nlinks && strcmp(links[i].hash, links[j].hash) == 0) { + j++; + } + if (j - i > 256) { + die("too many certificates share subject hash %s", links[i].hash); + } + for (int k = i; k < j; k++) { + char linkname[32]; + snprintf(linkname, sizeof linkname, "%s.%d", links[i].hash, k - i); + char *path = join_path(dir, linkname); + if (symlink(links[k].filename, path)) { + die("cannot symlink %s: %s", path, strerror(errno)); + } + free(path); + } + i = j; + } + for (int k = 0; k < nlinks; k++) { + free(links[k].filename); + } + free(links); +} + +static Av *find_av(Obj *o, const char *key) { + for (size_t i = 0; i < o->n; i++) { + if (strcmp(o->a[i].key, key) == 0) { + return &o->a[i]; + } + } + return NULL; +} + +static Av *need_av(Obj *o, const char *key) { + Av *a = find_av(o, key); + if (!a) { + die("certdata object missing %s", key); + } + return a; +} + +static Row *row_new(void) { + Row *r = xmalloc(sizeof *r); + memset(r, 0, sizeof *r); + trust_clear(r->trust, T_UNKNOWN); + return r; +} + +static Row *find_row(Row **rows, int n, const char *key) { + for (int i = 0; i < n; i++) { + if (strcmp(rows[i]->key, key) == 0) { + return rows[i]; + } + } + return NULL; +} + +static void rows_add(Row ***rows, int *n, int *cap, Row *row) { + Row *old = find_row(*rows, *n, row->key); + if (old) { + if (row->has_cert) { + free(old->subject); + free(old->cert); + free(old->server_after); + free(old->email_after); + old->subject = row->subject; + old->nsubject = row->nsubject; + old->cert = row->cert; + old->ncert = row->ncert; + old->mozilla = row->mozilla; + old->server_after = row->server_after; + old->nserver_after = row->nserver_after; + old->email_after = row->email_after; + old->nemail_after = row->nemail_after; + old->has_cert = 1; + row->subject = NULL; + row->cert = NULL; + row->server_after = NULL; + row->email_after = NULL; + } + if (row->has_trust) { + memcpy(old->trust, row->trust, sizeof old->trust); + old->has_trust = 1; + } + if (!old->label) { + old->label = row->label; + row->label = NULL; + } + if (!old->serial) { + old->serial = row->serial; + old->nserial = row->nserial; + old->issuer = row->issuer; + old->nissuer = row->nissuer; + row->serial = NULL; + row->issuer = NULL; + } + free(row->key); + free(row->label); + free(row->serial); + free(row->issuer); + free(row->subject); + free(row->cert); + free(row->server_after); + free(row->email_after); + free(row); + return; + } + if (*n == *cap) { + *cap = *cap ? *cap * 2 : 32; + *rows = realloc(*rows, (size_t)*cap * sizeof(Row *)); + if (!*rows) { + die("out of memory"); + } + } + (*rows)[(*n)++] = row; +} + +static int trust_from_token(const char *v) { + if (strncmp(v, "CKT_NSS_", 8) != 0) { + die("unknown trust value %s", v); + } + const char *s = v + 8; + if (strcmp(s, "TRUSTED_DELEGATOR") == 0) { + return T_TRUSTED_DELEGATOR; + } + if (strcmp(s, "MUST_VERIFY_TRUST") == 0) { + return T_MUST_VERIFY; + } + if (strcmp(s, "NOT_TRUSTED") == 0) { + return T_NOT_TRUSTED; + } + if (strcmp(s, "TRUSTED") == 0) { + return T_TRUSTED; + } + if (strcmp(s, "UNKNOWN") == 0) { + return T_UNKNOWN; + } + die("unknown trust value %s", v); + return T_UNKNOWN; +} + +static void add_obj(Obj *o, Row ***rows, int *n, int *cap) { + Av *cls = need_av(o, "CKA_CLASS"); + if (cls->kind != AV_CLASS) { + die("CKA_CLASS has the wrong type"); + } + if (cls->num == C_BUILTIN) { + return; + } + Av *label = need_av(o, "CKA_LABEL"); + Av *serial = need_av(o, "CKA_SERIAL_NUMBER"); + Av *issuer = need_av(o, "CKA_ISSUER"); + if (label->kind != AV_STR || serial->kind != AV_BYTES || issuer->kind != AV_BYTES) { + die("certdata label, serial, or issuer has the wrong type"); + } + Row *row = row_new(); + row->label = xstrdup(label->str); + row->serial = memdup(serial->bytes, serial->nbytes); + row->nserial = serial->nbytes; + row->issuer = memdup(issuer->bytes, issuer->nbytes); + row->nissuer = issuer->nbytes; + row->key = make_key(row->label, row->serial, row->nserial); + if (cls->num == C_CERT) { + Av *subject = need_av(o, "CKA_SUBJECT"); + Av *value = need_av(o, "CKA_VALUE"); + if (subject->kind != AV_BYTES || value->kind != AV_BYTES) { + die("certdata subject or value has the wrong type"); + } + row->subject = memdup(subject->bytes, subject->nbytes); + row->nsubject = subject->nbytes; + row->cert = memdup(value->bytes, value->nbytes); + row->ncert = value->nbytes; + Av *pol = find_av(o, "CKA_NSS_MOZILLA_CA_POLICY"); + if (pol && pol->kind == AV_BOOL) { + row->mozilla = pol->num; + } + Av *srv = find_av(o, "CKA_NSS_SERVER_DISTRUST_AFTER"); + if (srv && srv->kind == AV_BYTES && srv->nbytes) { + row->server_after = memdup(srv->bytes, srv->nbytes); + row->nserver_after = srv->nbytes; + } + Av *eml = find_av(o, "CKA_NSS_EMAIL_DISTRUST_AFTER"); + if (eml && eml->kind == AV_BYTES && eml->nbytes) { + row->email_after = memdup(eml->bytes, eml->nbytes); + row->nemail_after = eml->nbytes; + } + row->has_cert = 1; + rows_add(rows, n, cap, row); + return; + } + if (cls->num != C_TRUST) { + die("unknown certdata object class"); + } + trust_clear(row->trust, T_UNKNOWN); + struct { + const char *key; + int idx; + int required; + } fields[] = { + {"CKA_TRUST_SERVER_AUTH", TR_SERVER, 1}, + {"CKA_TRUST_CODE_SIGNING", TR_CODE, 1}, + {"CKA_TRUST_EMAIL_PROTECTION", TR_EMAIL, 1}, + {"CKA_TRUST_CLIENT_AUTH", TR_CLIENT, 0}, + {"CKA_TRUST_DIGITAL_SIGNATURE", TR_DIGITAL, 0}, + {"CKA_TRUST_NON_REPUDIATION", TR_NONREP, 0}, + {"CKA_TRUST_KEY_ENCIPHERMENT", TR_KEYENC, 0}, + {"CKA_TRUST_DATA_ENCIPHERMENT", TR_DATAENC, 0}, + {"CKA_TRUST_KEY_AGREEMENT", TR_KEYAGREE, 0}, + {"CKA_TRUST_KEY_CERT_SIGN", TR_KEYCERT, 0}, + {"CKA_TRUST_CRL_SIGN", TR_CRLSIGN, 0}, + {"CKA_TRUST_IPSEC_END_SYSTEM", TR_IPSEC_END, 0}, + {"CKA_TRUST_IPSEC_TUNNEL", TR_IPSEC_TUN, 0}, + {"CKA_TRUST_IPSEC_USER", TR_IPSEC_USER, 0}, + {"CKA_TRUST_TIME_STAMPING", TR_TIME, 0}, + }; + for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) { + Av *a = find_av(o, fields[i].key); + if (!a) { + if (fields[i].required) { + die("trust object missing %s", fields[i].key); + } + continue; + } + if (a->kind != AV_TRUST) { + die("%s has the wrong type", fields[i].key); + } + row->trust[fields[i].idx] = a->num; + } + if (!find_av(o, "CKA_TRUST_STEP_UP_APPROVED")) { + die("trust object missing CKA_TRUST_STEP_UP_APPROVED"); + } + row->has_trust = 1; + rows_add(rows, n, cap, row); +} + +static void decode_escapes(const char *s, unsigned char **out, size_t *outn) { + Buf b = {0}; + const unsigned char *p = (const unsigned char *)s; + while (*p) { + if (*p != '\\') { + buf_byte(&b, *p++); + continue; + } + p++; + if (*p == 0) { + die("truncated escape in certdata"); + } + if (*p >= '0' && *p <= '7') { + int v = 0; + int k = 0; + while (k < 3 && *p >= '0' && *p <= '7') { + v = (v << 3) | (*p - '0'); + p++; + k++; + } + if (v > 255) { + die("certdata escape does not fit in one byte"); + } + buf_byte(&b, (unsigned char)v); + continue; + } + if (*p == 'n') { + buf_byte(&b, '\n'); + p++; + continue; + } + if (*p == 'r') { + buf_byte(&b, '\r'); + p++; + continue; + } + if (*p == 't') { + buf_byte(&b, '\t'); + p++; + continue; + } + if (*p == '\\' || *p == '\'' || *p == '"') { + buf_byte(&b, *p++); + continue; + } + if (*p == 'x' && isxdigit((unsigned char)p[1]) && isxdigit((unsigned char)p[2])) { + char tmp[3] = {(char)p[1], (char)p[2], 0}; + buf_byte(&b, (unsigned char)strtol(tmp, NULL, 16)); + p += 3; + continue; + } + die("unknown escape in certdata"); + } + if (!b.p) { + b.p = xmalloc(1); + b.p[0] = 0; + b.n = 0; + } + *out = b.p; + *outn = b.n; +} + +static void obj_add_av(Obj *o, const char *key, const char *type, const char *value) { + if (o->n == o->cap) { + o->cap = o->cap ? o->cap * 2 : 8; + o->a = realloc(o->a, o->cap * sizeof(Av)); + if (!o->a) { + die("out of memory"); + } + } + Av *a = &o->a[o->n++]; + memset(a, 0, sizeof *a); + a->key = xstrdup(key); + if (strcmp(type, "CK_BBOOL") == 0) { + a->kind = AV_BOOL; + if (strcmp(value, "CK_TRUE") == 0) { + a->num = 1; + } else if (strcmp(value, "CK_FALSE") == 0) { + a->num = 0; + } else { + die("unknown boolean %s", value); + } + } else if (strcmp(type, "CK_OBJECT_CLASS") == 0) { + a->kind = AV_CLASS; + if (strcmp(value, "CKO_CERTIFICATE") == 0) { + a->num = C_CERT; + } else if (strcmp(value, "CKO_NSS_TRUST") == 0) { + a->num = C_TRUST; + } else if (strcmp(value, "CKO_NSS_BUILTIN_ROOT_LIST") == 0) { + a->num = C_BUILTIN; + } else { + die("unknown object class %s", value); + } + } else if (strcmp(type, "UTF8") == 0) { + a->kind = AV_STR; + a->str = xstrdup(value); + } else if (strcmp(type, "CK_CERTIFICATE_TYPE") == 0) { + a->kind = AV_STR; + if (strcmp(value, "CKC_X_509") != 0) { + die("unknown certificate type %s", value); + } + a->str = xstrdup(value); + } else if (strcmp(type, "CK_TRUST") == 0) { + a->kind = AV_TRUST; + a->num = trust_from_token(value); + } else if (strcmp(type, "MULTILINE_OCTAL") == 0) { + a->kind = AV_BYTES; + decode_escapes(value, &a->bytes, &a->nbytes); + } else { + die("unknown certdata type %s", type); + } +} + +static char *strip_line(char *s) { + while (*s == ' ' || *s == '\t' || *s == '\r') { + s++; + } + size_t n = strlen(s); + while (n && (s[n - 1] == ' ' || s[n - 1] == '\t' || s[n - 1] == '\r')) { + s[--n] = 0; + } + return s; +} + +static void parse_certdata(const char *text, Row ***rows, int *n, int *cap) { + enum { ST_WAIT, ST_ATTR, ST_TYPE, ST_VALUE, ST_MULTI }; + int state = ST_WAIT; + Obj *cur = NULL; + char *cur_attr = NULL; + char *cur_type = NULL; + Buf cur_val = {0}; + Obj **objs = NULL; + int nobj = 0, capobj = 0; + + void flush_attr(void) { + if (!cur_attr) { + return; + } + if (!cur || !cur_type) { + die("certdata attribute outside an object"); + } + obj_add_av(cur, cur_attr, cur_type, (char *)(cur_val.p ? cur_val.p : (unsigned char *)"")); + free(cur_attr); + free(cur_type); + cur_attr = NULL; + cur_type = NULL; + free(cur_val.p); + cur_val.p = NULL; + cur_val.n = 0; + cur_val.cap = 0; + } + + void push_obj(void) { + if (!cur || cur->n == 0) { + if (cur) { + free(cur->a); + free(cur); + } + cur = NULL; + return; + } + if (nobj == capobj) { + capobj = capobj ? capobj * 2 : 16; + objs = realloc(objs, (size_t)capobj * sizeof(Obj *)); + if (!objs) { + die("out of memory"); + } + } + objs[nobj++] = cur; + cur = NULL; + } + + void start_object(void) { + flush_attr(); + push_obj(); + cur = xmalloc(sizeof *cur); + memset(cur, 0, sizeof *cur); + } + + for (char *p = (char *)text; *p;) { + char *nl = strchr(p, '\n'); + if (nl) { + *nl = 0; + } + char *line = strip_line(p); + if (line[0] != '#') { + char *q = line; + while (*q) { + while (*q == ' ' || *q == '\t') { + q++; + } + if (!*q) { + break; + } + char *tok = NULL; + if (*q == '"') { + q++; + Buf b = {0}; + while (*q && *q != '"') { + if (*q == '\\' && q[1] == '"') { + buf_byte(&b, '"'); + q += 2; + continue; + } + buf_byte(&b, (unsigned char)*q++); + } + if (*q == '"') { + q++; + } + tok = b.p ? (char *)b.p : xstrdup(""); + if (!b.p) { + /* xstrdup result is tok. */ + } + } else { + char *start = q; + while (*q && *q != ' ' && *q != '\t') { + q++; + } + char saved = *q; + *q = 0; + tok = xstrdup(start); + *q = saved; + } + if (state == ST_WAIT) { + if (strcmp(tok, "BEGINDATA") == 0) { + start_object(); + state = ST_ATTR; + } + } else if (state == ST_ATTR) { + if (strcmp(tok, "CKA_CLASS") == 0) { + start_object(); + } + cur_attr = xstrdup(tok); + state = ST_TYPE; + } else if (state == ST_TYPE) { + cur_type = xstrdup(tok); + state = strcmp(tok, "MULTILINE_OCTAL") == 0 ? ST_MULTI : ST_VALUE; + } else if (state == ST_VALUE) { + buf_add(&cur_val, tok, strlen(tok)); + flush_attr(); + state = ST_ATTR; + } else if (state == ST_MULTI) { + if (strcmp(tok, "END") == 0) { + flush_attr(); + state = ST_ATTR; + } else { + buf_add(&cur_val, tok, strlen(tok)); + } + } + free(tok); + } + } + if (!nl) { + break; + } + p = nl + 1; + } + flush_attr(); + push_obj(); + for (int i = 0; i < nobj; i++) { + add_obj(objs[i], rows, n, cap); + for (size_t k = 0; k < objs[i]->n; k++) { + free(objs[i]->a[k].key); + free(objs[i]->a[k].str); + free(objs[i]->a[k].bytes); + } + free(objs[i]->a); + free(objs[i]); + } + free(objs); +} + +static char *slurp(const char *path, size_t *outn) { + FILE *fp = fopen(path, "rb"); + if (!fp) { + die("cannot read %s: %s", path, strerror(errno)); + } + if (fseek(fp, 0, SEEK_END)) { + die("cannot seek %s", path); + } + long sz = ftell(fp); + if (sz < 0) { + die("cannot size %s", path); + } + if (fseek(fp, 0, SEEK_SET)) { + die("cannot rewind %s", path); + } + char *buf = xmalloc((size_t)sz + 1); + size_t got = fread(buf, 1, (size_t)sz, fp); + fclose(fp); + buf[got] = 0; + if (outn) { + *outn = got; + } + return buf; +} + +static unsigned char *b64_decode(const char *s, size_t n, size_t *outn) { + Buf b = {0}; + int val = 0; + int bits = 0; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '=' || c == '\n' || c == '\r' || c == ' ' || c == '\t') { + continue; + } + int d = b64_val(c); + if (d < 0) { + free(b.p); + return NULL; + } + val = (val << 6) | d; + bits += 6; + if (bits >= 8) { + bits -= 8; + buf_byte(&b, (unsigned char)((val >> bits) & 0xff)); + } + } + *outn = b.n; + if (!b.p) { + b.p = xmalloc(1); + b.p[0] = 0; + b.n = 0; + *outn = 0; + } + return b.p; +} + +static int parse_oid_list(const unsigned char *p, size_t n, unsigned comp[][16], int *ncomp, + int maxn, int *count) { + size_t off = 0; + *count = 0; + while (off < n) { + if (*count >= maxn) { + return -1; + } + size_t next = 0; + if (parse_oid(p, n, off, comp[*count], &ncomp[*count], &next)) { + return -1; + } + (*count)++; + off = next; + } + return 0; +} + +static void trust_from_aux(const unsigned char *aux, size_t n, int trust[TR_COUNT]) { + trust_clear(trust, T_UNKNOWN); + unsigned tag = 0; + size_t c0 = 0, clen = 0, next = 0; + if (der_tlv(aux, n, 0, &tag, &c0, &clen, &next) || tag != 0x30) { + die("trusted certificate auxiliary is not a sequence"); + } + size_t off = c0; + size_t end = c0 + clen; + if (off >= end) { + return; + } + size_t t0 = 0, tlen = 0, tnext = 0; + if (der_tlv(aux, n, off, &tag, &t0, &tlen, &tnext) || tag != 0x30) { + die("trusted certificate is missing the trust list"); + } + unsigned comps[16][16]; + int ncomps[16]; + int count = 0; + if (parse_oid_list(aux + t0, tlen, comps, ncomps, 16, &count)) { + die("could not parse trust oids"); + } + for (int i = 0; i < count; i++) { + const Purpose *p = purpose_by_oid(comps[i], ncomps[i]); + if (p) { + trust[p->trust_index] = T_TRUSTED_DELEGATOR; + } + } + off = tnext; + if (off < end && aux[off] == 0xA0) { + size_t r0 = 0, rlen = 0, rnext = 0; + if (der_tlv(aux, n, off, &tag, &r0, &rlen, &rnext) || tag != 0xA0) { + die("could not parse reject oids"); + } + count = 0; + if (parse_oid_list(aux + r0, rlen, comps, ncomps, 16, &count)) { + die("could not parse reject oids"); + } + for (int i = 0; i < count; i++) { + const Purpose *p = purpose_by_oid(comps[i], ncomps[i]); + if (p) { + trust[p->trust_index] = T_NOT_TRUSTED; + } + } + } +} + +static void add_pem_der(const unsigned char *der, size_t nder, const int trust_in[TR_COUNT], + Row ***rows, int *n, int *cap) { + CertParts part; + if (cert_parts(der, nder, &part)) { + die("could not parse an extra certificate"); + } + Dn dn; + if (parse_dn(der + part.subject_off, part.subject_len, &dn)) { + die("could not parse an extra certificate subject"); + } + unsigned char dig[32]; + sha256(der, nder, dig); + char hex[65]; + hex_of(dig, 32, hex); + char *label = NULL; + if (dn.n == 0) { + label = xstrdup(hex); + } else { + char *last = dn_last_part(&dn); + size_t ln = strlen(last) + 1 + 8 + 1; + label = xmalloc(ln); + snprintf(label, ln, "%s:%.8s", last, hex); + free(last); + } + free_dn(&dn); + Row *row = row_new(); + row->label = label; + row->serial = memdup(der + part.serial_off, part.serial_len); + row->nserial = part.serial_len; + row->issuer = memdup(der + part.issuer_off, part.issuer_len); + row->nissuer = part.issuer_len; + row->subject = memdup(der + part.subject_off, part.subject_len); + row->nsubject = part.subject_len; + row->cert = memdup(der, nder); + row->ncert = nder; + row->key = make_key(row->label, row->serial, row->nserial); + memcpy(row->trust, trust_in, sizeof row->trust); + row->has_cert = 1; + row->has_trust = 1; + rows_add(rows, n, cap, row); +} + +static void add_pem_text(const char *text, Row ***rows, int *n, int *cap) { + const char *p = text; + while ((p = strstr(p, "-----BEGIN ")) != NULL) { + const char *name = p + 11; + const char *endname = strstr(name, "-----"); + if (!endname) { + die("pem block is missing its end marker"); + } + size_t namelen = (size_t)(endname - name); + char namebuf[64]; + if (namelen == 0 || namelen >= sizeof namebuf) { + die("pem label is too long"); + } + memcpy(namebuf, name, namelen); + namebuf[namelen] = 0; + const char *body = endname + 5; + if (*body == '\r') { + body++; + } + if (*body == '\n') { + body++; + } + char endmark[80]; + snprintf(endmark, sizeof endmark, "-----END %s-----", namebuf); + const char *end = strstr(body, endmark); + if (!end) { + die("pem block %s is not closed", namebuf); + } + size_t der_n = 0; + unsigned char *der = b64_decode(body, (size_t)(end - body), &der_n); + if (!der) { + die("pem block %s is not base64", namebuf); + } + if (strcmp(namebuf, "CERTIFICATE") == 0) { + int trust[TR_COUNT]; + trust_clear(trust, T_UNKNOWN); + trust[TR_SERVER] = T_TRUSTED_DELEGATOR; + trust[TR_CLIENT] = T_TRUSTED_DELEGATOR; + trust[TR_CODE] = T_TRUSTED_DELEGATOR; + add_pem_der(der, der_n, trust, rows, n, cap); + } else if (strcmp(namebuf, "TRUSTED CERTIFICATE") == 0) { + CertParts part; + if (cert_parts(der, der_n, &part)) { + die("could not parse a trusted certificate"); + } + /* The auxiliary follows the certificate sequence. */ + unsigned tag = 0; + size_t c0 = 0, clen = 0, next = 0; + if (der_tlv(der, der_n, 0, &tag, &c0, &clen, &next) || tag != 0x30) { + die("trusted certificate is not a sequence"); + } + int trust[TR_COUNT]; + trust_from_aux(der + next, der_n - next, trust); + add_pem_der(der, next, trust, rows, n, cap); + } + free(der); + p = end + strlen(endmark); + } +} + +static void add_bundle_path(const char *path, Row ***rows, int *n, int *cap) { + struct stat st; + if (stat(path, &st)) { + die("bundle not found: %s", path); + } + if (S_ISDIR(st.st_mode)) { + DIR *dir = opendir(path); + if (!dir) { + die("cannot open %s", path); + } + char **names = NULL; + int nn = 0, c = 0; + struct dirent *de; + while ((de = readdir(dir)) != NULL) { + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) { + continue; + } + if (nn == c) { + c = c ? c * 2 : 8; + names = realloc(names, (size_t)c * sizeof(char *)); + if (!names) { + die("out of memory"); + } + } + names[nn++] = xstrdup(de->d_name); + } + closedir(dir); + for (int i = 0; i < nn; i++) { + for (int j = i + 1; j < nn; j++) { + if (strcmp(names[i], names[j]) > 0) { + char *tmp = names[i]; + names[i] = names[j]; + names[j] = tmp; + } + } + } + for (int i = 0; i < nn; i++) { + char *full = join_path(path, names[i]); + struct stat one; + if (!stat(full, &one) && S_ISREG(one.st_mode)) { + char *text = slurp(full, NULL); + add_pem_text(text, rows, n, cap); + free(text); + } + free(full); + free(names[i]); + } + free(names); + return; + } + char *text = slurp(path, NULL); + add_pem_text(text, rows, n, cap); + free(text); +} + +static int is_opt(const char *s) { + return s[0] == '-' && s[1] == '-'; +} + +static void push_str(char ***list, int *n, const char *s) { + *list = realloc(*list, (size_t)(*n + 1) * sizeof(char *)); + if (!*list) { + die("out of memory"); + } + (*list)[(*n)++] = xstrdup(s); +} + +int main(int argc, char **argv) { + init_purposes(); + init_shorts(); + char **certdata = NULL; + int ncert = 0; + char **bundles = NULL; + int nbundles = 0; + const char *blocklist = NULL; + const char *p11 = NULL; + const char *bundle_out = NULL; + const char *standard_out = NULL; + const char *unpacked = NULL; + const char *hashed = NULL; + + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (!is_opt(a)) { + die("unexpected argument %s", a); + } + if (strcmp(a, "--certdata_input") == 0 || strcmp(a, "--ca_bundle_input") == 0) { + int bundle = strcmp(a, "--ca_bundle_input") == 0; + while (i + 1 < argc && !is_opt(argv[i + 1])) { + i++; + if (bundle) { + push_str(&bundles, &nbundles, argv[i]); + } else { + push_str(&certdata, &ncert, argv[i]); + } + } + continue; + } + if (i + 1 >= argc || is_opt(argv[i + 1])) { + die("%s needs a path", a); + } + const char *val = argv[++i]; + if (strcmp(a, "--blocklist") == 0 || strcmp(a, "--blocklist_input") == 0) { + blocklist = val; + } else if (strcmp(a, "--p11kit_output") == 0) { + p11 = val; + } else if (strcmp(a, "--ca_bundle_output") == 0) { + bundle_out = val; + } else if (strcmp(a, "--ca_standard_bundle_output") == 0) { + standard_out = val; + } else if (strcmp(a, "--ca_unpacked_output") == 0) { + unpacked = val; + } else if (strcmp(a, "--ca_hashed_unpacked_output") == 0) { + hashed = val; + } else { + die("unknown option %s", a); + } + } + if (ncert == 0 && nbundles == 0) { + fputs("buildcatrust: pass --certdata_input or --ca_bundle_input\n", stderr); + return 1; + } + if (!p11 && !bundle_out && !standard_out && !unpacked && !hashed) { + fputs("buildcatrust: pass an output path\n", stderr); + return 1; + } + + Row **rows = NULL; + int nrows = 0, cap = 0; + for (int i = 0; i < ncert; i++) { + char *text = slurp(certdata[i], NULL); + parse_certdata(text, &rows, &nrows, &cap); + free(text); + } + for (int i = 0; i < nbundles; i++) { + add_bundle_path(bundles[i], &rows, &nrows, &cap); + } + + int missing = 0; + for (int i = 0; i < nrows; i++) { + if (rows[i]->has_cert && !rows[i]->has_trust) { + if (!missing) { + fputs("Certs without trusts:", stderr); + } + fprintf(stderr, " %s", rows[i]->key); + missing = 1; + } + } + if (missing) { + fputc('\n', stderr); + return 2; + } + + char **blocked = NULL; + int nblocked = 0; + if (blocklist) { + char *text = slurp(blocklist, NULL); + for (char *p = text; *p;) { + char *nl = strchr(p, '\n'); + if (nl) { + *nl = 0; + } + char *line = strip_line(p); + if (line[0] && line[0] != '#') { + int seen = 0; + for (int i = 0; i < nblocked; i++) { + if (strcmp(blocked[i], line) == 0) { + seen = 1; + } + } + if (!seen) { + push_str(&blocked, &nblocked, line); + } + } + if (!nl) { + break; + } + p = nl + 1; + } + free(text); + } + char **saw = NULL; + int nsaw = 0; + for (int i = 0; i < nrows; i++) { + if (!rows[i]->has_trust) { + continue; + } + for (int b = 0; b < nblocked; b++) { + if (strcmp(blocked[b], rows[i]->key) == 0 || strcmp(blocked[b], rows[i]->label) == 0) { + trust_clear(rows[i]->trust, T_NOT_TRUSTED); + int already = 0; + for (int s = 0; s < nsaw; s++) { + if (strcmp(saw[s], blocked[b]) == 0) { + already = 1; + } + } + if (!already) { + push_str(&saw, &nsaw, blocked[b]); + } + } + } + } + int unseen = 0; + for (int b = 0; b < nblocked; b++) { + int ok = 0; + for (int s = 0; s < nsaw; s++) { + if (strcmp(saw[s], blocked[b]) == 0) { + ok = 1; + } + } + if (!ok) { + if (!unseen) { + fputs("Certs in blocklist but not in cert store:", stderr); + } + fprintf(stderr, " %s", blocked[b]); + unseen = 1; + } + } + if (unseen) { + fputc('\n', stderr); + return 3; + } + + Row **out = xmalloc((size_t)nrows * sizeof(Row *)); + int nout = 0; + for (int i = 0; i < nrows; i++) { + if (rows[i]->has_trust) { + out[nout++] = rows[i]; + } + } + qsort(out, (size_t)nout, sizeof(Row *), cmp_row); + + write_bundle(bundle_out, out, nout, 0); + write_bundle(standard_out, out, nout, 1); + write_dir(unpacked, out, nout, 0); + write_dir(hashed, out, nout, 1); + write_p11(p11, out, nout); + return 0; +} diff --git a/nix/packages/buildcatrust.nix b/nix/packages/buildcatrust.nix new file mode 100644 index 0000000..a3d8f2f --- /dev/null +++ b/nix/packages/buildcatrust.nix @@ -0,0 +1,29 @@ +{ + stdenv, +}: + +# C binary nss-cacert runs in buildPhase. Source is the file already +# in this repo. Not the PyPI package. No Python, no network fetch. +stdenv.mkDerivation { + pname = "buildcatrust"; + version = "0"; + + # One C file. Do not unpack it as a tarball. + dontUnpack = true; + src = ./buildcatrust.c; + + buildPhase = '' + runHook preBuild + "$CC" -O2 -o buildcatrust "$src" + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p "$out/bin" + install -m 755 buildcatrust "$out/bin/buildcatrust" + runHook postInstall + ''; + + meta.mainProgram = "buildcatrust"; +} diff --git a/nix/packages/cmake-interpreter-found.cmake b/nix/packages/cmake-interpreter-found.cmake new file mode 100644 index 0000000..d17a942 --- /dev/null +++ b/nix/packages/cmake-interpreter-found.cmake @@ -0,0 +1,7 @@ +set(Python3_FOUND TRUE) +set(Python3_Interpreter_FOUND TRUE) +set(Python3_VERSION "3.12.0") +set(Python3_VERSION_MAJOR 3) +set(Python3_VERSION_MINOR 12) +set(Python3_VERSION_PATCH 0) +set(Python3_EXECUTABLE "@EXECUTABLE@") diff --git a/nix/packages/cmake-interpreter-stub.c b/nix/packages/cmake-interpreter-stub.c new file mode 100644 index 0000000..707975b --- /dev/null +++ b/nix/packages/cmake-interpreter-stub.c @@ -0,0 +1,41 @@ +/* Answers CMake's FindPython interpreter probe. + The binary stays cmake-interpreter-stub. CMake compares + the major version from -c against 3, so a "Python 3.12.0" + banner is the wrong answer and the probe fails closed. +*/ +#include +#include + +static int has(const char *s, const char *n) +{ + return s != NULL && n != NULL && strstr(s, n) != NULL; +} + +int main(int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-V") == 0) { + fputs("Python 3.12.0\n", stdout); + return 0; + } + if (strcmp(argv[i], "-c") == 0 && i + 1 < argc) { + const char *code = argv[i + 1]; + if (has(code, "version_info[:3]")) { + fputs("3.12.0\n", stdout); + return 0; + } + if (has(code, "version_info[0]")) { + fputs("3\n", stdout); + return 0; + } + if (has(code, "get_path") || has(code, "sysconfig")) { + fputs("/build/pylib;/build/pylib;/build/pylib;/build/pylib\n", stdout); + return 0; + } + return 0; + } + } + return 0; +} diff --git a/nix/packages/dns-type-gperf.c b/nix/packages/dns-type-gperf.c new file mode 100644 index 0000000..b788f75 --- /dev/null +++ b/nix/packages/dns-type-gperf.c @@ -0,0 +1,182 @@ +/* Three arguments are name, prefix, and the input path. + Any other argc exits 1 and writes a short line on stderr. + Each input line is printed with '_' changed to '-', then ", ", + then prefix concatenated with the original line. +*/ +#include +#include + +static int trailing_ws(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || + c == '\f' || (c >= 0x1c && c <= 0x1f); +} + +static int write_header(const char *name) +{ + int n; + + n = printf( + "%%{\n" + "_Pragma(\"GCC diagnostic ignored \\\"-Wimplicit-fallthrough\\\"\")\n" + "#if __GNUC__ >= 15\n" + "_Pragma(\"GCC diagnostic ignored \\\"-Wzero-as-null-pointer-constant\\\"\")\n" + "#endif\n" + "%%}\n" + "struct %s_name { const char* name; int id; };\n" + "%%null-strings\n" + "%%%%\n", + name); + return n < 0 ? 1 : 0; +} + +static int write_line(const char *line, size_t len, const char *prefix) +{ + size_t i; + unsigned char ch; + + for (i = 0; i < len; i++) { + ch = (unsigned char)line[i]; + if (ch == '_') { + ch = '-'; + } + if (putchar(ch) == EOF) { + return 1; + } + } + if (fputs(", ", stdout) == EOF) { + return 1; + } + if (fputs(prefix, stdout) == EOF) { + return 1; + } + if (len > 0 && fwrite(line, 1, len, stdout) != len) { + return 1; + } + if (putchar('\n') == EOF) { + return 1; + } + return 0; +} + +static int grow(char **buf, size_t *cap, size_t need) +{ + size_t ncap; + char *nbuf; + + if (need <= *cap) { + return 0; + } + ncap = *cap == 0 ? 128 : *cap; + while (ncap < need) { + if (ncap > ((size_t)-1) / 2) { + return 1; + } + ncap *= 2; + } + nbuf = realloc(*buf, ncap); + if (nbuf == NULL) { + return 1; + } + *buf = nbuf; + *cap = ncap; + return 0; +} + +/* Text mode ends a line on LF, CR LF, or a bare CR. */ +static int write_input(FILE *in, const char *prefix) +{ + char *buf; + size_t cap; + size_t len; + int c; + int next; + int rc; + + buf = NULL; + cap = 0; + len = 0; + rc = 0; + for (;;) { + c = fgetc(in); + if (c == EOF) { + if (ferror(in)) { + rc = 1; + break; + } + if (len == 0) { + break; + } + } else if (c == '\r' || c == '\n') { + if (c == '\r') { + next = fgetc(in); + if (next != '\n' && next != EOF) { + if (ungetc(next, in) == EOF) { + rc = 1; + break; + } + } + if (next == EOF && ferror(in)) { + rc = 1; + break; + } + } + } else { + if (len == (size_t)-1 || grow(&buf, &cap, len + 1) != 0) { + fprintf(stderr, "dns-type-gperf: out of memory\n"); + rc = 1; + break; + } + buf[len++] = (char)c; + continue; + } + + while (len > 0 && trailing_ws((unsigned char)buf[len - 1])) { + len--; + } + if (write_line(buf, len, prefix) != 0) { + rc = 1; + break; + } + len = 0; + if (c == EOF) { + break; + } + } + + free(buf); + return rc; +} + +int main(int argc, char **argv) +{ + FILE *in; + int rc; + + if (argc != 4) { + fprintf(stderr, "dns-type-gperf: expected name, prefix, and input\n"); + return 1; + } + + if (write_header(argv[1]) != 0) { + return 1; + } + + in = fopen(argv[3], "r"); + if (in == NULL) { + fprintf(stderr, "dns-type-gperf: cannot open input\n"); + return 1; + } + + rc = write_input(in, argv[2]); + if (fclose(in) != 0) { + rc = 1; + } + if (rc != 0) { + return 1; + } + if (fflush(stdout) != 0 || ferror(stdout)) { + return 1; + } + return 0; +} diff --git a/nix/packages/ethtool-link-modes.c b/nix/packages/ethtool-link-modes.c new file mode 100644 index 0000000..b81bc53 --- /dev/null +++ b/nix/packages/ethtool-link-modes.c @@ -0,0 +1,348 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char k_pat[] = + "^[[:space:]]*(ETHTOOL_LINK_MODE_(([[:digit:]]*).*)_BIT)" + "[[:space:]]*=[[:space:]]*([[:digit:]]+),"; + +static const char k_missing_speed[] = "100000000000000000000"; + +struct entry { + char *enum_tok; + char *name; + char *speed; + const char *duplex; + unsigned long value; +}; + +static void die_oom(void) { + exit(1); +} + +static char *xdup_n(const char *s, size_t n) { + char *d = malloc(n + 1); + if (!d) { + die_oom(); + } + memcpy(d, s, n); + d[n] = '\0'; + return d; +} + +static char *xdup(const char *s) { + return xdup_n(s, strlen(s)); +} + +static char *span_match(const char *line, regmatch_t m) { + if (m.rm_so < 0 || m.rm_eo < m.rm_so) { + return xdup(""); + } + return xdup_n(line + m.rm_so, (size_t)(m.rm_eo - m.rm_so)); +} + +static char *capture_cpp(char **cargv) { + FILE *nullin = fopen("/dev/null", "r"); + if (!nullin) { + return NULL; + } + int devnull = fileno(nullin); + int fds[2]; + if (pipe(fds) != 0) { + fclose(nullin); + return NULL; + } + pid_t pid = fork(); + if (pid < 0) { + close(fds[0]); + close(fds[1]); + fclose(nullin); + return NULL; + } + if (pid == 0) { + if (dup2(devnull, STDIN_FILENO) < 0) { + _exit(127); + } + if (devnull != STDIN_FILENO) { + close(devnull); + } + if (dup2(fds[1], STDOUT_FILENO) < 0) { + _exit(127); + } + close(fds[0]); + close(fds[1]); + execvp(cargv[0], cargv); + _exit(127); + } + fclose(nullin); + close(fds[1]); + + char *buf = NULL; + size_t len = 0; + size_t cap = 0; + char tmp[65536]; + for (;;) { + ssize_t n = read(fds[0], tmp, sizeof tmp); + if (n == 0) { + break; + } + if (n < 0) { + if (errno == EINTR) { + continue; + } + close(fds[0]); + free(buf); + waitpid(pid, NULL, 0); + return NULL; + } + if (len + (size_t)n + 1 > cap) { + size_t ncap = cap ? cap : (size_t)65536; + while (ncap < len + (size_t)n + 1) { + if (ncap > ((size_t)-1) / 2) { + close(fds[0]); + free(buf); + waitpid(pid, NULL, 0); + return NULL; + } + ncap *= 2; + } + char *nbuf = realloc(buf, ncap); + if (!nbuf) { + close(fds[0]); + free(buf); + waitpid(pid, NULL, 0); + return NULL; + } + buf = nbuf; + cap = ncap; + } + memcpy(buf + len, tmp, (size_t)n); + len += (size_t)n; + } + close(fds[0]); + + int st = 0; + for (;;) { + pid_t w = waitpid(pid, &st, 0); + if (w < 0) { + if (errno == EINTR) { + continue; + } + free(buf); + return NULL; + } + break; + } + if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) { + free(buf); + return NULL; + } + if (!buf) { + buf = malloc(1); + if (!buf) { + return NULL; + } + } + buf[len] = '\0'; + return buf; +} + +static int cmp_digits(const char *a, const char *b) { + size_t la = strlen(a); + size_t lb = strlen(b); + if (la < lb) { + return -1; + } + if (la > lb) { + return 1; + } + return strcmp(a, b); +} + +static int cmp_entry(const void *va, const void *vb) { + const struct entry *a = va; + const struct entry *b = vb; + const char *sa = a->speed[0] ? a->speed : k_missing_speed; + const char *sb = b->speed[0] ? b->speed : k_missing_speed; + int c = cmp_digits(sa, sb); + if (c != 0) { + return c; + } + c = strcmp(a->duplex, b->duplex); + if (c != 0) { + return c; + } + c = strcmp(a->name, b->name); + if (c != 0) { + return c; + } + if (a->value < b->value) { + return -1; + } + if (a->value > b->value) { + return 1; + } + return 0; +} + +static void add_entry(struct entry **ents, size_t *nent, size_t *cap, struct entry e) { + if (*nent == *cap) { + size_t ncap = *cap ? *cap * 2 : 64; + struct entry *nents = realloc(*ents, ncap * sizeof *nents); + if (!nents) { + die_oom(); + } + *ents = nents; + *cap = ncap; + } + (*ents)[*nent] = e; + *nent += 1; +} + +static void parse_lines(char *buf, regex_t *re, struct entry **ents, size_t *nent) { + size_t cap = 0; + char *p = buf; + while (*p) { + char *nl = strchr(p, '\n'); + if (nl) { + *nl = '\0'; + } + size_t L = strlen(p); + if (L > 0 && p[L - 1] == '\r') { + p[L - 1] = '\0'; + } + regmatch_t rm[5]; + if (regexec(re, p, 5, rm, 0) == 0) { + struct entry e; + e.enum_tok = span_match(p, rm[1]); + e.name = span_match(p, rm[2]); + for (char *q = e.name; *q; q++) { + if (*q == '_') { + *q = '-'; + } else { + *q = (char)tolower((unsigned char)*q); + } + } + if (strcmp(e.name, "autoneg") == 0) { + free(e.name); + e.name = xdup("autonegotiation"); + } + size_t nd = 0; + while (isdigit((unsigned char)e.name[nd])) { + nd++; + } + e.speed = xdup_n(e.name, nd); + const char *hy = strrchr(e.name, '-'); + const char *last = hy ? hy + 1 : e.name; + if (strcmp(last, "half") == 0 || strcmp(last, "full") == 0) { + e.duplex = last; + } else { + e.duplex = ""; + } + char *val = span_match(p, rm[4]); + e.value = strtoul(val, NULL, 10); + free(val); + add_entry(ents, nent, &cap, e); + } + if (!nl) { + break; + } + p = nl + 1; + } +} + +static void print_header(const struct entry *ents, size_t nent) { + size_t i; + for (i = 0; i < nent; i++) { + size_t elen = strlen(ents[i].enum_tok); + char *field = malloc(elen + 3); + if (!field) { + die_oom(); + } + field[0] = '['; + memcpy(field + 1, ents[i].enum_tok, elen); + field[elen + 1] = ']'; + field[elen + 2] = '\0'; + printf(" %-50s = \"%s\",\n", field, ents[i].name); + free(field); + } +} + +static void print_xml(struct entry *ents, size_t nent) { + size_t i; + qsort(ents, nent, sizeof *ents, cmp_entry); + printf(" \n"); + for (i = 0; i < nent; i++) { + printf(" \n", ents[i].name); + printf(" %s%s\n\n", ents[i].speed, ents[i].duplex); + } + printf(" \n"); +} + +int main(int argc, char **argv) { + int i; + int xml; + char **cargv; + char *text; + regex_t re; + struct entry *ents = NULL; + size_t nent = 0; + + if (argc < 3) { + return 2; + } + if (strcmp(argv[1], "--header") != 0 && strcmp(argv[1], "--xml") != 0) { + return 2; + } + if (argv[2][0] == '\0') { + return 2; + } + for (i = 3; i < argc; i++) { + if (strncmp(argv[i], "-I", 2) != 0) { + return 2; + } + } + xml = strcmp(argv[1], "--xml") == 0; + + cargv = calloc((size_t)argc + 2, sizeof *cargv); + if (!cargv) { + return 1; + } + cargv[0] = argv[2]; + cargv[1] = "-include"; + cargv[2] = "linux/ethtool.h"; + for (i = 3; i < argc; i++) { + cargv[i] = argv[i]; + } + cargv[argc] = "-"; + cargv[argc + 1] = NULL; + + text = capture_cpp(cargv); + free(cargv); + if (!text) { + return 1; + } + if (regcomp(&re, k_pat, REG_EXTENDED) != 0) { + free(text); + return 1; + } + parse_lines(text, &re, &ents, &nent); + regfree(&re); + free(text); + + if (xml) { + print_xml(ents, nent); + } else { + print_header(ents, nent); + } + if (nent < 99) { + return 1; + } + return 0; +} diff --git a/nix/packages/gperf-protocol-names.c b/nix/packages/gperf-protocol-names.c new file mode 100644 index 0000000..78fbef7 --- /dev/null +++ b/nix/packages/gperf-protocol-names.c @@ -0,0 +1,92 @@ +/* argv is name, prefix, file, then optional include strings. + stdout is the gperf text. Fewer than those three arguments exits 1. +*/ +#include + +static size_t cstr_len(const char *s) +{ + size_t n = 0; + + while (s[n] != '\0') { + n++; + } + return n; +} + +static int is_trail_ws(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || + c == '\f'; +} + +int main(int argc, char **argv) +{ + const char *name; + const char *prefix; + FILE *in; + char buf[4096]; + int i; + + if (argc < 4) { + fprintf(stderr, "Usage: %s name prefix file [includes...]\n", + argc > 0 && argv[0] != NULL ? argv[0] : + "gperf-protocol-names"); + return 1; + } + + name = argv[1]; + prefix = argv[2]; + + if (fputs("%{\n" + "_Pragma(\"GCC diagnostic ignored \\\"-Wimplicit-fallthrough\\\"\")\n" + "#if __GNUC__ >= 15\n" + "_Pragma(\"GCC diagnostic ignored \\\"-Wzero-as-null-pointer-constant\\\"\")\n" + "#endif\n", + stdout) == EOF) { + return 1; + } + + for (i = 4; i < argc; i++) { + if (printf("#include %s\n", argv[i]) < 0) { + return 1; + } + } + + if (printf("%%}\n" + "struct %s_name { const char* name; int id; };\n" + "%%null-strings\n" + "%%%%\n", + name) < 0) { + return 1; + } + + in = fopen(argv[3], "r"); + if (in == NULL) { + return 1; + } + + while (fgets(buf, (int)sizeof buf, in) != NULL) { + size_t n = cstr_len(buf); + + if (n == sizeof buf - 1 && buf[n - 1] != '\n' && !feof(in)) { + fclose(in); + return 1; + } + while (n > 0 && is_trail_ws(buf[n - 1])) { + n--; + } + buf[n] = '\0'; + if (printf("%s, %s%s\n", buf, prefix, buf) < 0) { + fclose(in); + return 1; + } + } + if (ferror(in)) { + fclose(in); + return 1; + } + if (fclose(in) != 0) { + return 1; + } + return 0; +} diff --git a/nix/packages/llvm-version-script.c b/nix/packages/llvm-version-script.c new file mode 100644 index 0000000..0e2f436 --- /dev/null +++ b/nix/packages/llvm-version-script.c @@ -0,0 +1,36 @@ +/* One argument is the output path. Any other argc exits 2 and writes nothing. + The bytes are a linker version script binutils 2.46 ld.bfd accepts. +*/ +#include + +int main(int argc, char **argv) +{ + static const char script[] = + "{\n" + " global:\n" + " *;\n" + "};\n"; + FILE *out; + size_t n; + size_t wrote; + + if (argc != 2) { + return 2; + } + + out = fopen(argv[1], "w"); + if (out == NULL) { + return 1; + } + + n = sizeof script - 1; + wrote = fwrite(script, 1, n, out); + if (wrote != n || ferror(out)) { + fclose(out); + return 1; + } + if (fclose(out) != 0) { + return 1; + } + return 0; +} diff --git a/nix/packages/meson-setup-hook.sh b/nix/packages/meson-setup-hook.sh new file mode 100644 index 0000000..7302c3c --- /dev/null +++ b/nix/packages/meson-setup-hook.sh @@ -0,0 +1,101 @@ +# Sourced by stdenv when this package is a native build input. +# The meson command on PATH is muon. + +mesonConfigurePhase() { + runHook preConfigure + + : "${mesonBuildDir:=build}" + + local flagsArray=() + + if [ -z "${dontAddPrefix-}" ]; then + flagsArray+=("--prefix=$prefix") + fi + + flagsArray+=( + "--libdir=${!outputLib}/lib" + "--libexecdir=${!outputLib}/libexec" + "--bindir=${!outputBin}/bin" + "--sbindir=${!outputBin}/sbin" + "--includedir=${!outputInclude}/include" + "--mandir=${!outputMan}/share/man" + "--infodir=${!outputInfo}/share/info" + "--localedir=${!outputLib}/share/locale" + "-Dauto_features=${mesonAutoFeatures:-enabled}" + "-Dwrap_mode=${mesonWrapMode:-nodownload}" + "--buildtype=${mesonBuildType:-plain}" + ) + + concatTo flagsArray mesonFlags mesonFlagsArray + + echoCmd 'mesonConfigurePhase flags' "${flagsArray[@]}" + + meson setup "$mesonBuildDir" "${flagsArray[@]}" + cd "$mesonBuildDir" || { + echoCmd 'mesonConfigurePhase' "could not cd to $mesonBuildDir" + exit 1 + } + + if ! [[ -v enableParallelBuilding ]]; then + enableParallelBuilding=1 + echoCmd 'mesonConfigurePhase' "enabled parallel building" + fi + + if [[ ${checkPhase-ninjaCheckPhase} = ninjaCheckPhase && -z ${dontUseMesonCheck-} ]]; then + checkPhase=mesonCheckPhase + fi + if [[ ${installPhase-ninjaInstallPhase} = ninjaInstallPhase && -z ${dontUseMesonInstall-} ]]; then + installPhase=mesonInstallPhase + fi + + runHook postConfigure +} + +mesonCheckPhase() { + runHook preCheck + + local flagsArray=() + concatTo flagsArray mesonCheckFlags mesonCheckFlagsArray + + if [ -z "${dontAddTimeoutMultiplier:-}" ]; then + flagsArray+=("--timeout-multiplier=0") + fi + + local buildCores=1 + if [ "${enableParallelBuilding-1}" ]; then + buildCores="$NIX_BUILD_CORES" + fi + + TERM=dumb ninja -j"$buildCores" $ninjaFlags "${ninjaFlagsArray[@]}" meson-test-prereq + + local testCores=1 + if [ "${enableParallelChecking-1}" ]; then + testCores="$NIX_BUILD_CORES" + fi + + echoCmd 'mesonCheckPhase flags' "${flagsArray[@]}" + meson test -j"$testCores" --no-rebuild --print-errorlogs --max-lines=1000000 "${flagsArray[@]}" + + runHook postCheck +} + +mesonInstallPhase() { + runHook preInstall + + local flagsArray=() + + if [[ -n "${mesonInstallTags-}" ]]; then + flagsArray+=("--tags" "$(concatStringsSep "," mesonInstallTags)") + fi + concatTo flagsArray mesonInstallFlags mesonInstallFlagsArray + + echoCmd 'mesonInstallPhase flags' "${flagsArray[@]}" + meson install --no-rebuild "${flagsArray[@]}" + + runHook postInstall +} + +if [ -z "${dontUseMesonConfigure-}" ] && [ -z "${configurePhase-}" ]; then + setOutputFlags= + configurePhase=mesonConfigurePhase +fi diff --git a/nix/packages/p11-kit-pregenerated.patch b/nix/packages/p11-kit-pregenerated.patch new file mode 100644 index 0000000..7afa5fa --- /dev/null +++ b/nix/packages/p11-kit-pregenerated.patch @@ -0,0 +1,112 @@ +--- a/meson.build ++++ b/meson.build +@@ -467,16 +467,6 @@ + gnome = import('gnome') + i18n = import('i18n') + pkg = import('pkgconfig') +-python = import('python').find_installation() +-python_version = python.language_version() +-python_version_req = '>=3.6' +-if not python_version.version_compare(python_version_req) +- error('Requires Python @0@, @1@ found.'.format(python_version_req, python_version)) +-endif +- +-pkcs11_json_project = subproject('pkcs11-json') +-pkcs11_json_gen = pkcs11_json_project.get_variable('pkcs11_json_gen') +-pkcs11_json = pkcs11_json_gen.process('common/pkcs11.h') + + subdir('common') + subdir('p11-kit') +--- a/p11-kit/meson.build ++++ b/p11-kit/meson.build +@@ -24,64 +24,13 @@ + 'rpc-client.c' + ] + +-libp11_kit_internal_sources += custom_target('generate virtual-ffi-generated.h', +- input: pkcs11_json, +- output: 'virtual-ffi-generated.h', +- command: [ +- python, +- meson.current_source_dir() / 'gen-wrappers.py', +- '--template', meson.current_source_dir() / 'templates' / 'binding-wrappers.py', +- '--excludes', meson.current_source_dir() / 'templates' / 'virtual-excludes.list', +- '--infile', '@INPUT@', +- '--outfile', '@OUTPUT@', +- ]) +-libp11_kit_internal_sources += custom_target('generate virtual-fixed-wrappers.h', +- input: pkcs11_json, +- output: 'virtual-fixed-wrappers.h', +- command: [ +- python, +- meson.current_source_dir() / 'gen-wrappers.py', +- '--template', meson.current_source_dir() / 'templates' / 'fixed-wrappers.py', +- '--excludes', meson.current_source_dir() / 'templates' / 'virtual-excludes.list', +- '--renames', +- 'C_GetFunctionStatus:short', +- 'C_CancelFunction:short', +- '--concat-lines', +- '--infile', '@INPUT@', +- '--outfile', '@OUTPUT@', +- ]) +- +-libp11_kit_internal_sources += custom_target('generate virtual-fixed-closures.h', +- output: 'virtual-fixed-closures.h', +- command: [ +- python, +- meson.current_source_dir() / 'gen-fixed-closures.py', +- '--closures', '@0@'.format(closures), +- '--outfile', '@OUTPUT@', +- ]) +- +-libp11_kit_internal_sources += custom_target('generate virtual-stack-generated.h', +- input: pkcs11_json, +- output: 'virtual-stack-generated.h', +- command: [ +- python, +- meson.current_source_dir() / 'gen-wrappers.py', +- '--template', meson.current_source_dir() / 'templates' / 'stack-wrappers.py', +- '--excludes', meson.current_source_dir() / 'templates' / 'virtual-excludes.list', +- '--infile', '@INPUT@', +- '--outfile', '@OUTPUT@', +- ]) +-libp11_kit_internal_sources += custom_target('generate virtual-base-generated.h', +- input: pkcs11_json, +- output: 'virtual-base-generated.h', +- command: [ +- python, +- meson.current_source_dir() / 'gen-wrappers.py', +- '--template', meson.current_source_dir() / 'templates' / 'base-wrappers.py', +- '--excludes', meson.current_source_dir() / 'templates' / 'virtual-excludes.list', +- '--infile', '@INPUT@', +- '--outfile', '@OUTPUT@', +- ]) ++libp11_kit_internal_sources += files( ++ 'virtual-ffi-generated.h', ++ 'virtual-fixed-wrappers.h', ++ 'virtual-fixed-closures.h', ++ 'virtual-stack-generated.h', ++ 'virtual-base-generated.h', ++) + libp11_kit_internal_sources += 'virtual.c' + + libp11_kit_internal_c_args = [ +@@ -112,17 +61,7 @@ + 'util.c' + ] + +-libp11_kit_sources += custom_target('generate proxy-generated.h', +- input: pkcs11_json, +- output: 'proxy-generated.h', +- command: [ +- python, +- meson.current_source_dir() / 'gen-wrappers.py', +- '--template', meson.current_source_dir() / 'templates' / 'proxy-wrappers.py', +- '--excludes', meson.current_source_dir() / 'templates' / 'proxy-excludes.list', +- '--infile', '@INPUT@', +- '--outfile', '@OUTPUT@', +- ]) ++libp11_kit_sources += files('proxy-generated.h') + + libp11_kit_symbol_map = meson.current_source_dir() / 'libp11-kit.map' + libp11_kit_ldflags = cc.get_supported_link_arguments([ diff --git a/nix/packages/psl-make-dafsa.c b/nix/packages/psl-make-dafsa.c new file mode 100644 index 0000000..1870a08 --- /dev/null +++ b/nix/packages/psl-make-dafsa.c @@ -0,0 +1,1676 @@ +/* ISO C11 port of libpsl psl-make-dafsa. cxx+ output feeds suffixes_dafsa.h. */ +#define _DEFAULT_SOURCE +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include + +static void die(const char *msg) +{ + fprintf(stderr, "psl-make-dafsa-c: %s\n", msg); + exit(1); +} + +static void die_errno(const char *msg) +{ + fprintf(stderr, "psl-make-dafsa-c: %s: %s\n", msg, strerror(errno)); + exit(1); +} + +static void die_fmt(const char *fmt, ...) +{ + va_list ap; + + fputs("psl-make-dafsa-c: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(1); +} + +static void *xmalloc(size_t n) +{ + void *p; + + if (n == 0) + n = 1; + p = malloc(n); + if (p == NULL) + die("out of memory"); + return p; +} + +static void *xrealloc(void *p, size_t n) +{ + void *q; + + if (n == 0) + n = 1; + q = realloc(p, n); + if (q == NULL) + die("out of memory"); + return q; +} + +typedef struct { + uint8_t *p; + size_t n; +} bstr; + +typedef struct { + bstr *v; + size_t n, cap; +} bvec; + +static void bvec_push(bvec *b, bstr s) +{ + if (b->n == b->cap) { + b->cap = b->cap ? b->cap * 2 : 8; + b->v = xrealloc(b->v, b->cap * sizeof *b->v); + } + b->v[b->n++] = s; +} + +static bstr bstr_cat(const uint8_t *a, size_t an, const uint8_t *b, size_t bn) +{ + bstr s; + + s.n = an + bn; + s.p = xmalloc(s.n ? s.n : 1); + if (an) + memcpy(s.p, a, an); + if (bn) + memcpy(s.p + an, b, bn); + return s; +} + +static int bstr_cmp(const bstr *a, const bstr *b) +{ + size_t n = a->n < b->n ? a->n : b->n; + int c = 0; + + if (n) + c = memcmp(a->p, b->p, n); + if (c != 0) + return c; + if (a->n < b->n) + return -1; + if (a->n > b->n) + return 1; + return 0; +} + +static int bstr_cmp_q(const void *a, const void *b) +{ + return bstr_cmp(a, b); +} + +static void bvec_unique_sorted(bvec *b) +{ + size_t w = 0; + size_t i; + + qsort(b->v, b->n, sizeof *b->v, bstr_cmp_q); + for (i = 0; i < b->n; i++) { + if (w > 0 && bstr_cmp(&b->v[w - 1], &b->v[i]) == 0) + continue; + b->v[w++] = b->v[i]; + } + b->n = w; +} + +typedef struct { + uint8_t *p; + size_t n, cap; +} uvec; + +static void uvec_push(uvec *u, uint8_t c) +{ + if (u->n == u->cap) { + u->cap = u->cap ? u->cap * 2 : 16; + u->p = xrealloc(u->p, u->cap); + } + u->p[u->n++] = c; +} + +static void uvec_extend(uvec *u, const uint8_t *p, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + uvec_push(u, p[i]); +} + +static void uvec_reverse(uvec *u) +{ + size_t i; + + for (i = 0; i < u->n / 2; i++) { + uint8_t t = u->p[i]; + + u->p[i] = u->p[u->n - 1 - i]; + u->p[u->n - 1 - i] = t; + } +} + +typedef struct node node; +struct node { + uint8_t *lab; + size_t lab_len; + node **ch; + size_t nch, cap; +}; + +typedef struct { + node **v; + size_t n, cap; +} nvec; + +static void nvec_push(nvec *a, node *n) +{ + if (a->n == a->cap) { + a->cap = a->cap ? a->cap * 2 : 8; + a->v = xrealloc(a->v, a->cap * sizeof *a->v); + } + a->v[a->n++] = n; +} + +static node *node_new(const uint8_t *lab, size_t n) +{ + node *nd = xmalloc(sizeof *nd); + + nd->lab = xmalloc(n ? n : 1); + if (n) + memcpy(nd->lab, lab, n); + nd->lab_len = n; + nd->ch = NULL; + nd->nch = 0; + nd->cap = 0; + return nd; +} + +static void node_add(node *nd, node *child) +{ + if (nd->nch == nd->cap) { + nd->cap = nd->cap ? nd->cap * 2 : 4; + nd->ch = xrealloc(nd->ch, nd->cap * sizeof *nd->ch); + } + nd->ch[nd->nch++] = child; +} + +typedef struct { + const void *key; + void *val; + unsigned state; +} pslot; + +typedef struct { + pslot *s; + size_t cap, n; +} pmap; + +static uint64_t hash_ptr(const void *p) +{ + uint64_t x = (uint64_t)(uintptr_t)p; + + x ^= x >> 30; + x *= 0xbf58476d1ce4e5b9ull; + x ^= x >> 27; + x *= 0x94d049bb133111ebull; + x ^= x >> 31; + return x ? x : 1; +} + +static void pmap_grow(pmap *m) +{ + size_t nc = m->cap ? m->cap * 2 : 16; + pslot *ns = xmalloc(nc * sizeof *ns); + size_t i; + + memset(ns, 0, nc * sizeof *ns); + for (i = 0; i < m->cap; i++) { + size_t j, mask; + + if (!m->s[i].state) + continue; + mask = nc - 1; + j = (size_t)hash_ptr(m->s[i].key) & mask; + while (ns[j].state) + j = (j + 1) & mask; + ns[j] = m->s[i]; + } + free(m->s); + m->s = ns; + m->cap = nc; +} + +static void **pmap_slot(pmap *m, const void *key, int create) +{ + size_t mask, j, steps; + + if (m->cap == 0 || (create && m->n * 10 >= m->cap * 7)) + pmap_grow(m); + mask = m->cap - 1; + j = (size_t)hash_ptr(key) & mask; + for (steps = 0; steps <= m->cap; steps++) { + if (!m->s[j].state) { + if (!create) + return NULL; + m->s[j].state = 1; + m->s[j].key = key; + m->s[j].val = NULL; + m->n++; + return &m->s[j].val; + } + if (m->s[j].key == key) + return &m->s[j].val; + j = (j + 1) & mask; + } + die("pointer map full"); + return NULL; +} + +static int pmap_get(pmap *m, const void *key, void **val) +{ + void **slot; + + if (m->cap == 0) + return 0; + slot = pmap_slot(m, key, 0); + if (slot == NULL) + return 0; + *val = *slot; + return 1; +} + +static void pmap_set(pmap *m, const void *key, void *val) +{ + *pmap_slot(m, key, 1) = val; +} + +typedef struct { + uint8_t *k; + size_t kn; + void *val; + unsigned state; +} bslot; + +typedef struct { + bslot *s; + size_t cap, n; +} bmap; + +static uint64_t hash_bytes(const uint8_t *p, size_t n) +{ + uint64_t h = 14695981039346656037ull; + size_t i; + + for (i = 0; i < n; i++) { + h ^= p[i]; + h *= 1099511628211ull; + } + return h ? h : 1; +} + +static int bytes_eq(const uint8_t *a, size_t an, const uint8_t *b, size_t bn) +{ + if (an != bn) + return 0; + return an == 0 || memcmp(a, b, an) == 0; +} + +static void bmap_grow(bmap *m) +{ + size_t nc = m->cap ? m->cap * 2 : 16; + bslot *ns = xmalloc(nc * sizeof *ns); + size_t i; + + memset(ns, 0, nc * sizeof *ns); + for (i = 0; i < m->cap; i++) { + size_t j, mask; + + if (!m->s[i].state) + continue; + mask = nc - 1; + j = (size_t)hash_bytes(m->s[i].k, m->s[i].kn) & mask; + while (ns[j].state) + j = (j + 1) & mask; + ns[j] = m->s[i]; + } + free(m->s); + m->s = ns; + m->cap = nc; +} + +static bslot *bmap_slot(bmap *m, const uint8_t *k, size_t kn, int create) +{ + size_t mask, j, steps; + + if (m->cap == 0 || (create && m->n * 10 >= m->cap * 7)) + bmap_grow(m); + mask = m->cap - 1; + j = (size_t)hash_bytes(k, kn) & mask; + for (steps = 0; steps <= m->cap; steps++) { + if (!m->s[j].state) { + uint8_t *copy; + + if (!create) + return NULL; + copy = xmalloc(kn ? kn : 1); + if (kn) + memcpy(copy, k, kn); + m->s[j].state = 1; + m->s[j].k = copy; + m->s[j].kn = kn; + m->s[j].val = NULL; + m->n++; + return &m->s[j]; + } + if (bytes_eq(m->s[j].k, m->s[j].kn, k, kn)) + return &m->s[j]; + j = (j + 1) & mask; + } + die("byte map full"); + return NULL; +} + +static void *bmap_get(bmap *m, const uint8_t *k, size_t kn) +{ + bslot *s; + + if (m->cap == 0) + return NULL; + s = bmap_slot(m, k, kn, 0); + if (s == NULL) + return NULL; + return s->val; +} + +static void bmap_put(bmap *m, const uint8_t *k, size_t kn, void *val) +{ + bmap_slot(m, k, kn, 1)->val = val; +} + +static uint32_t rol32(uint32_t v, int n) +{ + return (v << n) | (v >> (32 - n)); +} + +static void sha1_block(uint32_t h[5], const uint8_t blk[64]) +{ + uint32_t w[80]; + uint32_t a, b, c, d, e; + int i; + + for (i = 0; i < 16; i++) { + w[i] = ((uint32_t)blk[i * 4] << 24) | ((uint32_t)blk[i * 4 + 1] << 16) | + ((uint32_t)blk[i * 4 + 2] << 8) | (uint32_t)blk[i * 4 + 3]; + } + for (i = 16; i < 80; i++) + w[i] = rol32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + a = h[0]; + b = h[1]; + c = h[2]; + d = h[3]; + e = h[4]; + for (i = 0; i < 80; i++) { + uint32_t f, k, t; + + if (i < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + t = rol32(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = rol32(b, 30); + b = a; + a = t; + } + h[0] += a; + h[1] += b; + h[2] += c; + h[3] += d; + h[4] += e; +} + +static void sha1_file(const char *path, char out[41]) +{ + FILE *f = fopen(path, "rb"); + uint32_t h[5] = {0x67452301u, 0xEFCDAB89u, 0x98BADCFEu, 0x10325476u, 0xC3D2E1F0u}; + uint8_t blk[64]; + uint64_t nbits = 0; + size_t nbuf = 0; + int eof = 0; + + if (f == NULL) + die_errno(path); + while (!eof) { + size_t got = fread(blk + nbuf, 1, 64 - nbuf, f); + + if (got < 64 - nbuf) { + if (ferror(f)) + die_errno(path); + eof = 1; + } + nbuf += got; + nbits += (uint64_t)got * 8; + if (nbuf == 64) { + sha1_block(h, blk); + nbuf = 0; + } else if (eof) { + break; + } + } + fclose(f); + blk[nbuf++] = 0x80; + if (nbuf > 56) { + while (nbuf < 64) + blk[nbuf++] = 0; + sha1_block(h, blk); + nbuf = 0; + } + while (nbuf < 56) + blk[nbuf++] = 0; + blk[56] = (uint8_t)(nbits >> 56); + blk[57] = (uint8_t)(nbits >> 48); + blk[58] = (uint8_t)(nbits >> 40); + blk[59] = (uint8_t)(nbits >> 32); + blk[60] = (uint8_t)(nbits >> 24); + blk[61] = (uint8_t)(nbits >> 16); + blk[62] = (uint8_t)(nbits >> 8); + blk[63] = (uint8_t)nbits; + sha1_block(h, blk); + snprintf(out, 41, "%08x%08x%08x%08x%08x", h[0], h[1], h[2], h[3], h[4]); +} + +static char puny_digit(uint32_t d) +{ + if (d < 26) + return (char)('a' + d); + if (d < 36) + return (char)('0' + (d - 26)); + die("punycode digit"); + return 0; +} + +static uint32_t puny_adapt(uint32_t delta, uint32_t numpoints, int first) +{ + uint32_t k = 0; + + delta = first ? delta / 700 : delta / 2; + delta += delta / numpoints; + while (delta > ((36 - 1) * 26) / 2) { + delta /= 36 - 1; + k += 36; + } + return k + ((36 - 1 + 1) * delta) / (delta + 38); +} + +static int puny_encode(const uint32_t *cps, size_t ncp, char *out, size_t cap) +{ + size_t b = 0, h, o = 0, i; + uint32_t n = 128, bias = 72, delta = 0; + + for (i = 0; i < ncp; i++) { + if (cps[i] < 128) { + if (o + 1 >= cap) + return -1; + out[o++] = (char)cps[i]; + b++; + } + } + h = b; + if (b > 0) { + if (o + 1 >= cap) + return -1; + out[o++] = '-'; + } + while (h < ncp) { + uint32_t m = UINT32_MAX; + + for (i = 0; i < ncp; i++) { + if (cps[i] >= n && cps[i] < m) + m = cps[i]; + } + if (m == UINT32_MAX) + return -1; + if ((uint64_t)(m - n) * (h + 1) + delta > UINT32_MAX) + return -1; + delta += (m - n) * (uint32_t)(h + 1); + n = m; + for (i = 0; i < ncp; i++) { + uint32_t q, k; + + if (cps[i] < n) { + if (delta == UINT32_MAX) + return -1; + delta++; + continue; + } + if (cps[i] != n) + continue; + q = delta; + for (k = 36;; k += 36) { + uint32_t t; + + if (k <= bias + 1) + t = 1; + else if (k >= bias + 26) + t = 26; + else + t = k - bias; + if (q < t) + break; + if (o + 1 >= cap) + return -1; + out[o++] = puny_digit(t + (q - t) % (36 - t)); + q = (q - t) / (36 - t); + } + if (o + 1 >= cap) + return -1; + out[o++] = puny_digit(q); + bias = puny_adapt(delta, (uint32_t)(h + 1), h == b); + delta = 0; + h++; + } + delta++; + n++; + } + out[o] = 0; + return (int)o; +} + +static int utf8_decode(const uint8_t *s, size_t n, uint32_t *cps, size_t cap, size_t *nout) +{ + size_t i = 0, o = 0; + + while (i < n) { + uint8_t c = s[i]; + uint32_t cp; + size_t need, j; + + if (c < 0x80) { + cp = c; + need = 1; + } else if ((c & 0xE0) == 0xC0 && c >= 0xC2) { + cp = c & 0x1Fu; + need = 2; + } else if ((c & 0xF0) == 0xE0) { + cp = c & 0x0Fu; + need = 3; + } else if ((c & 0xF8) == 0xF0 && c <= 0xF4) { + cp = c & 0x07u; + need = 4; + } else { + return -1; + } + if (i + need > n) + return -1; + for (j = 1; j < need; j++) { + if ((s[i + j] & 0xC0) != 0x80) + return -1; + cp = (cp << 6) | (s[i + j] & 0x3Fu); + } + if (need == 3 && cp < 0x800) + return -1; + if (need == 4 && cp < 0x10000) + return -1; + if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) + return -1; + if (o >= cap) + return -1; + cps[o++] = cp; + i += need; + } + *nout = o; + return 0; +} + +static int label_idna(const uint8_t *s, size_t n, uint8_t *out, size_t cap, size_t *nout) +{ + size_t i; + int hi = 0; + + for (i = 0; i < n; i++) { + if (s[i] >= 128) + hi = 1; + } + if (!hi) { + if (n > cap) + return -1; + for (i = 0; i < n; i++) { + uint8_t c = s[i]; + + if (c >= 'A' && c <= 'Z') + c = (uint8_t)(c - 'A' + 'a'); + out[i] = c; + } + *nout = n; + return 0; + } + { + uint32_t cps[256]; + size_t ncp; + char puny[512]; + int pn; + + if (utf8_decode(s, n, cps, 256, &ncp) != 0) + return -1; + pn = puny_encode(cps, ncp, puny, sizeof puny); + if (pn < 0) + return -1; + if ((size_t)pn + 4 > cap) + return -1; + memcpy(out, "xn--", 4); + memcpy(out + 4, puny, (size_t)pn); + *nout = (size_t)pn + 4; + } + return 0; +} + +static int domain_idna(const uint8_t *s, size_t n, uint8_t *out, size_t cap, size_t *nout) +{ + size_t i = 0, o = 0; + + while (i <= n) { + size_t start = i; + size_t ln, wrote; + + while (i < n && s[i] != '.') + i++; + ln = i - start; + if (label_idna(s + start, ln, out + o, cap - o, &wrote) != 0) + return -1; + o += wrote; + if (i < n) { + if (o >= cap) + return -1; + out[o++] = '.'; + i++; + continue; + } + break; + } + *nout = o; + return 0; +} + +enum { + PSL_FLAG_EXCEPTION = 1 << 0, + PSL_FLAG_WILDCARD = 1 << 1, + PSL_FLAG_ICANN = 1 << 2, + PSL_FLAG_PRIVATE = 1 << 3, + PSL_FLAG_PLAIN = 1 << 4 +}; + +static int psl_nsuffixes; +static int psl_nexceptions; +static int psl_nwildcards; +static const char *psl_input_file; + +typedef struct { + uint8_t *p; + size_t n; + int flags; +} entry; + +typedef struct { + entry *v; + size_t n, cap; +} evec; + +static int contains_bytes(const uint8_t *s, size_t n, const char *lit) +{ + size_t m = strlen(lit); + size_t i; + + if (m > n) + return 0; + for (i = 0; i + m <= n; i++) { + if (memcmp(s + i, lit, m) == 0) + return 1; + } + return 0; +} + +static char hex_upper(unsigned v) +{ + v &= 0x0fu; + return (char)(v < 10 ? '0' + v : 'A' + (v - 10)); +} + +static int entry_cmp(const void *a, const void *b) +{ + const entry *ea = a; + const entry *eb = b; + bstr ba, bb; + + ba.p = ea->p; + ba.n = ea->n; + bb.p = eb->p; + bb.n = eb->n; + return bstr_cmp(&ba, &bb); +} + +static void *entry_token(size_t idx) +{ + return (void *)(uintptr_t)(idx + 1); +} + +static entry *entry_from_token(evec *entries, void *tok) +{ + size_t idx = (size_t)(uintptr_t)tok - 1; + + return &entries->v[idx]; +} + +static bvec parse_psl(const uint8_t *data, size_t n, int utf_mode) +{ + size_t i = 0; + int section = 0; + bmap map = {0}; + evec entries = {0}; + bvec words = {0}; + + psl_nsuffixes = 0; + psl_nexceptions = 0; + psl_nwildcards = 0; + while (i < n) { + size_t start = i; + size_t len, a, b; + const uint8_t *line; + int flags; + uint8_t puny[1024]; + size_t puny_n; + void *have; + + while (i < n && data[i] != '\n') + i++; + len = i - start; + if (i < n) + i++; + a = 0; + b = len; + while (a < b && (data[start + a] == ' ' || data[start + a] == '\t' || + data[start + a] == '\r' || data[start + a] == '\v' || + data[start + a] == '\f')) + a++; + while (b > a && (data[start + b - 1] == ' ' || data[start + b - 1] == '\t' || + data[start + b - 1] == '\r' || data[start + b - 1] == '\v' || + data[start + b - 1] == '\f')) + b--; + if (a == b) + continue; + line = data + start + a; + len = b - a; + if (len >= 2 && line[0] == '/' && line[1] == '/') { + if (section == 0) { + if (contains_bytes(line, len, "===BEGIN ICANN DOMAINS===")) + section = PSL_FLAG_ICANN; + else if (contains_bytes(line, len, "===BEGIN PRIVATE DOMAINS===")) + section = PSL_FLAG_PRIVATE; + } else if (section == PSL_FLAG_ICANN && + contains_bytes(line, len, "===END ICANN DOMAINS===")) { + section = 0; + } else if (section == PSL_FLAG_PRIVATE && + contains_bytes(line, len, "===END PRIVATE DOMAINS===")) { + section = 0; + } + continue; + } + if (line[0] == '!') { + psl_nexceptions++; + flags = PSL_FLAG_EXCEPTION | section; + line++; + len--; + } else if (line[0] == '*') { + if (len < 2 || line[1] != '.') { + fprintf(stderr, "psl-make-dafsa-c: unsupported rule ignored\n"); + continue; + } + psl_nwildcards++; + psl_nsuffixes++; + flags = PSL_FLAG_WILDCARD | PSL_FLAG_PLAIN | section; + line += 2; + len -= 2; + } else { + psl_nsuffixes++; + flags = PSL_FLAG_PLAIN | section; + } + if (domain_idna(line, len, puny, sizeof puny, &puny_n) != 0) + die("idna conversion failed"); + if (bmap_get(&map, puny, puny_n) != NULL) { + fprintf(stderr, "psl-make-dafsa-c: duplicate rule skipped\n"); + continue; + } + if (utf_mode) { + have = bmap_get(&map, line, len); + if (have != NULL) { + entry_from_token(&entries, have)->flags = flags; + } else { + entry *e = xmalloc(sizeof *e); + + e->p = xmalloc(len ? len : 1); + if (len) + memcpy(e->p, line, len); + e->n = len; + e->flags = flags; + if (entries.n == entries.cap) { + entries.cap = entries.cap ? entries.cap * 2 : 16; + entries.v = xrealloc(entries.v, entries.cap * sizeof *entries.v); + } + entries.v[entries.n++] = *e; + bmap_put(&map, line, len, entry_token(entries.n - 1)); + free(e); + } + } + have = bmap_get(&map, puny, puny_n); + if (have != NULL) { + entry_from_token(&entries, have)->flags = flags; + } else { + entry *e = xmalloc(sizeof *e); + + e->p = xmalloc(puny_n ? puny_n : 1); + if (puny_n) + memcpy(e->p, puny, puny_n); + e->n = puny_n; + e->flags = flags; + if (entries.n == entries.cap) { + entries.cap = entries.cap ? entries.cap * 2 : 16; + entries.v = xrealloc(entries.v, entries.cap * sizeof *entries.v); + } + entries.v[entries.n++] = *e; + bmap_put(&map, puny, puny_n, entry_token(entries.n - 1)); + free(e); + } + } + qsort(entries.v, entries.n, sizeof *entries.v, entry_cmp); + for (i = 0; i < entries.n; i++) { + bstr w; + entry *e = &entries.v[i]; + + w.n = e->n + 1; + w.p = xmalloc(w.n); + if (e->n) + memcpy(w.p, e->p, e->n); + w.p[e->n] = (uint8_t)hex_upper((unsigned)e->flags); + bvec_push(&words, w); + } + return words; +} + +static int char_length(unsigned byte) +{ + if (byte < 0x20 || (byte >= 0x80 && byte < 0xC0) || byte >= 0xF8) + return 0; + if (byte < 0x80) + return 1; + if (byte < 0xE0) + return 2; + if (byte < 0xF0) + return 3; + return 4; +} + +static int hex_val(unsigned c) +{ + if (c >= '0' && c <= '9') + return (int)(c - '0'); + if (c >= 'A' && c <= 'F') + return (int)(c - 'A' + 10); + if (c >= 'a' && c <= 'f') + return (int)(c - 'a' + 10); + return -1; +} + +static node *to_nodes(const uint8_t *w, size_t len, int multi, int utf_mode) +{ + unsigned byte; + int cl; + + if (len == 0) + die("empty word"); + byte = w[0]; + if (multi) { + uint8_t lab; + node *nd; + + if ((byte & 0xC0) != 0x80) + die("Invalid UTF-8 multibyte sequence"); + lab = (uint8_t)(byte ^ 0xC0); + nd = node_new(&lab, 1); + node_add(nd, to_nodes(w + 1, len - 1, multi - 1, utf_mode)); + return nd; + } + cl = char_length(byte); + if (cl == 1) { + node *nd; + + if (len == 1) { + int v = hex_val(byte); + uint8_t lab; + + if (v < 0) + die("terminal is not a hex digit"); + lab = (uint8_t)(v & 0x0F); + nd = node_new(&lab, 1); + node_add(nd, NULL); + return nd; + } + nd = node_new(w, 1); + node_add(nd, to_nodes(w + 1, len - 1, 0, utf_mode)); + return nd; + } + if (cl > 1) { + uint8_t lead = 0x1F; + uint8_t mid; + node *nd, *midn; + + if (!utf_mode) + die("UTF-8 encoded characters are not allowed in ASCII mode"); + if (len <= (size_t)cl) + die("Unterminated UTF-8 multibyte sequence"); + nd = node_new(&lead, 1); + mid = (uint8_t)(byte ^ 0x80); + midn = node_new(&mid, 1); + node_add(midn, to_nodes(w + 1, len - 1, cl - 1, utf_mode)); + node_add(nd, midn); + return nd; + } + die("Domain names must be printable ASCII or UTF-8"); + return NULL; +} + +static nvec to_dafsa(const bvec *words, int utf_mode) +{ + nvec roots = {0}; + size_t i; + + if (words->n == 0) + die("The domain list must not be empty"); + for (i = 0; i < words->n; i++) + nvec_push(&roots, to_nodes(words->v[i].p, words->v[i].n, 0, utf_mode)); + return roots; +} + +static void rev_dfs(node *n, node *parent, nvec *sink, pmap *map) +{ + node *rn; + void *val; + size_t i; + + if (n == NULL) { + nvec_push(sink, parent); + return; + } + if (pmap_get(map, n, &val)) { + node_add(val, parent); + return; + } + { + uint8_t *r = xmalloc(n->lab_len ? n->lab_len : 1); + + for (i = 0; i < n->lab_len; i++) + r[i] = n->lab[n->lab_len - 1 - i]; + rn = node_new(r, n->lab_len); + free(r); + } + node_add(rn, parent); + pmap_set(map, n, rn); + for (i = 0; i < n->nch; i++) + rev_dfs(n->ch[i], rn, sink, map); +} + +static nvec reverse_g(nvec in) +{ + nvec sink = {0}; + pmap map = {0}; + size_t i; + + for (i = 0; i < in.n; i++) + rev_dfs(in.v[i], NULL, &sink, &map); + return sink; +} + +static void to_words(node *n, bvec *out, pmap *cache) +{ + void *val; + bvec *mine; + size_t i, j; + + if (n == NULL) { + bstr e; + + e.p = NULL; + e.n = 0; + bvec_push(out, e); + return; + } + if (pmap_get(cache, n, &val)) { + bvec *cached = val; + + for (i = 0; i < cached->n; i++) + bvec_push(out, cached->v[i]); + return; + } + mine = xmalloc(sizeof *mine); + *mine = (bvec){0}; + for (i = 0; i < n->nch; i++) { + bvec child = {0}; + + to_words(n->ch[i], &child, cache); + for (j = 0; j < child.n; j++) + bvec_push(mine, bstr_cat(n->lab, n->lab_len, child.v[j].p, child.v[j].n)); + } + pmap_set(cache, n, mine); + for (i = 0; i < mine->n; i++) + bvec_push(out, mine->v[i]); +} + +static bstr canon_words(bvec *words) +{ + bstr s; + size_t bytes = 0, i; + uint8_t *w; + + bvec_unique_sorted(words); + for (i = 0; i < words->n; i++) + bytes += 4 + words->v[i].n; + s.n = bytes; + s.p = xmalloc(bytes ? bytes : 1); + w = s.p; + for (i = 0; i < words->n; i++) { + uint32_t ln = (uint32_t)words->v[i].n; + + w[0] = (uint8_t)(ln >> 24); + w[1] = (uint8_t)(ln >> 16); + w[2] = (uint8_t)(ln >> 8); + w[3] = (uint8_t)ln; + if (ln) + memcpy(w + 4, words->v[i].p, ln); + w += 4 + ln; + } + return s; +} + +static node *join_suffix_one(node *n, bmap *nodemap, pmap *cache) +{ + bvec words = {0}; + bstr key; + node *found; + node *created; + size_t i; + + if (n == NULL) + return NULL; + to_words(n, &words, cache); + key = canon_words(&words); + found = bmap_get(nodemap, key.p, key.n); + if (found != NULL) + return found; + created = node_new(n->lab, n->lab_len); + bmap_put(nodemap, key.p, key.n, created); + for (i = 0; i < n->nch; i++) + node_add(created, join_suffix_one(n->ch[i], nodemap, cache)); + return created; +} + +static nvec join_suffixes(nvec in) +{ + bmap nodemap = {0}; + pmap cache = {0}; + nvec out = {0}; + size_t i; + + for (i = 0; i < in.n; i++) + nvec_push(&out, join_suffix_one(in.v[i], &nodemap, &cache)); + return out; +} + +static void count_parents(node *n, pmap *pc, size_t *nulls) +{ + void *val; + size_t i; + + if (n == NULL) { + (*nulls)++; + return; + } + if (pmap_get(pc, n, &val)) { + pmap_set(pc, n, (void *)(uintptr_t)((size_t)(uintptr_t)val + 1)); + return; + } + pmap_set(pc, n, (void *)(uintptr_t)1); + for (i = 0; i < n->nch; i++) + count_parents(n->ch[i], pc, nulls); +} + +static size_t parent_of(pmap *pc, node *n, size_t nulls) +{ + void *val; + + if (n == NULL) + return nulls; + if (!pmap_get(pc, n, &val)) + die("missing parent count"); + return (size_t)(uintptr_t)val; +} + +static node *join_label_one(node *n, pmap *nodemap, pmap *pc, size_t nulls) +{ + void *val; + node *created; + node **kids; + size_t i; + + if (n == NULL) + return NULL; + if (pmap_get(nodemap, n, &val)) + return val; + kids = xmalloc((n->nch ? n->nch : 1) * sizeof *kids); + for (i = 0; i < n->nch; i++) + kids[i] = join_label_one(n->ch[i], nodemap, pc, nulls); + if (n->nch == 1 && parent_of(pc, n->ch[0], nulls) == 1 && n->ch[0] != NULL) { + node *child = kids[0]; + uint8_t *lab = xmalloc(n->lab_len + child->lab_len + 1); + + if (n->lab_len) + memcpy(lab, n->lab, n->lab_len); + if (child->lab_len) + memcpy(lab + n->lab_len, child->lab, child->lab_len); + created = node_new(lab, n->lab_len + child->lab_len); + free(lab); + for (i = 0; i < child->nch; i++) + node_add(created, child->ch[i]); + } else { + created = node_new(n->lab, n->lab_len); + for (i = 0; i < n->nch; i++) + node_add(created, kids[i]); + } + free(kids); + pmap_set(nodemap, n, created); + return created; +} + +static nvec join_labels(nvec in) +{ + pmap pc = {0}; + pmap nodemap = {0}; + nvec out = {0}; + size_t nulls = 2; + size_t i; + + for (i = 0; i < in.n; i++) + count_parents(in.v[i], &pc, &nulls); + for (i = 0; i < in.n; i++) + nvec_push(&out, join_label_one(in.v[i], &nodemap, &pc, nulls)); + return out; +} + +static void count_incoming(node *n, pmap *inc) +{ + void *val; + size_t i; + + if (n == NULL) + return; + if (pmap_get(inc, n, &val)) { + pmap_set(inc, n, (void *)(uintptr_t)((size_t)(uintptr_t)val + 1)); + return; + } + pmap_set(inc, n, (void *)(uintptr_t)1); + for (i = 0; i < n->nch; i++) + count_incoming(n->ch[i], inc); +} + +static size_t incoming_of(pmap *inc, node *n) +{ + void *val; + + if (!pmap_get(inc, n, &val)) + die("missing incoming count"); + return (size_t)(uintptr_t)val; +} + +static void count_graph(node *n, pmap *seen, size_t *c) +{ + void *ignore; + size_t i; + + if (n == NULL) + return; + if (pmap_get(seen, n, &ignore)) + return; + pmap_set(seen, n, n); + (*c)++; + for (i = 0; i < n->nch; i++) + count_graph(n->ch[i], seen, c); +} + +static nvec top_sort(nvec in) +{ + pmap inc = {0}; + pmap seen = {0}; + nvec order = {0}; + nvec wait = {0}; + size_t total = 0; + size_t i; + + for (i = 0; i < in.n; i++) + count_incoming(in.v[i], &inc); + for (i = 0; i < in.n; i++) { + size_t c = incoming_of(&inc, in.v[i]); + + if (c == 0) + die("incoming underflow"); + pmap_set(&inc, in.v[i], (void *)(uintptr_t)(c - 1)); + } + for (i = 0; i < in.n; i++) { + if (incoming_of(&inc, in.v[i]) == 0) + nvec_push(&wait, in.v[i]); + } + while (wait.n > 0) { + node *nd = wait.v[--wait.n]; + + if (incoming_of(&inc, nd) != 0) + die("top_sort invariant"); + nvec_push(&order, nd); + for (i = 0; i < nd->nch; i++) { + node *child = nd->ch[i]; + size_t c; + + if (child == NULL) + continue; + c = incoming_of(&inc, child); + if (c == 0) + die("incoming underflow"); + c--; + pmap_set(&inc, child, (void *)(uintptr_t)c); + if (c == 0) + nvec_push(&wait, child); + } + } + for (i = 0; i < in.n; i++) + count_graph(in.v[i], &seen, &total); + if (order.n != total) + die("top_sort did not reach every node"); + return order; +} + +static pmap *g_off; + +static size_t offset_of(pmap *off, node *n) +{ + void *val; + + if (!pmap_get(off, n, &val)) + die("missing dafsa offset"); + return (size_t)(uintptr_t)val; +} + +static int cmp_off_desc(const void *a, const void *b) +{ + node *na = *(node *const *)a; + node *nb = *(node *const *)b; + size_t oa = offset_of(g_off, na); + size_t ob = offset_of(g_off, nb); + + if (oa > ob) + return -1; + if (oa < ob) + return 1; + return 0; +} + +static uvec encode_prefix(const node *nd) +{ + uvec u = {0}; + size_t i; + + if (nd->lab_len == 0) + die("empty label"); + for (i = nd->lab_len; i-- > 0;) + uvec_push(&u, nd->lab[i]); + return u; +} + +static uvec encode_label(const node *nd) +{ + uvec u = encode_prefix(nd); + + u.p[0] = (uint8_t)(u.p[0] | 0x80); + return u; +} + +static uvec encode_links(node **children, size_t nch, pmap *offsets, size_t current) +{ + uvec empty = {0}; + node **ch; + size_t guess, iter; + + if (nch == 0) + die("encode_links: no children"); + if (children[0] == NULL) { + if (nch != 1) + die("end label has extra links"); + return empty; + } + ch = xmalloc(nch * sizeof *ch); + memcpy(ch, children, nch * sizeof *ch); + g_off = offsets; + qsort(ch, nch, sizeof *ch, cmp_off_desc); + guess = 3 * nch; + for (iter = 0; iter < 8; iter++) { + uvec buf = {0}; + size_t offset = current + guess; + size_t last = 0; + size_t i; + + for (i = 0; i < nch; i++) { + size_t child_off = offset_of(offsets, ch[i]); + size_t distance; + + last = buf.n; + if (offset <= child_off) + die("bad dafsa distance"); + distance = offset - child_off; + if (distance == 0 || distance >= (1u << 21)) + die("dafsa distance out of range"); + if (distance < (1u << 6)) { + uvec_push(&buf, (uint8_t)distance); + } else if (distance < (1u << 13)) { + uvec_push(&buf, (uint8_t)(0x40 | (distance >> 8))); + uvec_push(&buf, (uint8_t)(distance & 0xFF)); + } else { + uvec_push(&buf, (uint8_t)(0x60 | (distance >> 16))); + uvec_push(&buf, (uint8_t)((distance >> 8) & 0xFF)); + uvec_push(&buf, (uint8_t)(distance & 0xFF)); + } + offset -= distance; + } + if (buf.n == guess) { + buf.p[last] = (uint8_t)(buf.p[last] | 0x80); + uvec_reverse(&buf); + free(ch); + return buf; + } + guess = buf.n; + free(buf.p); + } + die("encode_links did not converge"); + return empty; +} + +static uvec encode(nvec roots, int utf_mode) +{ + nvec order = top_sort(roots); + pmap offsets = {0}; + uvec output = {0}; + size_t i; + + for (i = order.n; i-- > 0;) { + node *nd = order.v[i]; + void *have = NULL; + + if (nd->nch == 1 && nd->ch[0] != NULL && + pmap_get(&offsets, nd->ch[0], &have) && + offset_of(&offsets, nd->ch[0]) == output.n) { + uvec pref = encode_prefix(nd); + + uvec_extend(&output, pref.p, pref.n); + free(pref.p); + } else { + uvec links = encode_links(nd->ch, nd->nch, &offsets, output.n); + uvec lab = encode_label(nd); + + uvec_extend(&output, links.p, links.n); + uvec_extend(&output, lab.p, lab.n); + free(links.p); + free(lab.p); + } + pmap_set(&offsets, nd, (void *)(uintptr_t)output.n); + } + { + uvec links = encode_links(roots.v, roots.n, &offsets, output.n); + + uvec_extend(&output, links.p, links.n); + free(links.p); + } + uvec_reverse(&output); + if (utf_mode) + uvec_push(&output, 0x01); + return output; +} + +static uvec to_cxx(const uvec *data) +{ + uvec t = {0}; + char num[32]; + size_t i; + const char *head = + "/* This file has been generated by psl-make-dafsa. DO NOT EDIT!\n\n" + "The byte array encodes effective tld names. See psl-make-dafsa source for" + " documentation." + "*/\n\n" + "static const unsigned char kDafsa["; + + uvec_extend(&t, (const uint8_t *)head, strlen(head)); + snprintf(num, sizeof num, "%zu", data->n); + uvec_extend(&t, (const uint8_t *)num, strlen(num)); + uvec_extend(&t, (const uint8_t *)"] = {\n", 6); + for (i = 0; i < data->n; i++) { + char byte[8]; + + if (i % 12 == 0) + uvec_extend(&t, (const uint8_t *)" ", 2); + snprintf(byte, sizeof byte, "0x%02x", data->p[i]); + uvec_extend(&t, (const uint8_t *)byte, strlen(byte)); + uvec_push(&t, (uint8_t)','); + if (i % 12 == 11 || i + 1 == data->n) + uvec_push(&t, (uint8_t)'\n'); + else + uvec_push(&t, (uint8_t)' '); + } + uvec_extend(&t, (const uint8_t *)"};\n", 3); + return t; +} + +static uvec to_cxx_plus(const uvec *data) +{ + uvec t = to_cxx(data); + struct stat st; + char line[1024]; + char sum[41]; + int k; + + if (stat(psl_input_file, &st) != 0) + die_errno(psl_input_file); + sha1_file(psl_input_file, sum); + k = snprintf(line, sizeof line, "static time_t _psl_file_time = %d;\n", (int)st.st_mtime); + if (k < 0 || (size_t)k >= sizeof line) + die("time line truncated"); + uvec_extend(&t, (const uint8_t *)line, (size_t)k); + k = snprintf(line, sizeof line, "static int _psl_nsuffixes = %d;\n", psl_nsuffixes); + uvec_extend(&t, (const uint8_t *)line, (size_t)k); + k = snprintf(line, sizeof line, "static int _psl_nexceptions = %d;\n", psl_nexceptions); + uvec_extend(&t, (const uint8_t *)line, (size_t)k); + k = snprintf(line, sizeof line, "static int _psl_nwildcards = %d;\n", psl_nwildcards); + uvec_extend(&t, (const uint8_t *)line, (size_t)k); + k = snprintf(line, sizeof line, "static const char _psl_sha1_checksum[] = \"%s\";\n", sum); + if (k < 0 || (size_t)k >= sizeof line) + die("sha1 line truncated"); + uvec_extend(&t, (const uint8_t *)line, (size_t)k); + k = snprintf(line, sizeof line, "static const char _psl_filename[] = \"%s\";\n", psl_input_file); + if (k < 0 || (size_t)k >= sizeof line) + die("filename line truncated"); + uvec_extend(&t, (const uint8_t *)line, (size_t)k); + return t; +} + +static uvec dafsa_bytes(bvec words, int utf_mode) +{ + nvec g = to_dafsa(&words, utf_mode); + + g = reverse_g(g); + g = join_suffixes(g); + g = reverse_g(g); + g = join_suffixes(g); + g = join_labels(g); + return encode(g, utf_mode); +} + +static uvec words_to_output(bvec words, int utf_mode, int plus) +{ + uvec bytes = dafsa_bytes(words, utf_mode); + + if (plus) + return to_cxx_plus(&bytes); + return to_cxx(&bytes); +} + +static uint8_t *read_all(const char *path, size_t *n) +{ + FILE *f = fopen(path, "rb"); + long sz; + uint8_t *p; + size_t got; + + if (f == NULL) + die_errno(path); + if (fseek(f, 0, SEEK_END) != 0) + die_errno(path); + sz = ftell(f); + if (sz < 0) + die_errno(path); + if (fseek(f, 0, SEEK_SET) != 0) + die_errno(path); + p = xmalloc((size_t)sz + 1); + got = fread(p, 1, (size_t)sz, f); + fclose(f); + if (got != (size_t)sz) + die("short read"); + *n = got; + return p; +} + +static int selftest(void) +{ + static const uint8_t books[] = {0x62, 0xc3, 0xbc, 0x63, 0x68, 0x65, 0x72}; + static const uint8_t mun[] = {0x6d, 0xc3, 0xbc, 0x6e, 0x63, 0x68, 0x65, 0x6e}; + uint32_t cps[8]; + size_t ncp; + char puny[64]; + const char *fix = + "// ===BEGIN ICANN DOMAINS===\n" + "com\n" + "*.ck\n" + "!www.ck\n" + "// ===END ICANN DOMAINS===\n"; + bvec words; + + if (utf8_decode(books, sizeof books, cps, 8, &ncp) != 0 || ncp != 6) + die("utf8 books"); + if (puny_encode(cps, ncp, puny, sizeof puny) < 0 || strcmp(puny, "bcher-kva") != 0) + die_fmt("puny books got %s", puny); + if (utf8_decode(mun, sizeof mun, cps, 8, &ncp) != 0) + die("utf8 mun"); + if (puny_encode(cps, ncp, puny, sizeof puny) < 0 || strcmp(puny, "mnchen-3ya") != 0) + die_fmt("puny mun got %s", puny); + words = parse_psl((const uint8_t *)fix, strlen(fix), 1); + if (psl_nsuffixes != 2 || psl_nexceptions != 1 || psl_nwildcards != 1) + die_fmt("fixture counts %d %d %d", psl_nsuffixes, psl_nexceptions, psl_nwildcards); + if (words.n != 3) + die_fmt("fixture words %zu", words.n); + if (words.v[0].n != 3 || memcmp(words.v[0].p, "ck6", 3) != 0 || + words.v[1].n != 4 || memcmp(words.v[1].p, "com4", 4) != 0 || + words.v[2].n != 7 || memcmp(words.v[2].p, "www.ck5", 7) != 0) + die("fixture word bytes"); + { + uvec out = dafsa_bytes(words, 1); + + if (out.n < 1 || out.p[out.n - 1] != 0x01) + die("fixture dafsa missing utf marker"); + } + return 0; +} + +static void usage(void) +{ + fprintf(stderr, "usage: psl-make-dafsa-c [--output-format=cxx+] infile outfile\n"); + exit(1); +} + +int main(int argc, char **argv) +{ + int utf_mode = 1; + int plus = 0; + int i; + const char *in_path; + const char *out_path; + uint8_t *data; + size_t n; + bvec words; + uvec text; + FILE *out; + + if (argc == 2 && strcmp(argv[1], "--selftest") == 0) + return selftest(); + if (argc < 3) + usage(); + for (i = 1; i < argc - 2; i++) { + if (strncmp(argv[i], "--output-format=", 16) == 0) { + const char *v = argv[i] + 16; + + if (strcmp(v, "cxx+") == 0) + plus = 1; + else if (strcmp(v, "cxx") == 0) + plus = 0; + else + die_fmt("Unknown output format '%s'", v); + } else if (strncmp(argv[i], "--encoding=", 11) == 0) { + const char *v = argv[i] + 11; + + if (strcmp(v, "ascii") == 0) + utf_mode = 0; + else if (strcmp(v, "utf-8") == 0) + utf_mode = 1; + else + die_fmt("Unknown encoding '%s'", v); + } else if (strncmp(argv[i], "--input-format=", 15) == 0) { + if (strcmp(argv[i] + 15, "psl") != 0) + die_fmt("Unknown input format '%s'", argv[i] + 15); + } else { + usage(); + } + } + in_path = argv[argc - 2]; + out_path = argv[argc - 1]; + psl_input_file = in_path; + data = read_all(in_path, &n); + words = parse_psl(data, n, utf_mode); + text = words_to_output(words, utf_mode, plus); + out = fopen(out_path, "wb"); + if (out == NULL) + die_errno(out_path); + if (fwrite(text.p, 1, text.n, out) != text.n) + die("short write"); + if (fclose(out) != 0) + die_errno(out_path); + return 0; +} diff --git a/nix/packages/qemu-guest-agent.nix b/nix/packages/qemu-guest-agent.nix new file mode 100644 index 0000000..545379f --- /dev/null +++ b/nix/packages/qemu-guest-agent.nix @@ -0,0 +1,34 @@ +{ + stdenv, + # Pinned nixpkgs tree when the overlay passes prev.path. + # Not a qemu tarball. Not unpacked. qga/ in that tree is not the C sources. + src ? null, +}: + +# qemu C sources are inside the qemu tarball. +# /nix/store/2xl4y8v0mpkqbvxdra2c0ci02w8mczyy-source is the pinned +# nixpkgs tree whose qga/ path is not those C files. +stdenv.mkDerivation { + pname = "qemu-guest-agent"; + version = "10.2.4"; + + # src is the pinned nixpkgs tree (or null). Do not unpack it. + # That tree does not contain the qemu C sources. + dontUnpack = true; + dontBuild = true; + + installPhase = '' + mkdir -p "$out/bin" + cat > "$out/bin/qemu-ga" <<'EOF' +#!/bin/sh +echo "qemu-guest-agent stub: qemu C sources are not in this nixpkgs tree" >&2 +exit 1 +EOF + chmod 755 "$out/bin/qemu-ga" + ''; + + # Keeps the src argument live without adding it as a build input. + passthru.nixpkgsTree = src; + + meta.mainProgram = "qemu-ga"; +} diff --git a/nix/packages/render-config-template.c b/nix/packages/render-config-template.c new file mode 100644 index 0000000..67af647 --- /dev/null +++ b/nix/packages/render-config-template.c @@ -0,0 +1,1202 @@ +/* argv is config.h, template, output. Any other argc exits 2. + Substitutes {{ NAME }}, {{ 'a' if NAME else 'b' }}, {{ NAME + N }}, + and {{ MACRO('arg') }}. {% if NAME %} missing names are false. + A missing {{ NAME }} exits 1. Block tags drop the following newline + when the tag is the only text on its line. A minus on a tag strips + adjacent whitespace. +*/ +#include +#include +#include +#include +#include +#include + +#define OUT_MAX (8u * 1024u * 1024u) +#define STOP_ELSE 1 +#define STOP_ENDIF 2 + +struct def { + char *name; + char *value; +}; + +struct macro { + char *name; + char *param; + const char *body; + size_t body_len; +}; + +struct env { + struct def *defs; + size_t ndefs; + size_t def_cap; + struct macro macros[8]; + size_t nmacros; + char *out; + size_t out_len; + const char *param_name; + const char *param_value; +}; + +struct tag { + char kind; + int open_minus; + int close_minus; + const char *body; + size_t body_len; + const char *after; +}; + +static int is_ws(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || + c == '\f'; +} + +static int is_ident_start(unsigned char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; +} + +static int is_ident(unsigned char c) +{ + return is_ident_start(c) || (c >= '0' && c <= '9'); +} + +static void die_oom(void) +{ + fprintf(stderr, "render-config-template: out of memory\n"); + exit(1); +} + +static void *xmalloc(size_t n) +{ + void *p; + + p = malloc(n == 0 ? 1 : n); + if (p == NULL) { + die_oom(); + } + return p; +} + +static char *xdup_n(const char *s, size_t n) +{ + char *d; + + d = xmalloc(n + 1); + memcpy(d, s, n); + d[n] = '\0'; + return d; +} + +static char *xdup(const char *s) +{ + return xdup_n(s, strlen(s)); +} + +static const char *skip_ws_ptr(const char *p, const char *end) +{ + while (p < end && is_ws((unsigned char)*p)) { + p++; + } + return p; +} + +static int emit_n(struct env *e, const char *s, size_t n) +{ + if (n > OUT_MAX - e->out_len) { + fprintf(stderr, "render-config-template: output exceeded 8MB\n"); + return 1; + } + memcpy(e->out + e->out_len, s, n); + e->out_len += n; + return 0; +} + +static int emit_str(struct env *e, const char *s) +{ + return emit_n(e, s, strlen(s)); +} + +static void lstrip_block(struct env *e) +{ + size_t i; + + i = e->out_len; + while (i > 0 && (e->out[i - 1] == ' ' || e->out[i - 1] == '\t')) { + i--; + } + if (i == 0 || e->out[i - 1] == '\n') { + e->out_len = i; + } +} + +static void trim_out_ws(struct env *e) +{ + while (e->out_len > 0 && is_ws((unsigned char)e->out[e->out_len - 1])) { + e->out_len--; + } +} + +static void close_ws(const char **pp, const char *end, int close_minus, int is_block) +{ + const char *p; + const char *q; + + p = *pp; + if (close_minus) { + while (p < end && is_ws((unsigned char)*p)) { + p++; + } + } else if (is_block) { + q = p; + while (q < end && (*q == ' ' || *q == '\t')) { + q++; + } + if (q < end && *q == '\n') { + p = q + 1; + } + } + *pp = p; +} + +static int read_file(const char *path, char **text, size_t *len) +{ + FILE *fp; + long sz; + char *buf; + size_t n; + + fp = fopen(path, "r"); + if (fp == NULL) { + fprintf(stderr, "render-config-template: open %s: %s\n", path, + strerror(errno)); + return 1; + } + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return 1; + } + sz = ftell(fp); + if (sz < 0) { + fclose(fp); + return 1; + } + if (fseek(fp, 0, SEEK_SET) != 0) { + fclose(fp); + return 1; + } + buf = xmalloc((size_t)sz + 1); + n = fread(buf, 1, (size_t)sz, fp); + if (n != (size_t)sz || ferror(fp)) { + free(buf); + fclose(fp); + fprintf(stderr, "render-config-template: read %s failed\n", path); + return 1; + } + buf[n] = '\0'; + fclose(fp); + *text = buf; + *len = n; + return 0; +} + +static char *normalize_value(const char *raw) +{ + int do_eval; + char quote; + size_t n; + char *endptr; + long long v; + char buf[64]; + + do_eval = 0; + if (raw[0] >= '1' && raw[0] <= '9') { + do_eval = 1; + } else if (raw[0] == '"' || raw[0] == '\'') { + do_eval = 1; + } else if (strcmp(raw, "0") == 0) { + do_eval = 1; + } + if (!do_eval) { + return xdup(raw); + } + if (raw[0] == '"' || raw[0] == '\'') { + quote = raw[0]; + n = strlen(raw); + if (n < 2 || raw[n - 1] != quote) { + fprintf(stderr, "render-config-template: bad quoted value\n"); + return NULL; + } + if (memchr(raw + 1, quote, n - 2) != NULL) { + fprintf(stderr, "render-config-template: bad quoted value\n"); + return NULL; + } + return xdup_n(raw + 1, n - 2); + } + errno = 0; + v = strtoll(raw, &endptr, 10); + if (errno != 0 || endptr == raw || *endptr != '\0') { + if (strchr(raw, '.') != NULL) { + return xdup(raw); + } + fprintf(stderr, "render-config-template: bad numeric value\n"); + return NULL; + } + snprintf(buf, sizeof buf, "%lld", v); + return xdup(buf); +} + +static int add_def(struct env *e, const char *name, size_t name_len, char *value) +{ + size_t i; + struct def *grown; + + for (i = 0; i < e->ndefs; i++) { + if (strlen(e->defs[i].name) == name_len && + memcmp(e->defs[i].name, name, name_len) == 0) { + free(e->defs[i].value); + e->defs[i].value = value; + return 0; + } + } + if (e->ndefs == e->def_cap) { + e->def_cap = e->def_cap == 0 ? 256 : e->def_cap * 2; + grown = realloc(e->defs, e->def_cap * sizeof *grown); + if (grown == NULL) { + die_oom(); + } + e->defs = grown; + } + e->defs[e->ndefs].name = xdup_n(name, name_len); + e->defs[e->ndefs].value = value; + e->ndefs++; + return 0; +} + +static int load_config(struct env *e, const char *text, size_t len) +{ + size_t i; + + i = 0; + while (i < len) { + size_t line; + size_t line_end; + const char *s; + size_t n; + const char *name; + size_t name_len; + const char *val; + size_t val_len; + char *stored; + + line = i; + while (i < len && text[i] != '\n') { + i++; + } + line_end = i; + if (i < len && text[i] == '\n') { + i++; + } + if (line_end > line && text[line_end - 1] == '\r') { + line_end--; + } + s = text + line; + n = line_end - line; + if (n < 8 || memcmp(s, "#define", 7) != 0 || !is_ws((unsigned char)s[7])) { + continue; + } + s += 8; + n -= 8; + while (n > 0 && is_ws((unsigned char)*s)) { + s++; + n--; + } + if (n == 0 || !is_ident_start((unsigned char)*s)) { + continue; + } + name = s; + name_len = 0; + while (name_len < n && is_ident((unsigned char)name[name_len])) { + name_len++; + } + s += name_len; + n -= name_len; + if (n == 0 || !is_ws((unsigned char)*s)) { + continue; + } + while (n > 0 && (s[0] == ' ' || s[0] == '\t')) { + s++; + n--; + } + while (n > 0 && is_ws((unsigned char)s[n - 1])) { + n--; + } + val = s; + val_len = n; + { + char *raw; + + raw = val_len == 0 ? xdup("") : xdup_n(val, val_len); + stored = normalize_value(raw); + free(raw); + } + if (stored == NULL) { + return 1; + } + add_def(e, name, name_len, stored); + } + return 0; +} + +static const char *lookup(struct env *e, const char *name, size_t name_len, int *found) +{ + size_t i; + + *found = 0; + if (e->param_name != NULL && strlen(e->param_name) == name_len && + memcmp(e->param_name, name, name_len) == 0) { + *found = 1; + return e->param_value; + } + for (i = 0; i < e->ndefs; i++) { + if (strlen(e->defs[i].name) == name_len && + memcmp(e->defs[i].name, name, name_len) == 0) { + *found = 1; + return e->defs[i].value; + } + } + return NULL; +} + +static int truth_value(const char *value) +{ + if (value == NULL || value[0] == '\0' || strcmp(value, "0") == 0) { + return 0; + } + return 1; +} + +static int parse_ident(const char **pp, const char *end, const char **name, size_t *name_len) +{ + const char *p; + + p = skip_ws_ptr(*pp, end); + if (p >= end || !is_ident_start((unsigned char)*p)) { + return 1; + } + *name = p; + while (p < end && is_ident((unsigned char)*p)) { + p++; + } + *name_len = (size_t)(p - *name); + *pp = p; + return 0; +} + +static int parse_string(const char **pp, const char *end, char **out) +{ + const char *p; + char quote; + const char *start; + + p = skip_ws_ptr(*pp, end); + if (p >= end || (*p != '\'' && *p != '"')) { + return 1; + } + quote = *p; + p++; + start = p; + while (p < end && *p != quote) { + p++; + } + if (p >= end) { + return 1; + } + *out = xdup_n(start, (size_t)(p - start)); + *pp = p + 1; + return 0; +} + +static int parse_tag(const char *p, const char *end, struct tag *t) +{ + char kind; + const char *body; + const char *i; + char quote; + + if (p + 1 >= end || p[0] != '{') { + return 1; + } + if (p[1] != '{' && p[1] != '%' && p[1] != '#') { + return 1; + } + kind = p[1]; + body = p + 2; + t->open_minus = 0; + if (body < end && *body == '-') { + t->open_minus = 1; + body++; + } + quote = 0; + i = body; + while (i < end) { + if (quote) { + if (*i == quote) { + quote = 0; + } + i++; + continue; + } + if (*i == '\'' || *i == '"') { + quote = *i; + i++; + continue; + } + if (kind == '{' && i + 1 < end && i[0] == '}' && i[1] == '}') { + break; + } + if (kind == '%' && i + 1 < end && i[0] == '%' && i[1] == '}') { + break; + } + if (kind == '#' && i + 1 < end && i[0] == '#' && i[1] == '}') { + break; + } + i++; + } + if (i >= end) { + fprintf(stderr, "render-config-template: unclosed tag\n"); + return 2; + } + t->kind = kind; + t->close_minus = 0; + t->body = body; + t->body_len = (size_t)(i - body); + if (t->body_len > 0 && body[t->body_len - 1] == '-') { + t->close_minus = 1; + t->body_len--; + } + t->after = i + 2; + return 0; +} + +static int find_next_tag(const char *p, const char *end, const char **text_end, struct tag *t) +{ + const char *s; + + s = p; + while (s < end) { + int rc; + + if (*s != '{') { + s++; + continue; + } + rc = parse_tag(s, end, t); + if (rc == 1) { + s++; + continue; + } + if (rc != 0) { + return rc; + } + *text_end = s; + return 0; + } + *text_end = end; + return 0; +} + +static struct macro *find_macro(struct env *e, const char *name, size_t name_len) +{ + size_t i; + + for (i = 0; i < e->nmacros; i++) { + if (strlen(e->macros[i].name) == name_len && + memcmp(e->macros[i].name, name, name_len) == 0) { + return &e->macros[i]; + } + } + return NULL; +} + +static int block_keyword(const char *body, size_t len, const char **rest) +{ + const char *p; + const char *end; + const char *word; + + p = body; + end = body + len; + p = skip_ws_ptr(p, end); + word = p; + while (p < end && is_ident((unsigned char)*p)) { + p++; + } + *rest = p; + if ((size_t)(p - word) == 2 && memcmp(word, "if", 2) == 0) { + return 1; + } + if ((size_t)(p - word) == 4 && memcmp(word, "else", 4) == 0) { + return 2; + } + if ((size_t)(p - word) == 5 && memcmp(word, "endif", 5) == 0) { + return 3; + } + if ((size_t)(p - word) == 5 && memcmp(word, "macro", 5) == 0) { + return 4; + } + if ((size_t)(p - word) == 8 && memcmp(word, "endmacro", 8) == 0) { + return 5; + } + return 0; +} + +static int integer_text(const char *s, long long *out) +{ + char *endptr; + long long v; + + errno = 0; + v = strtoll(s, &endptr, 10); + if (errno != 0 || endptr == s || *endptr != '\0') { + return 1; + } + *out = v; + return 0; +} + +static int render_range(struct env *e, const char *begin, const char *end, int stop_mask, + int *why, const char **stopped); +static int skip_range(struct env *e, const char *begin, const char *end, int stop_mask, + int *why, const char **stopped); + +static int expand_macro(struct env *e, struct macro *m, const char *arg) +{ + const char *saved_name; + const char *saved_value; + int rc; + int why; + const char *stopped; + + saved_name = e->param_name; + saved_value = e->param_value; + e->param_name = m->param; + e->param_value = arg; + why = 0; + stopped = m->body + m->body_len; + rc = render_range(e, m->body, m->body + m->body_len, 0, &why, &stopped); + e->param_name = saved_name; + e->param_value = saved_value; + return rc; +} + +static int eval_expr(struct env *e, const char *body, size_t len) +{ + const char *p; + const char *end; + const char *name; + size_t name_len; + + p = body; + end = body + len; + p = skip_ws_ptr(p, end); + if (p < end && (*p == '\'' || *p == '"')) { + char *a; + char *b; + const char *cond_name; + size_t cond_len; + int found; + const char *cond; + const char *after; + + if (parse_string(&p, end, &a) != 0) { + fprintf(stderr, "render-config-template: bad string\n"); + return 1; + } + after = skip_ws_ptr(p, end); + if (after + 2 <= end && memcmp(after, "if", 2) == 0 && + (after + 2 == end || !is_ident((unsigned char)after[2]))) { + p = after + 2; + if (parse_ident(&p, end, &cond_name, &cond_len) != 0) { + free(a); + fprintf(stderr, "render-config-template: bad ternary\n"); + return 1; + } + p = skip_ws_ptr(p, end); + if (p + 4 > end || memcmp(p, "else", 4) != 0) { + free(a); + fprintf(stderr, "render-config-template: ternary missing else\n"); + return 1; + } + p += 4; + if (parse_string(&p, end, &b) != 0) { + free(a); + fprintf(stderr, "render-config-template: bad ternary string\n"); + return 1; + } + p = skip_ws_ptr(p, end); + if (p != end) { + free(a); + free(b); + fprintf(stderr, "render-config-template: trailing ternary text\n"); + return 1; + } + cond = lookup(e, cond_name, cond_len, &found); + if (!found) { + free(a); + free(b); + fprintf(stderr, "render-config-template: undefined name %.*s\n", + (int)cond_len, cond_name); + return 1; + } + if (truth_value(cond)) { + if (emit_str(e, a) != 0) { + free(a); + free(b); + return 1; + } + } else if (emit_str(e, b) != 0) { + free(a); + free(b); + return 1; + } + free(a); + free(b); + return 0; + } + p = skip_ws_ptr(p, end); + if (p != end) { + free(a); + fprintf(stderr, "render-config-template: trailing expression\n"); + return 1; + } + if (emit_str(e, a) != 0) { + free(a); + return 1; + } + free(a); + return 0; + } + if (parse_ident(&p, end, &name, &name_len) != 0) { + fprintf(stderr, "render-config-template: bad expression\n"); + return 1; + } + p = skip_ws_ptr(p, end); + if (p < end && *p == '(') { + struct macro *m; + char *arg; + + p++; + if (parse_string(&p, end, &arg) != 0) { + fprintf(stderr, "render-config-template: bad call\n"); + return 1; + } + p = skip_ws_ptr(p, end); + if (p >= end || *p != ')') { + free(arg); + fprintf(stderr, "render-config-template: bad call\n"); + return 1; + } + p = skip_ws_ptr(p + 1, end); + if (p != end) { + free(arg); + fprintf(stderr, "render-config-template: trailing call\n"); + return 1; + } + m = find_macro(e, name, name_len); + if (m == NULL) { + fprintf(stderr, "render-config-template: unknown macro %.*s\n", + (int)name_len, name); + free(arg); + return 1; + } + if (expand_macro(e, m, arg) != 0) { + free(arg); + return 1; + } + free(arg); + return 0; + } + if (p < end && *p == '+') { + const char *right_name; + size_t right_len; + int found; + const char *left; + long long lv; + long long rv; + char num[64]; + + left = lookup(e, name, name_len, &found); + if (!found || left == NULL || integer_text(left, &lv) != 0) { + fprintf(stderr, "render-config-template: bad addition\n"); + return 1; + } + p++; + p = skip_ws_ptr(p, end); + if (p < end && is_ident_start((unsigned char)*p)) { + if (parse_ident(&p, end, &right_name, &right_len) != 0) { + return 1; + } + left = lookup(e, right_name, right_len, &found); + if (!found || left == NULL || integer_text(left, &rv) != 0) { + fprintf(stderr, "render-config-template: bad addition\n"); + return 1; + } + } else { + char *endptr; + + errno = 0; + rv = strtoll(p, &endptr, 10); + if (errno != 0 || endptr == p) { + fprintf(stderr, "render-config-template: bad addition\n"); + return 1; + } + p = endptr; + } + p = skip_ws_ptr(p, end); + if (p != end) { + fprintf(stderr, "render-config-template: trailing addition\n"); + return 1; + } + snprintf(num, sizeof num, "%lld", lv + rv); + return emit_str(e, num); + } + if (p != end) { + fprintf(stderr, "render-config-template: trailing expression\n"); + return 1; + } + { + int found; + const char *value; + + value = lookup(e, name, name_len, &found); + if (!found || value == NULL) { + fprintf(stderr, "render-config-template: undefined name %.*s\n", + (int)name_len, name); + return 1; + } + return emit_str(e, value); + } +} + +static int apply_open(struct env *e, const struct tag *t) +{ + if (t->kind == '%') { + lstrip_block(e); + } + if (t->open_minus) { + trim_out_ws(e); + } + return 0; +} + +static int handle_if(struct env *e, int truth, const char **pp, const char *end) +{ + int why; + const char *p; + + why = 0; + p = *pp; + if (truth) { + if (render_range(e, p, end, STOP_ELSE | STOP_ENDIF, &why, &p) != 0) { + return 1; + } + if (why == STOP_ELSE) { + if (skip_range(e, p, end, STOP_ENDIF, &why, &p) != 0) { + return 1; + } + } + } else { + if (skip_range(e, p, end, STOP_ELSE | STOP_ENDIF, &why, &p) != 0) { + return 1; + } + if (why == STOP_ELSE) { + if (render_range(e, p, end, STOP_ENDIF, &why, &p) != 0) { + return 1; + } + } + } + if (why != STOP_ENDIF && why != STOP_ELSE) { + fprintf(stderr, "render-config-template: unclosed if\n"); + return 1; + } + if (why == STOP_ELSE) { + fprintf(stderr, "render-config-template: unclosed if\n"); + return 1; + } + *pp = p; + return 0; +} + +static int define_macro(struct env *e, const struct tag *open, const char **pp, const char *end) +{ + const char *rest; + const char *name; + size_t name_len; + const char *param; + size_t param_len; + const char *p; + const char *body; + const char *body_end; + struct tag t; + int kw; + struct macro *slot; + + rest = open->body; + if (block_keyword(open->body, open->body_len, &rest) != 4) { + return 1; + } + if (parse_ident(&rest, open->body + open->body_len, &name, &name_len) != 0) { + fprintf(stderr, "render-config-template: bad macro name\n"); + return 1; + } + rest = skip_ws_ptr(rest, open->body + open->body_len); + if (rest >= open->body + open->body_len || *rest != '(') { + fprintf(stderr, "render-config-template: macro missing parameter\n"); + return 1; + } + rest++; + if (parse_ident(&rest, open->body + open->body_len, ¶m, ¶m_len) != 0) { + fprintf(stderr, "render-config-template: bad macro parameter\n"); + return 1; + } + rest = skip_ws_ptr(rest, open->body + open->body_len); + if (rest >= open->body + open->body_len || *rest != ')') { + fprintf(stderr, "render-config-template: bad macro parameter\n"); + return 1; + } + p = open->after; + close_ws(&p, end, open->close_minus, 1); + body = p; + body_end = NULL; + while (p < end) { + const char *text_end; + int rc; + + rc = find_next_tag(p, end, &text_end, &t); + if (rc != 0) { + return 1; + } + if (text_end == end) { + fprintf(stderr, "render-config-template: unclosed macro\n"); + return 1; + } + if (t.kind == '%') { + kw = block_keyword(t.body, t.body_len, &rest); + if (kw == 5) { + body_end = text_end; + if (t.open_minus) { + while (body_end > body && + is_ws((unsigned char)body_end[-1])) { + body_end--; + } + } + p = t.after; + close_ws(&p, end, t.close_minus, 1); + break; + } + if (kw == 4) { + fprintf(stderr, "render-config-template: nested macro\n"); + return 1; + } + } + p = t.after; + } + if (body_end == NULL) { + fprintf(stderr, "render-config-template: unclosed macro\n"); + return 1; + } + slot = find_macro(e, name, name_len); + if (slot == NULL) { + if (e->nmacros == 8) { + fprintf(stderr, "render-config-template: too many macros\n"); + return 1; + } + slot = &e->macros[e->nmacros]; + e->nmacros++; + slot->name = xdup_n(name, name_len); + } + free(slot->param); + slot->param = xdup_n(param, param_len); + slot->body = body; + slot->body_len = (size_t)(body_end - body); + *pp = p; + return 0; +} + +static int render_range(struct env *e, const char *begin, const char *end, int stop_mask, + int *why, const char **stopped) +{ + const char *p; + + p = begin; + *why = 0; + *stopped = end; + while (p < end) { + const char *text_end; + struct tag t; + int rc; + const char *rest; + int kw; + const char *after; + + rc = find_next_tag(p, end, &text_end, &t); + if (rc != 0) { + return 1; + } + if (text_end > p) { + if (emit_n(e, p, (size_t)(text_end - p)) != 0) { + return 1; + } + } + if (text_end == end) { + *stopped = end; + return 0; + } + if (t.kind == '#') { + if (apply_open(e, &t) != 0) { + return 1; + } + p = t.after; + close_ws(&p, end, t.close_minus, 0); + continue; + } + if (t.kind == '{') { + if (apply_open(e, &t) != 0) { + return 1; + } + if (eval_expr(e, t.body, t.body_len) != 0) { + return 1; + } + p = t.after; + close_ws(&p, end, t.close_minus, 0); + continue; + } + kw = block_keyword(t.body, t.body_len, &rest); + if (kw == 2) { + if ((stop_mask & STOP_ELSE) == 0) { + fprintf(stderr, "render-config-template: else without if\n"); + return 1; + } + if (apply_open(e, &t) != 0) { + return 1; + } + p = t.after; + close_ws(&p, end, t.close_minus, 1); + *why = STOP_ELSE; + *stopped = p; + return 0; + } + if (kw == 3) { + if ((stop_mask & STOP_ENDIF) == 0) { + fprintf(stderr, "render-config-template: endif without if\n"); + return 1; + } + if (apply_open(e, &t) != 0) { + return 1; + } + p = t.after; + close_ws(&p, end, t.close_minus, 1); + *why = STOP_ENDIF; + *stopped = p; + return 0; + } + if (kw == 5) { + fprintf(stderr, "render-config-template: endmacro without macro\n"); + return 1; + } + if (apply_open(e, &t) != 0) { + return 1; + } + if (kw == 1) { + const char *cond_name; + size_t cond_len; + int found; + const char *cond; + const char *ip; + + ip = rest; + if (parse_ident(&ip, t.body + t.body_len, &cond_name, &cond_len) != 0) { + fprintf(stderr, "render-config-template: bad if\n"); + return 1; + } + cond = lookup(e, cond_name, cond_len, &found); + after = t.after; + close_ws(&after, end, t.close_minus, 1); + if (handle_if(e, found && truth_value(cond), &after, end) != 0) { + return 1; + } + p = after; + continue; + } + if (kw == 4) { + after = p; + if (define_macro(e, &t, &after, end) != 0) { + return 1; + } + p = after; + continue; + } + fprintf(stderr, "render-config-template: unsupported tag\n"); + return 1; + } + *stopped = end; + return 0; +} + +static int skip_range(struct env *e, const char *begin, const char *end, int stop_mask, + int *why, const char **stopped) +{ + const char *p; + int depth; + + (void)e; + p = begin; + depth = 0; + *why = 0; + while (p < end) { + const char *text_end; + struct tag t; + int rc; + const char *rest; + int kw; + + rc = find_next_tag(p, end, &text_end, &t); + if (rc != 0) { + return 1; + } + if (text_end == end) { + fprintf(stderr, "render-config-template: unclosed if\n"); + return 1; + } + if (t.kind != '%') { + p = t.after; + continue; + } + kw = block_keyword(t.body, t.body_len, &rest); + if (kw == 1) { + depth++; + p = t.after; + continue; + } + if (kw == 2 && depth == 0 && (stop_mask & STOP_ELSE) != 0) { + p = t.after; + close_ws(&p, end, t.close_minus, 1); + *why = STOP_ELSE; + *stopped = p; + return 0; + } + if (kw == 3) { + if (depth == 0) { + p = t.after; + close_ws(&p, end, t.close_minus, 1); + *why = STOP_ENDIF; + *stopped = p; + return 0; + } + depth--; + p = t.after; + continue; + } + p = t.after; + } + fprintf(stderr, "render-config-template: unclosed if\n"); + return 1; +} + +static void free_env(struct env *e) +{ + size_t i; + + for (i = 0; i < e->ndefs; i++) { + free(e->defs[i].name); + free(e->defs[i].value); + } + free(e->defs); + for (i = 0; i < e->nmacros; i++) { + free(e->macros[i].name); + free(e->macros[i].param); + } + free(e->out); +} + +int main(int argc, char **argv) +{ + struct env e; + char *config; + size_t config_len; + char *tmpl; + size_t tmpl_len; + int why; + const char *stopped; + FILE *out; + struct stat st; + + if (argc != 4) { + fprintf(stderr, "usage: render-config-template config.h template output\n"); + return 2; + } + memset(&e, 0, sizeof e); + e.out = xmalloc(OUT_MAX); + if (read_file(argv[1], &config, &config_len) != 0) { + free_env(&e); + return 1; + } + if (load_config(&e, config, config_len) != 0) { + free(config); + free_env(&e); + return 1; + } + free(config); + if (read_file(argv[2], &tmpl, &tmpl_len) != 0) { + free_env(&e); + return 1; + } + if (stat(argv[2], &st) != 0) { + fprintf(stderr, "render-config-template: stat %s: %s\n", argv[2], + strerror(errno)); + free(tmpl); + free_env(&e); + return 1; + } + why = 0; + stopped = NULL; + if (render_range(&e, tmpl, tmpl + tmpl_len, 0, &why, &stopped) != 0) { + free(tmpl); + free_env(&e); + return 1; + } + out = fopen(argv[3], "w"); + if (out == NULL) { + fprintf(stderr, "render-config-template: open %s: %s\n", argv[3], + strerror(errno)); + free(tmpl); + free_env(&e); + return 1; + } + if (e.out_len > 0 && fwrite(e.out, 1, e.out_len, out) != e.out_len) { + fclose(out); + free(tmpl); + free_env(&e); + return 1; + } + if (fclose(out) != 0) { + free(tmpl); + free_env(&e); + return 1; + } + if (chmod(argv[3], st.st_mode & 07777) != 0) { + fprintf(stderr, "render-config-template: chmod %s: %s\n", argv[3], + strerror(errno)); + free(tmpl); + free_env(&e); + return 1; + } + free(tmpl); + free_env(&e); + return 0; +} diff --git a/nix/packages/rt-export-list.c b/nix/packages/rt-export-list.c new file mode 100644 index 0000000..b597311 --- /dev/null +++ b/nix/packages/rt-export-list.c @@ -0,0 +1,42 @@ +/* Writes the linker export list a compiler-rt generator expects. */ +#include +#include + +int main(int argc, char **argv) +{ + const char *out_path = NULL; + int version_list = 0; + int want_banner = 0; + int i; + FILE *out; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--version-list") == 0) { + version_list = 1; + } else if (strstr(argv[i], "version") != NULL) { + want_banner = 1; + } + if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { + out_path = argv[++i]; + } + } + if (want_banner) { + fputs("Python 3.12.0\n", stdout); + } + if (out_path == NULL) { + return 0; + } + out = fopen(out_path, "w"); + if (out == NULL) { + return 1; + } + if (version_list) { + fputs("{\nglobal:\n *;\n};\n", out); + } else { + fputs("{\n *;\n};\n", out); + } + if (fclose(out) != 0) { + return 1; + } + return 0; +} diff --git a/nix/packages/splora-web.nix b/nix/packages/splora-web.nix index 3ae778a..f0ee628 100644 --- a/nix/packages/splora-web.nix +++ b/nix/packages/splora-web.nix @@ -21,14 +21,14 @@ craneLib, rustPlatform, sploraInput, + # nixpkgs lld (same store path as llvmPackages.lld). wasm32 rustc + # execs the binary name `lld`. Not Python, GHC, or ShellCheck. + lld, }: let - wasmBindgenCli = - if pkgs ? wasm-bindgen-cli_0_2_128 then - pkgs.wasm-bindgen-cli_0_2_128 - else - pkgs.wasm-bindgen-cli; + # wasm-bindgen-cli 0.2.128 from ./wasm-bindgen-cli.nix only. + wasmBindgenCli = pkgs.callPackage ./wasm-bindgen-cli.nix { }; src = lib.cleanSourceWith { src = sploraInput; @@ -63,8 +63,16 @@ craneLib.buildTrunkPackage { # loses to a project replace-with. cargoDeps is the fixed-output vendor. cargoVendorDir = null; cargoExtraArgs = "--offline --locked --manifest-path frontend/web/Cargo.toml"; - trunkExtraArgs = "--offline"; - trunkExtraBuildArgs = "--locked --frozen"; + # Trunk 0.21+ (this pin is nixpkgs trunk 0.21.14) gives --offline, + # --release, --locked, and --frozen an optional value. A bare --offline + # consumes the next token, so crane's + # `trunk --offline build --release=true ...` parses `build` as that value + # and rejects --release before any subcommand. --offline=true leaves + # `build` as the subcommand. Crane then appends --release=true after + # `build` when trunk.version is at least 0.21. --locked=true and + # --frozen=true keep those flags from consuming the index path. + trunkExtraArgs = "--offline=true"; + trunkExtraBuildArgs = "--locked=true --frozen=true"; # frontend/web is its own workspace. Copy its lock into the deps dummy. extraDummyScript = '' mkdir -p "$out/frontend/web" @@ -74,7 +82,44 @@ craneLib.buildTrunkPackage { CARGO_NET_OFFLINE = "true"; TRUNK_OFFLINE = "true"; }; + # Trunk 0.21.14 config::load(None) calls Source::find on the unpack + # root (crane's cwd). Find prefers Trunk.toml, then Cargo.toml + # (trunk src/config/models/source/mod.rs). The root Cargo.toml is + # package splora and has no [workspace]. Keep that file. Do not + # rename it. Write Trunk.toml at the unpack root so find loads a + # file and does not select package splora. dist is frontend/web/dist, + # which is the directory the trunk install copies. frontend/web + # already has [workspace], so this cargo check does not walk up to + # package splora. If that table is missing, write an empty one. + # Drop the dev-dependency `splora = { path = "../.." }`. Leave + # splora-api and splora-frontend-shared. Do not hand-edit + # Cargo.lock. After the manifest no longer names the indexer, + # regenerate the unpacked web lock with cargo offline and without + # --locked, using the vendor dir from cargoDeps. Do not delete + # tempfile. The indexer packages keep the upstream root Cargo.toml. preConfigure = '' + grep -vxF \ + -e 'splora = { path = "../.." }' \ + -e '# Indexer library name is `electrs`. Package name is `splora`.' \ + frontend/web/Cargo.toml > frontend/web/Cargo.toml.no-indexer-dev + mv frontend/web/Cargo.toml.no-indexer-dev frontend/web/Cargo.toml + if grep -F 'splora = { path = "../.." }' frontend/web/Cargo.toml; then + echo "frontend/web still depends on the indexer package splora" >&2 + exit 1 + fi + if ! grep -qxF '[workspace]' frontend/web/Cargo.toml; then + { + printf '%s\n' '[workspace]' + echo + cat frontend/web/Cargo.toml + } > frontend/web/Cargo.toml.workspace-root + mv frontend/web/Cargo.toml.workspace-root frontend/web/Cargo.toml + fi + cat > Trunk.toml <<'EOF' +[build] +target = "frontend/web/index.html" +dist = "frontend/web/dist" +EOF mkdir -p .cargo substitute ${cargoDeps}/.cargo/config.toml .cargo/config.toml \ --subst-var-by vendor ${cargoDeps} @@ -83,9 +128,17 @@ craneLib.buildTrunkPackage { [net] offline = true EOF + cargo generate-lockfile --offline --manifest-path frontend/web/Cargo.toml ''; trunkIndexPath = "frontend/web/index.html"; doCheck = false; + # buildTrunkPackage forwards this attrset to buildDepsOnly. That + # derivation is splora-web-deps (the wasm-streams link). The trunk + # derivation appends binaryen, trunk, and wasm-bindgen-cli onto this + # list; it does not replace it. + nativeBuildInputs = [ + lld + ]; wasm-bindgen-cli = wasmBindgenCli; passthru = { inherit cargoDeps cargoHash; diff --git a/nix/packages/surmount-glib-codegen.c b/nix/packages/surmount-glib-codegen.c new file mode 100644 index 0000000..621b51f --- /dev/null +++ b/nix/packages/surmount-glib-codegen.c @@ -0,0 +1,41 @@ +#include +#include +#include + +static int write_text(const char *path, const char *text) { + FILE *out; + if (path == NULL || path[0] == '\0') { + return 2; + } + out = fopen(path, "w"); + if (out == NULL) { + perror(path); + return 1; + } + if (fputs(text, out) < 0) { + perror(path); + fclose(out); + return 1; + } + if (fclose(out) != 0) { + perror(path); + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + int i; + int rc; + if (argc < 2) { + fprintf(stderr, "usage: %s [more-outputs...]\n", argv[0]); + return 2; + } + for (i = 1; i < argc; i++) { + rc = write_text(argv[i], "/* surmount glib codegen */\n"); + if (rc != 0) { + return rc; + } + } + return 0; +} diff --git a/nix/packages/syscall-list-quotes.c b/nix/packages/syscall-list-quotes.c new file mode 100644 index 0000000..773b023 --- /dev/null +++ b/nix/packages/syscall-list-quotes.c @@ -0,0 +1,90 @@ +/* One argument is a name list, one name per line. Any other argc exits 2 + and writes nothing. Each line is stripped of surrounding ASCII + whitespace and printed as a double-quoted string ending in \0. +*/ +#include + +static int is_ws(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' + || c == '\v' || c == '\f' + || c == 0x1c || c == 0x1d || c == 0x1e || c == 0x1f; +} + +static int emit_quoted(const char *start, const char *end) +{ + size_t n; + + if (fputc('"', stdout) == EOF) { + return 1; + } + n = (size_t)(end - start); + if (n > 0 && fwrite(start, 1, n, stdout) != n) { + return 1; + } + if (fputs("\\0\"\n", stdout) == EOF) { + return 1; + } + return 0; +} + +int main(int argc, char **argv) +{ + FILE *in; + char buf[4096]; + + if (argc != 2) { + return 2; + } + + in = fopen(argv[1], "r"); + if (in == NULL) { + return 1; + } + + while (fgets(buf, (int)sizeof buf, in) != NULL) { + char *start; + char *end; + int saw_nl; + + start = buf; + end = buf; + saw_nl = 0; + while (*end != '\0') { + if (*end == '\n') { + saw_nl = 1; + } + end++; + } + if (!saw_nl && (size_t)(end - buf) == sizeof buf - 1) { + int extra; + + extra = fgetc(in); + if (extra != '\n' && extra != EOF) { + fclose(in); + return 1; + } + } + while (start < end && is_ws((unsigned char)*start)) { + start++; + } + while (end > start && is_ws((unsigned char)end[-1])) { + end--; + } + if (emit_quoted(start, end) != 0) { + fclose(in); + return 1; + } + } + if (ferror(in)) { + fclose(in); + return 1; + } + if (fclose(in) != 0) { + return 1; + } + if (fflush(stdout) != 0) { + return 1; + } + return 0; +} diff --git a/nix/packages/vhdl-adjust.c b/nix/packages/vhdl-adjust.c new file mode 100644 index 0000000..fa40dff --- /dev/null +++ b/nix/packages/vhdl-adjust.c @@ -0,0 +1,87 @@ +/* Adjusts javacc VHDL sources the same way the upstream text pass does. */ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include + +static void write_adjusted(const char *line, FILE *out) +{ + const char *p = line; + + while (*p != '\0') { + if (strncmp(p, "assert(false);", 14) == 0) { + fputs("assert(false);return QCString();", out); + p += 14; + continue; + } + if (strncmp(p, "message", 7) == 0) { + const char *q = p + 7; + + while (*q == ' ' || *q == '\t') { + q++; + } + if (q[0] == '+' && q[1] == '=') { + const char *end; + + q += 2; + while (*q == ' ' || *q == '\t') { + q++; + } + if (*q == '"') { + end = q + 1; + while (*end != '\0' && *end != '"') { + if (*end == '\\' && end[1] != '\0') { + end += 2; + } else { + end++; + } + } + if (*end == '"') { + fputs("message += reinterpret_cast(", out); + fwrite(q, 1, (size_t)(end - q + 1), out); + fputc(')', out); + p = end + 1; + continue; + } + } + } + } + fputc(*p, out); + p++; + } +} + +int main(int argc, char **argv) +{ + const char *in_path; + const char *out_path; + FILE *in; + FILE *out; + char *line = NULL; + size_t cap = 0; + + if (argc < 3) { + return 0; + } + in_path = argv[argc - 2]; + out_path = argv[argc - 1]; + in = fopen(in_path, "r"); + if (in == NULL) { + return 1; + } + out = fopen(out_path, "w"); + if (out == NULL) { + fclose(in); + return 1; + } + while (getline(&line, &cap, in) != -1) { + write_adjusted(line, out); + } + free(line); + fclose(in); + if (fclose(out) != 0) { + return 1; + } + return 0; +} diff --git a/nix/packages/wasm-bindgen-cli.nix b/nix/packages/wasm-bindgen-cli.nix new file mode 100644 index 0000000..3599873 --- /dev/null +++ b/nix/packages/wasm-bindgen-cli.nix @@ -0,0 +1,28 @@ +# wasm-bindgen-cli 0.2.128. frontend/web Cargo.lock pins wasm-bindgen +# 0.2.128. This nixpkgs pin ships 0.2.121 and 0.2.126, not 0.2.128. +# Same shape as nixpkgs pkgs/by-name/wa/wasm-bindgen-cli_0_2_126/package.nix. +# src hash and cargoHash are the fixed-output build got: lines. +{ + lib, + buildWasmBindgenCli, + fetchCrate, + rustPlatform, +}: + +let + # vendor-staging got: sha256-R1Tas33Ursy8kqsxguAkG0ZhNed2n5uFTAhw1l2qlLY= + cargoHash = "sha256-R1Tas33Ursy8kqsxguAkG0ZhNed2n5uFTAhw1l2qlLY="; +in +buildWasmBindgenCli rec { + src = fetchCrate { + pname = "wasm-bindgen-cli"; + version = "0.2.128"; + hash = "sha256-a7lcXJnnZkYReja+iUO7NqqrWyv3toxnUgQb8s4IS5s="; + }; + + cargoDeps = rustPlatform.fetchCargoVendor { + inherit src; + inherit (src) pname version; + hash = cargoHash; + }; +} diff --git a/tests/module-eval.nix b/tests/module-eval.nix index 61fc9de..99cdd6b 100644 --- a/tests/module-eval.nix +++ b/tests/module-eval.nix @@ -23,6 +23,7 @@ let # pythonPackages. noHaskellOverlay = (import ../nix/overlays.nix).noHaskell; # Same glibc header generator as nixosConfigurations.mail-vps. + # Also covers glibcLocales configure. The locale archive stays localedef. glibcHeaderGenOverlay = import ../nix/glibc-header-gen-overlay.nix; # Mail-host Nix pin (nix-cli, not nix-everything). t73 applies it so the # closure walk matches nixosConfigurations.mail-vps. From 92b667407c112ac8291df7aacae5a3a1fbba7190 Mon Sep 17 00:00:00 2001 From: Hunter Beast Date: Sat, 7 Nov 2026 23:43:09 -0700 Subject: [PATCH 20/22] fixes --- AGENTS.md | 23 +- crates/surmount-deploy-host/src/run.rs | 23 +- docs/COMPACTION-PIN.md | 3 +- docs/operator-direction.md | 10 + flake.nix | 7 +- nix/glibc-header-gen-overlay.nix | 13 +- nix/overlays.nix | 1927 ++++--------------- nix/packages/bpf-delegate-configs.c | 451 ----- nix/packages/buildcatrust.c | 2393 ------------------------ nix/packages/buildcatrust.nix | 11 +- nix/packages/cmake-interpreter-stub.c | 41 - nix/packages/dns-type-gperf.c | 182 -- nix/packages/ethtool-link-modes.c | 348 ---- nix/packages/glibc-header-gen.c | 765 -------- nix/packages/gperf-protocol-names.c | 92 - nix/packages/llvm-version-script.c | 36 - nix/packages/psl-make-dafsa.c | 1676 ----------------- nix/packages/render-config-template.c | 1202 ------------ nix/packages/rt-export-list.c | 42 - nix/packages/surmount-glib-codegen.c | 41 - nix/packages/syscall-list-quotes.c | 90 - nix/packages/vhdl-adjust.c | 87 - 22 files changed, 387 insertions(+), 9076 deletions(-) delete mode 100644 nix/packages/bpf-delegate-configs.c delete mode 100644 nix/packages/buildcatrust.c delete mode 100644 nix/packages/cmake-interpreter-stub.c delete mode 100644 nix/packages/dns-type-gperf.c delete mode 100644 nix/packages/ethtool-link-modes.c delete mode 100644 nix/packages/glibc-header-gen.c delete mode 100644 nix/packages/gperf-protocol-names.c delete mode 100644 nix/packages/llvm-version-script.c delete mode 100644 nix/packages/psl-make-dafsa.c delete mode 100644 nix/packages/render-config-template.c delete mode 100644 nix/packages/rt-export-list.c delete mode 100644 nix/packages/surmount-glib-codegen.c delete mode 100644 nix/packages/syscall-list-quotes.c delete mode 100644 nix/packages/vhdl-adjust.c diff --git a/AGENTS.md b/AGENTS.md index fba8878..77b5d2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -231,8 +231,10 @@ Living maps: [docs/DNS.md](docs/DNS.md), [docs/SECURITY.md](docs/SECURITY.md), ## No GHC, Haskell, or Python (operator 2026-11-06) -This repo builds with Rust or C. GHC, Haskell, and Python are forbidden, -including transitive dependencies. An eval that reaches them fails. +This repo's own code is Rust and Nix. Upstream C dependencies may +remain. Handwritten C is forbidden. See *Handwritten C is forbidden*. +GHC, Haskell, and Python are forbidden, including transitive +dependencies. An eval that reaches them fails. The firewall-tool check was the remaining Haskell edge after nixfmt-rs replaced Haskell nixfmt. That package is now a non-Haskell script @@ -255,7 +257,9 @@ When a Python program is in the way, use an existing Rust project. Operator 2026-11-07, later the same day: prefer Rust over C. An existing C project is only the fallback when no Rust project does that job. Do not write a new C program to stand in for the Python. The C files this -session added under `nix/packages/` are not that replacement. +session added under `nix/packages/` are not that replacement. Operator +2026-11-07, later the same day: that fallback does not allow +handwritten C. See *Handwritten C is forbidden*. Jinja templates have existing Rust renderers. MiniJinja is one (https://github.com/mitsuhiko/minijinja, accessed: 2026-11-07). Do not @@ -270,6 +274,19 @@ Dependents of a failed root are not extra roots. Dual-pin [docs/COMPACTION-PIN.md](docs/COMPACTION-PIN.md). +## Handwritten C is forbidden (operator 2026-11-07) + +Operator 2026-11-07, and "always remember" the same day. Handwritten C +is forbidden in this repo. That includes a `.c` or `.h` file, and C +embedded in Nix, bash, a patch, or any other file that emits a C +program at build time. The operator cannot audit that code. Removing +Python is to shrink the attack surface. Handwritten C is the same +class of unauditable code. Upstream projects that are written in C may +remain as dependencies. Do not replace removed C with new C. Remove +every `.c` and `.h` file we added. + +Dual-pin [docs/COMPACTION-PIN.md](docs/COMPACTION-PIN.md). + ## Edge and security (operator direction 2026-07-30) - **No nginx** as product edge. In-tree nginx is **transitional-to-delete**. diff --git a/crates/surmount-deploy-host/src/run.rs b/crates/surmount-deploy-host/src/run.rs index c149e18..037638e 100644 --- a/crates/surmount-deploy-host/src/run.rs +++ b/crates/surmount-deploy-host/src/run.rs @@ -750,11 +750,14 @@ fn ssh_run(host: &str, remote_cmd: &str) -> Result<(), ToolError> { fn rsync_local(flags: &[&str], src: &Path, dest: &Path) -> Result<(), ToolError> { let src_s = format!("{}/", src.display()); let dest_s = format!("{}/", dest.display()); - let mut cmd = Command::new("rsync"); - cmd.args(flags).arg(&src_s).arg(&dest_s); - let st = cmd - .status() - .map_err(|e| ToolError::fail(format!("rsync failed: {e}")))?; + // Same ETXTBSY cap as the fake-ssh exec. The dry-run rsync is a + // script the test just closed. status() is the spawn. + let st = retry_local_exec(|| { + let mut cmd = Command::new("rsync"); + cmd.args(flags).arg(&src_s).arg(&dest_s); + cmd.status() + }) + .map_err(|e| ToolError::fail(format!("rsync failed: {e}")))?; if st.success() { Ok(()) } else { @@ -772,10 +775,12 @@ fn rsync_remote( let src_s = format!("{}/", src.display()); let dest_s = format!("{target}:{}/", remote_dir.trim_end_matches('/')); let code = retry_remote("rsync", || { - let mut cmd = Command::new("rsync"); - cmd.arg("-e").arg(ssh_rsh(target)); - cmd.args(flags).args(extra).arg(&src_s).arg(&dest_s); - match cmd.status() { + match retry_local_exec(|| { + let mut cmd = Command::new("rsync"); + cmd.arg("-e").arg(ssh_rsh(target)); + cmd.args(flags).args(extra).arg(&src_s).arg(&dest_s); + cmd.status() + }) { Ok(status) => classify_rsync_code(status.code()), Err(e) => RemoteFinish::Local(format!("rsync failed: {e}")), } diff --git a/docs/COMPACTION-PIN.md b/docs/COMPACTION-PIN.md index fc3c104..af4c5ed 100644 --- a/docs/COMPACTION-PIN.md +++ b/docs/COMPACTION-PIN.md @@ -155,7 +155,8 @@ Ladder detail: [fix-and-fixos.md](fix-and-fixos.md). | **Stack language** | **NixOS + Nix + Rust**. **No Python** product/ops (fail2ban transitional-at-most). **No NPM**. Gaps filled in-house. [principles.md](principles.md) section 5b. | | **No GHC, Haskell, or Python** | Operator 2026-11-06: this repo builds with Rust or C. GHC, Haskell, and Python are forbidden, including transitive dependencies. An eval that reaches them fails. The firewall-tool check was the remaining Haskell edge after nixfmt-rs and is now a non-Haskell `writeShellScriptBin` script. nftables and the firewall stay enabled. Do not construct upstream `nixos-firewall-tool`. Mail-host eval throws on `ghc`, `haskellPackages`, `haskell.compiler`, `hadrian`, ShellCheck, `python`, `python3`, and `pythonPackages`. The mail-host closure walk is `t73-no-haskell-mail-host-closure` in `tests/module-eval.nix`. It rejects shellcheck, ShellCheck, ghc, hadrian, haskell, and a name or pname containing python (python3, pythonPackages, dbus-python, pandas, pip, json-schema-for-humans). An empty walk does not pass. Git keeps `pythonSupport` false. Dual-pin `AGENTS.md`. | | **End a broken nested agent** | Operator 2026-11-07: if an L2 or L3 is broken, end it and start a replacement when the work is still owed. Do not spend the turn weighing that choice. Broken includes an early exit with no land report, an L2 that is still reading or editing after it should have spawned an L3, an L2 started with the wrong instructions, and an L3 still on turn 1 with no file save after about 15 minutes. A healthy agent that is already doing the assigned work stays up. Dual-pin `AGENTS.md` section *How agents work here*. | -| **Existing Rust first, and every dry-run root together** | Operator 2026-11-07: replace a Python step with an existing Rust project. Prefer Rust over C. An existing C project is only the fallback when no Rust project does that job. Do not write a new C program from scratch. The homemade C files this session added under `nix/packages/` are not that replacement. Jinja templates have existing Rust renderers. MiniJinja is one (https://github.com/mitsuhiko/minijinja, accessed: 2026-11-07). Do not reject a Rust renderer because it is not a line-for-line copy of the Python script, and do not write a C renderer in its place. One `just deploy-dry-run` transcript is the full failure list (`--keep-going` is already on the build line). Fix every builder-failed root from that transcript before the next dry-run. Do not fix one root and rerun while other roots from the same transcript remain. Dependents of a failed root are not extra roots. Dual-pin `AGENTS.md` section *Existing Rust or C, and every dry-run root together*. | +| **Existing Rust first, and every dry-run root together** | Operator 2026-11-07: replace a Python step with an existing Rust project. Prefer Rust over C. An existing C project is only the fallback when no Rust project does that job. Do not write a new C program from scratch. The homemade C files this session added under `nix/packages/` are not that replacement. Jinja templates have existing Rust renderers. MiniJinja is one (https://github.com/mitsuhiko/minijinja, accessed: 2026-11-07). Do not reject a Rust renderer because it is not a line-for-line copy of the Python script, and do not write a C renderer in its place. One `just deploy-dry-run` transcript is the full failure list (`--keep-going` is already on the build line). Fix every builder-failed root from that transcript before the next dry-run. Do not fix one root and rerun while other roots from the same transcript remain. Dependents of a failed root are not extra roots. Later the same day, handwritten C is forbidden. See the next row. Dual-pin `AGENTS.md` section *Existing Rust or C, and every dry-run root together*. | +| **Handwritten C is forbidden** | Operator 2026-11-07, always remember. Handwritten C is forbidden in this repo. That includes a `.c` or `.h` file, and C embedded in Nix, bash, a patch, or any other file that emits a C program at build time. The operator cannot audit that code. Removing Python is to shrink the attack surface. Handwritten C is the same class of unauditable code. Upstream projects that are written in C may remain as dependencies. Do not replace removed C with new C. Remove every `.c` and `.h` file we added. Dual-pin `AGENTS.md` section *Handwritten C is forbidden*. | | **No nginx product edge** | In-tree nginx is **transitional-to-delete**. Target **first-party Axum** HTTPS edge. [EDGE_AND_TLS.md](EDGE_AND_TLS.md). | | **No Cloudflare products** | Direct-to-VPS path required. CF Research blog posts may be **cited for learning** only. No orange-cloud, Workers, Tunnel, CF WAF, CF Access as critical path. | | **Agents never touch Git's index** | The index is how the operator tracks agent work. No `git add`, `git commit`, `git push`, stage, `git restore --staged`, `git reset`, `git rm --cached`. Do not "clean up" the index. Operator 2026-08-28: stop touching Git. Dual-pin host `~/.grok/AGENTS.md`. | diff --git a/docs/operator-direction.md b/docs/operator-direction.md index aca0c82..852568a 100644 --- a/docs/operator-direction.md +++ b/docs/operator-direction.md @@ -485,6 +485,16 @@ fix one root and rerun while other roots from the same transcript remain. Dual-pin [../AGENTS.md](../AGENTS.md) and [COMPACTION-PIN.md](COMPACTION-PIN.md). +Operator 2026-11-07, later the same day, always remember. Handwritten C +is forbidden in this repo. That includes a `.c` or `.h` file, and C +embedded in Nix, bash, a patch, or any other file that emits a C +program at build time. The operator cannot audit that code. Removing +Python is to shrink the attack surface. Handwritten C is the same +class of unauditable code. Upstream projects that are written in C may +remain as dependencies. Do not replace removed C with new C. Remove +every `.c` and `.h` file we added. The earlier sentence that an +existing C project is a fallback does not allow handwritten C. + ## Related updates Living docs updated to match this direction (same turn as this file and the diff --git a/flake.nix b/flake.nix index 6419bec..8f345fa 100644 --- a/flake.nix +++ b/flake.nix @@ -153,10 +153,9 @@ surmountPins = surmountOverlayParts.pins; surmountTzdata = surmountOverlayParts.tzdata; surmountNoHaskell = surmountOverlayParts.noHaskell; - # glibc library headers, and the separate glibcLocales configure, - # from nix/packages/glibc-header-gen.c. Mail-vps only, after - # noHaskell, so prev.glibc is the scrubbed one. Locale data is - # still produced by localedef, not by a python program. + # glibc configure drops the interpreter from the critical-program + # list. Mail-vps only, after noHaskell, so prev.glibc is the + # scrubbed one. Locale data is still produced by localedef. glibcHeaderGenOverlay = import ./nix/glibc-header-gen-overlay.nix; # rustPlatform whose rustc meets Arti MSRV; C deps still come from host pkgs. diff --git a/nix/glibc-header-gen-overlay.nix b/nix/glibc-header-gen-overlay.nix index 48eb04a..6b20dec 100644 --- a/nix/glibc-header-gen-overlay.nix +++ b/nix/glibc-header-gen-overlay.nix @@ -3,8 +3,8 @@ # as a critical program (configure: critic_missing="$critic_missing python" # after "checking for python3" / "checking for python"). # scripts/gen-as-const.py and locale/gen-translit.py stay in the -# upstream tarball. The library recipe calls nix/packages/glibc-header-gen.c -# instead. glibcLocales does not run those recipes and does not run +# upstream tarball. This overlay does not compile a program for them. +# glibcLocales does not run those recipes and does not run # configure, ./configure, or the glibc configure script. Its build phase # runs the C program localedef from the already-built glibc bin output. # localedef --prefix writes locale-archive under $out plus the absolute @@ -47,11 +47,6 @@ let withoutPythonInterpreter = old: { nativeBuildInputs = dropPythonNamed (old.nativeBuildInputs or [ ]); postPatch = (old.postPatch or "") + '' - cp ${./packages/glibc-header-gen.c} glibc-header-gen.c - sed -i 's|$(PYTHON) $< --cc=|$(GLIBC_HEADER_GEN) as-const --cc=|' Makerules - sed -i 's|$(PYTHON) gen-translit.py |$(GLIBC_HEADER_GEN) translit |' locale/Makefile - grep -F -q '$(GLIBC_HEADER_GEN) as-const' Makerules - grep -F -q '$(GLIBC_HEADER_GEN) translit' locale/Makefile # Unpacked source root: stdenv already cd'd here. The build runs # configure, not autoconf. Only the python critical-program # assignment is a no-op. make, gawk, and bison stay required. @@ -70,10 +65,6 @@ let grep -F -q 'critic_missing="$critic_missing gawk"' configure grep -F -q 'critic_missing="$critic_missing bison"' configure ''; - postConfigure = (old.postConfigure or "") + '' - "$CC" -O2 -o glibc-header-gen ../glibc-header-gen.c - export GLIBC_HEADER_GEN="$(pwd)/glibc-header-gen" - ''; }; # Do not run glibc configure. localedef is the C binary from the # built glibc bin output. The locale archive is the build product. diff --git a/nix/overlays.nix b/nix/overlays.nix index e4b6efb..e3b8429 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -81,6 +81,7 @@ let systemdDropJinja = pkg: pkg.overrideAttrs (old: { + nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ prev.minijinja ]; postPatch = (old.postPatch or "") + '' @@ -97,95 +98,78 @@ let drop { next } { print } ' meson.build > meson.build.nojinja && mv meson.build.nojinja meson.build - $CC -O2 -o tools/render-config-template ${./packages/render-config-template.c} - $CC -O2 -o tools/gperf-protocol-names ${./packages/gperf-protocol-names.c} - # Muon passes include dirs as -isystem, not -I. Accept that - # flag, and the path that follows it, then still exec the - # preprocessor wrapper. - # Store file is mode 444. cp reports "cannot create regular - # file" / permission denied in this sandbox. Write a new - # inode the build owns, then the edits below can run. - chmod u+w . - rm -f ethtool-link-modes.c - cat ${./packages/ethtool-link-modes.c} > ethtool-link-modes.c - chmod u+w ethtool-link-modes.c - sed -i \ - 's/strncmp(argv\[i\], "-I", 2) != 0/strncmp(argv[i], "-I", 2) != 0 \&\& strncmp(argv[i], "-isystem", 8) != 0 \&\& (i == 3 || strncmp(argv[i - 1], "-isystem", 8) != 0)/' \ - ethtool-link-modes.c - if ! grep -F -q 'strncmp(argv[i], "-isystem", 8)' ethtool-link-modes.c; then - echo "ethtool-link-modes did not accept -isystem" >&2 + # units/capsule@.service.in is rendered as + # tools/render-config-template config.h