From 208b8d4444db79f6764f71f2b56e678faccea036 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 9 Jul 2026 09:40:07 +0100 Subject: [PATCH] fix(install): swap=0 and unattended LUKS fail-closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass bare swapSize "0" when the user wants no swap (was "0G", which still created @swap). Accept "0"/"0G" in disko-config. Unattended installs require NOMARCHY_LUKS_PASSPHRASE or explicit NOMARCHY_NO_LUKS=1 instead of silently installing cleartext. Verified: bash -n; nix-instantiate subvolume sets for 0/0G/16G. VISION § v1.0 install golden path (workflow test iteration). --- agent/BACKLOG.md | 25 +++-------------------- agent/JOURNAL.md | 7 +++++++ pkgs/nomarchy-install/disko-config.nix | 6 ++++-- pkgs/nomarchy-install/nomarchy-install.sh | 23 +++++++++++++++++++-- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 7debed3..1925679 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -157,16 +157,6 @@ first-boot tips · human rebuild errors · neon-glass quarantine. ### Bugs / broken behavior (codebase exam 2026-07-09) -- **Installer: swap "none" still creates a 0G swap subvolume** - `nomarchy-install.sh` always passes `--argstr swapSize "${SWAP_GB}G"`. - When the user chooses 0 (no swap), disko gets `"0G"`, but - `disko-config.nix` only treats the exact string `"0"` as no-swap — - so it still creates `@swap` with size `0G`. Resume wiring correctly - skips (`SWAP_GB != "0"`), so layout and config disagree. Fix: pass - bare `"0"` (or teach disko to accept `"0G"`). Cheap pure check / - unit test for the swapSize contract would lock this. Cost: small - installer + disko-config change; high install-path impact. - - **Waybar doctor indicator never shows an icon** `modules/home/waybar.nix` `nomarchy-doctor-status` prints `"text":""` on failure. Waybar custom modules with empty `text` @@ -219,14 +209,6 @@ first-boot tips · human rebuild errors · neon-glass quarantine. battery-notify. Cost: small multi-file; `[blocked:hw]` to confirm on non-BAT* hardware. -- **Unattended install silently disables LUKS if passphrase unset** - Interactive path defaults encrypt-on; unattended without - `NOMARCHY_LUKS_PASSPHRASE` installs **unencrypted** with only a log - line — no fail-closed. `test-install.sh` always sets a passphrase, - so the regression path is untested. Options: require the env var - when unattended, or an explicit `NOMARCHY_NO_LUKS=1`. Cost: small - policy change in install script + test matrix note. - ### Docs / option-surface drift (codebase exam 2026-07-09) - **`option-docs` would fail: undocumented `nomarchy.hardware.i2c.*`** @@ -655,10 +637,9 @@ installer questionnaire.* #### Bugs (stability contracts — fix, don’t optionalize) -- **Installer swap=0 / unattended LUKS fail-open** — already under - *Bugs / broken behavior* above (`"0G"` vs `"0"`; missing - `NOMARCHY_LUKS_PASSPHRASE` → cleartext). Same audit reconfirmed both - as install-path P0. +- **Installer swap=0 + unattended LUKS fail-closed** — ✓ shipped + (workflow test run 2026-07-09): pass bare `"0"`; disko accepts + `"0"`/`"0G"`; unattended needs passphrase or `NOMARCHY_NO_LUKS=1`. - **Offline mode must not silently fall back to network lock** When the network probe fails (or install is offline), diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index bf7adc1..a0ac884 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -17,6 +17,13 @@ Template: --- +## 2026-07-09 — Install contracts: swap=0 + unattended LUKS (workflow test run) +- **Task:** VISION § v1.0 install golden path — PROPOSED install P0s (NEXT head blocked: item 14 wait-cron, 20 `[human]`, 33 V3 visual, 41 hw). Test of VISION→BACKLOG→verify→commit loop. +- **Did:** (1) `nomarchy-install` passes `swapSize=0` when swap disabled (was always `"${SWAP_GB}G"` → `"0G"` still created `@swap`). (2) `disko-config.nix` treats `"0"` and `"0G"` as no-swap. (3) Unattended LUKS fail-closed: require `NOMARCHY_LUKS_PASSPHRASE` or `NOMARCHY_NO_LUKS=1`. +- **Verified:** V0 — `bash -n`; pure `nix-instantiate` of disko subvols for `0`/`0G`/`16G` (no `@swap` only on zero). Not V2 install matrix this run. +- **Pending:** optional pure `checks.*` for swapSize; unattended no-swap/no-LUKS in `test-install.sh` when convenient. +- **Next suggestion:** promote another VISION § A slice (firmware menu / doctor) or boreal default to NEXT. + ## 2026-07-08 — Theme UI Review: Typography Polish & Icon Call (iteration #87) - **Task:** NEXT item 28 (Theme UI review). - **Did:** diff --git a/pkgs/nomarchy-install/disko-config.nix b/pkgs/nomarchy-install/disko-config.nix index c78e9b0..465d2be 100644 --- a/pkgs/nomarchy-install/disko-config.nix +++ b/pkgs/nomarchy-install/disko-config.nix @@ -15,12 +15,14 @@ # proved fragile twice before. { mainDrive , withLuks ? true -, swapSize ? "0" # "0" = no swap; otherwise e.g. "16G" (sized for hibernation) +, swapSize ? "0" # "0" or "0G" = no swap; otherwise e.g. "16G" (hibernation-sized) , ... }: let btrfsMountOptions = [ "compress=zstd" "noatime" ]; + # Installer historically always appended "G"; accept both "0" and "0G". + noSwap = swapSize == "0" || swapSize == "0G"; rootBtrfs = { type = "btrfs"; @@ -32,7 +34,7 @@ let "@log" = { mountpoint = "/var/log"; mountOptions = btrfsMountOptions; }; # snapper timeline snapshots (nomarchy.system.snapper.enable) "@snapshots" = { mountpoint = "/.snapshots"; mountOptions = btrfsMountOptions; }; - } // (if swapSize == "0" then { } else { + } // (if noSwap then { } else { # Hibernation-ready swapfile on its own subvolume; disko's # mkswapfile handles the BTRFS NOCOW requirements. "@swap" = { diff --git a/pkgs/nomarchy-install/nomarchy-install.sh b/pkgs/nomarchy-install/nomarchy-install.sh index f5d9254..db77f6d 100644 --- a/pkgs/nomarchy-install/nomarchy-install.sh +++ b/pkgs/nomarchy-install/nomarchy-install.sh @@ -18,8 +18,11 @@ # [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \ # [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \ # [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \ +# [NOMARCHY_LUKS_PASSPHRASE=... | NOMARCHY_NO_LUKS=1] \ # [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff] # nomarchy-install +# Unattended encryption is fail-closed: set a passphrase, or explicit +# NOMARCHY_NO_LUKS=1 — never silently install cleartext. set -euo pipefail @@ -126,7 +129,15 @@ section "Disk encryption" # logs you straight into the desktop (the passphrase already gates access). LUKS_PASSPHRASE="" if [[ "$UNATTENDED" == "1" ]]; then - LUKS_PASSPHRASE="${NOMARCHY_LUKS_PASSPHRASE:-}" + # Fail-closed: unattended without a passphrase used to install + # cleartext (easy CI footgun). Require an explicit opt-out. + if [[ "${NOMARCHY_NO_LUKS:-}" == "1" ]]; then + LUKS_PASSPHRASE="" + elif [[ -n "${NOMARCHY_LUKS_PASSPHRASE:-}" ]]; then + LUKS_PASSPHRASE="$NOMARCHY_LUKS_PASSPHRASE" + else + fail "Unattended install needs NOMARCHY_LUKS_PASSPHRASE or NOMARCHY_NO_LUKS=1" + fi elif gum confirm --default=yes "Encrypt the disk with LUKS? (default — also enables passwordless desktop login)"; then while true; do p1=$(gum input --password --placeholder "LUKS passphrase (min 8 chars)") @@ -317,11 +328,19 @@ if [[ $WITH_LUKS == true ]]; then unset LUKS_PASSPHRASE fi +# disko-config treats exact "0" as no-swap; "${SWAP_GB}G" would pass "0G" +# and still create a useless @swap subvolume (layout vs resume disagreed). +if [[ "$SWAP_GB" == "0" ]]; then + DISKO_SWAP_SIZE="0" +else + DISKO_SWAP_SIZE="${SWAP_GB}G" +fi + disko_log=$(mktemp --suffix=.disko.log) if ! disko --mode destroy,format,mount --yes-wipe-all-disks \ --argstr mainDrive "$TARGET_DISK" \ --arg withLuks "$WITH_LUKS" \ - --argstr swapSize "${SWAP_GB}G" \ + --argstr swapSize "$DISKO_SWAP_SIZE" \ "$SHARE/disko-config.nix" >"$disko_log" 2>&1; then tail -n 30 "$disko_log" fail "disko failed — full log: $disko_log"