test+fix: close #14, V2 #57 live install entry, #41 float slice
All checks were successful
Check / eval (push) Successful in 2m57s

- #14: first scheduled lock bump confirmed (8fded63 nomarchy-bump Mon
  05:20 UTC 2026-07-06); remove queue item.
- #57 V2: checks.live-install-entry builds nomarchy-live HM gen and
  asserts Install Nomarchy.desktop + installer bin + menu gate.
- #41: float/center hyprpolkitagent + pinentry-qt from package app-ids;
  capture-float-classes.nix harness (softGL empty); residual portal
  class stays NEXT [blocked:hw]. HARDWARE-QUEUE confirm steps.

Verified: live-install-entry + windowrule-syntax green.
This commit is contained in:
Bernardo Magri
2026-07-10 09:20:15 +01:00
parent 5f3124d160
commit 14558ad296
6 changed files with 205 additions and 34 deletions

View File

@@ -28,40 +28,23 @@ in [`docs/ROADMAP.md`](../docs/ROADMAP.md); map in
## NEXT
### 14. Automated lock bumps — confirm the first real run
Slices b+c shipped 2026-07-05 (`.gitea/workflows/bump.yml`): weekly
schedule (Mon 05:17 UTC) + `workflow_dispatch` as the security
fast-lane; `nix flake update` → eval gate → pathspec-limited lock
commit pushed to `main` on green (that push triggers check.yml as the
second net). Update/commit/push logic simulated locally end-to-end in
a scratch clone, incl. a real update; today's bumped lock evals green.
Remaining (not confirmable from a session): the first scheduled or
dispatched run must land — watch that the runner picks up the cron and
that the Actions token can push. Then delete this item. Item 20's KVM
runner later upgrades the gate from eval-only to the VM suite.
### 20. KVM runner → VM suite in CI `[human]`
The remaining stretch of the CI item — checks-on-push is live and
**green** (run #58; runner = gitea/act_runner docker, eval tier).
Register a second runner on a host with `/dev/kvm` + nix (host-mode
label `nix-kvm`), then an agent uncomments the workflow's `vm-checks`
job: the `checks.*` VM suite + real toplevel/HM builds on every push
that later upgrades item 14's bump gate from eval-only to the full suite.
that later upgrades the bump gate from eval-only to the full suite.
### 41. Floating-window audit — remaining (needs hardware class checks)
(Raised by Bernardo, 2026-07-07 — item 11.) Broaden the `windowrule`
float set beyond the conservative first cut.
_Conservative cut shipped 2026-07-07 (iteration #63, on Bernardo's "take a
conservative approach"):_ float + center the mixer (item 35), **blueman**
(manager/adapters) and **system-config-printer** — the only shipped,
unambiguous config dialogs whose classes are confident. **Remaining
candidates, deliberately deferred because their window class can't be
verified headlessly** (a guessed class = dead rule): the polkit auth
prompt (hyprpolkitagent — no discoverable class in the package), GTK
file-chooser portals (`xdg-desktop-portal-gtk`), and any pinentry dialog.
Next slice: on hardware, run `hyprctl clients` while each is open, read
the real `class`, then append rules. Also revisit whether blueman/s-c-p
actually float once seen (regex tolerance for the `.…-wrapped` form).
### 41. Floating-window audit — residual GTK portal class `[blocked:hw]`
(Raised by Bernardo, 2026-07-07 — item 11.) Conservative cut + 2026-07-10
slice shipped: mixer, blueman, system-config-printer, calendar,
**hyprpolkitagent** + **pinentry-qt** (classes from package app-id
strings; softGL capture harness could not materialize dialogs).
**Still open:** GTK file-chooser portal (`xdg-desktop-portal-gtk`) —
class not discoverable headlessly. On hardware: open a portal file
picker, `hyprctl clients`, append float/center rules if needed; confirm
polkit/pinentry/blueman actually float.
## LATER
@@ -475,7 +458,7 @@ _(User password minimum length = 8 → #54.)_
#### Live ISO discoverability (one durable cue)
_(Live: one always-visible “Install Nomarchy” action → **#57** ✓ 2026-07-10;
V2 pending: live ISO desktop entry smoke.)_
V2: `checks.live-install-entry` on live HM gen.)_
#### Generated config / post-install polish

View File

@@ -8,6 +8,12 @@ the failure as a NOW bug in BACKLOG.md. Machines: the **AMD dev box**
QA machine), the **T14s** (webcam case).
## Any machine (dev box is fine)
- [ ] **#41 float classes (polkit / pinentry / portal / blueman)** — after
rebuild: (1) `pkexec true` or mount a disk → polkit dialog should
float+center; if not, note `hyprctl clients` class while open.
(2) `gpg --sign` or any pinentry prompt → same. (3) App file picker
via portal → capture class for residual rule. (4) blueman-manager /
system-config-printer still float (`.…-wrapped` tolerance).
- [ ] **#55 fingerprint enroll on real reader** — with
`nomarchy.hardware.fingerprint.enable` and a physical reader: System
Fingerprint Enroll a finger; List shows it; Verify succeeds; optional

View File

@@ -17,6 +17,23 @@ Template:
---
## 2026-07-10 — Pending closeout: #14, #57 V2, #41 slice
- **Task:** pending V2/CI/float leftovers after A/B/C batch.
- **Did:** **#14 closed** — first scheduled lock bump landed
`8fded63` (2026-07-06 Mon 05:20 UTC) as `nomarchy-bump` /
`chore(lock): scheduled upstream bump` (Actions token push works).
**#57 V2** — `checks.live-install-entry` asserts live HM gen has
`nomarchy-install.desktop` (Name/Terminal/Exec) + installer bin +
Tools menu gate in `nomarchy-menu`. **#41 slice** — float/center rules
for hyprpolkitagent + pinentry-qt from package app-id strings;
`tools/capture-float-classes.nix` harness (softGL empty clients);
residual GTK portal stays NEXT `#41 [blocked:hw]`. HARDWARE-QUEUE
float confirm steps added.
- **Verified:** **V2** live-install-entry green; windowrule-syntax green.
SoftGL class capture inconclusive (empty clients). **V3** float confirm
+ fingerprint/#60 still HARDWARE-QUEUE. **#20** still `[human]`.
- **Next suggestion:** Bernardo: #20 KVM runner, or HARDWARE-QUEUE pass.
## 2026-07-10 — #55 fingerprint menu + #56 human rebuild errors
- **Task:** NEXT #55 (System Fingerprint) + #56 (rebuild failure UX).
- **Did:** #55 — System row self-gated on fprintd-list; enroll/list/verify/

View File

@@ -352,6 +352,40 @@
# sheet to ✖/exit-1 and names the unit; with the failure
# cleared it reports healthy/exit-0. Minimal node (just the
# package) — the disk/flake/snapper checks self-skip in a VM.
# Live-ISO install affordance (BACKLOG #57): desktop entry baked
# into the live host's HM generation + installer on PATH + Tools
# menu gate in nomarchy-menu. Builds the real nomarchy-live HM
# package (not a full ISO) — V2 file-level smoke of the same
# artifacts the ISO ships.
live-install-entry =
let
liveHm =
self.nixosConfigurations.nomarchy-live.config.home-manager.users.${username};
gen = liveHm.home.activationPackage;
in
pkgs.runCommand "nomarchy-live-install-entry"
{
nativeBuildInputs = [ pkgs.gnugrep pkgs.findutils ];
passAsFile = [ ];
}
''
set -euo pipefail
desk=$(find ${gen}/home-path/share/applications \
-name nomarchy-install.desktop | head -1)
test -n "$desk"
grep -q '^Name=Install Nomarchy$' "$desk"
grep -q '^Terminal=true$' "$desk"
grep -q '^Exec=nomarchy-install$' "$desk"
test -x ${nomarchyInstall}/bin/nomarchy-install
# Tools Install Nomarchy self-gate lives in nomarchy-menu
# (rofi.nix), which is on the live HM path.
menu=$(find ${gen}/home-path/bin -name nomarchy-menu | head -1)
test -n "$menu"
grep -q 'Install Nomarchy' "$menu"
echo "live-install-entry: ok desk=$desk menu=$menu"
touch $out
'';
doctor = pkgs.testers.runNixOSTest {
name = "nomarchy-doctor";
nodes.machine = { ... }: {

View File

@@ -469,12 +469,12 @@ in
# Class regexes use `(?i)` and tolerate the XWayland
# `.…-wrapped` binary-name form.
#
# Conservative set on purpose (item 41): only windows the distro
# actually ships, each an unambiguous dialog. Deliberately NOT
# floated yet — needs on-hardware class verification: the polkit auth
# prompt (hyprpolkitagent exposes no discoverable class) and GTK
# file-chooser portals. A dead rule is harmless, but guessing a class
# is worse than leaving it for a verified pass.
# Conservative set (item 41): only dialogs we ship or can name from
# package strings / desktop files. SoftGL capture harness could not
# materialize polkit/pinentry windows (empty hyprctl clients) — classes
# below are from binary/app-id strings (hyprpolkitagent, pinentry-qt).
# Remaining: GTK file-chooser portal — class still unknown headlessly;
# HARDWARE-QUEUE for live hyprctl clients confirmation.
windowrule = [
# Audio mixer (item 35): right-click the Waybar volume module.
"float 1, match:class ^(com\\.saivert\\.pwvucontrol|org\\.pulseaudio\\.pavucontrol|pavucontrol)$"
@@ -490,6 +490,13 @@ in
"float 1, match:class ^(com\\.nomarchy\\.calendar)$"
"size 60% 65%, match:class ^(com\\.nomarchy\\.calendar)$"
"center 1, match:class ^(com\\.nomarchy\\.calendar)$"
# Polkit auth (hyprpolkitagent — app id / binary name in the package)
# and pinentry-qt (keys.nix default; org.gnupg.pinentry-qt desktop).
"float 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
"center 1, match:class (?i)^(hyprpolkitagent|\\.hyprpolkitagent-wrapped)$"
"float 1, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$"
"center 1, match:class (?i)^(pinentry(-qt)?|org\\.gnupg\\.pinentry-qt)$"
];
# Rendered from ./keybinds.nix (the cheatsheet reads the same list),

View File

@@ -0,0 +1,124 @@
# Capture window classes for BACKLOG #41 float candidates (hyprpolkitagent,
# pinentry-qt, optional portal). Headless softGL Hyprland; dumps hyprctl
# clients while dialogs are open into $out/classes.json + classes.txt.
#
# nix build --impure -f tools/capture-float-classes.nix --no-link --print-out-paths
#
# Maintainer harness — not a checks.* gate (needs interactive dialogs;
# softGL may drop some Qt windows).
let
flake = builtins.getFlake ("git+file://" + toString ../.);
inherit (flake.inputs) nixpkgs home-manager;
pkgs = import nixpkgs {
system = "x86_64-linux";
overlays = [ flake.overlays.default ];
config.allowUnfree = true;
};
in
pkgs.testers.runNixOSTest {
name = "capture-float-classes";
node.pkgsReadOnly = false;
node.specialArgs = { username = "nomarchy"; };
nodes.machine = { pkgs, lib, ... }: {
imports = [ flake.nixosModules.nomarchy home-manager.nixosModules.home-manager ];
virtualisation.memorySize = 4096;
virtualisation.cores = 2;
hardware.graphics.enable = true;
environment.variables.LIBGL_ALWAYS_SOFTWARE = "1";
boot.initrd.availableKernelModules = [ "virtio_gpu" ];
virtualisation.qemu.options = [ "-vga none" "-device virtio-gpu-pci,xres=1280,yres=800" ];
users.users.nomarchy = {
isNormalUser = true;
password = "test";
extraGroups = [ "wheel" "video" ];
};
security.sudo.wheelNeedsPassword = false;
# PolicyKit test action so pkexec can raise the agent.
security.polkit.enable = true;
nomarchy.system.stateFile = flake + "/theme-state.json";
systemd.tmpfiles.rules = [
"d /home/nomarchy/.nomarchy 0755 nomarchy users -"
"C /home/nomarchy/.nomarchy/theme-state.json 0644 nomarchy users - ${flake + "/theme-state.json"}"
];
services.greetd.settings.initial_session = {
command = "start-hyprland";
user = "nomarchy";
};
home-manager.useGlobalPkgs = true;
home-manager.users.nomarchy = {
imports = [ flake.homeModules.nomarchy ];
nomarchy.stateFile = flake + "/theme-state.json";
nomarchy.idle.enable = false;
# Ensure pinentry-qt is the active pinentry (keys.nix default).
};
};
testScript = ''
machine.wait_for_unit("multi-user.target")
machine.wait_for_file("/run/user/1000/hypr", 180)
machine.sleep(15)
hy_env = "XDG_RUNTIME_DIR=/run/user/1000 HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/1000/hypr | head -1)"
hyctl = f"su - nomarchy -c '{hy_env} hyprctl"
def clients():
return machine.succeed(f"{hyctl} clients -j'").strip()
dumps = []
# pinentry-qt (keys.nix default)
machine.execute(
f"su - nomarchy -c '{hy_env} timeout 8 pinentry-qt' >/tmp/pinentry.log 2>&1 &"
)
machine.sleep(3)
dumps.append(("pinentry", clients()))
machine.execute("pkill -x pinentry-qt >/dev/null 2>&1 || true")
machine.sleep(1)
# hyprpolkitagent via pkexec (passwordless sudo wheel may still
# open agent depending on polkit rules; best-effort)
machine.execute(
f"su - nomarchy -c '{hy_env} timeout 8 pkexec true' >/tmp/pkexec.log 2>&1 &"
)
machine.sleep(3)
dumps.append(("pkexec", clients()))
machine.execute("pkill -x hyprpolkitagent >/dev/null 2>&1 || true")
machine.execute("pkill -f pkexec >/dev/null 2>&1 || true")
machine.sleep(1)
# Always dump current clients for residual
dumps.append(("idle", clients()))
import json
out_path = machine.succeed("mktemp -d").strip()
summary = []
for label, raw in dumps:
path = f"{out_path}/{label}.json"
machine.succeed(f"cat > {path} <<'EOF'\n{raw}\nEOF")
try:
data = json.loads(raw) if raw else []
except Exception as e:
data = []
summary.append(f"{label}: parse error {e}")
classes = sorted({
c.get("class") or c.get("initialClass") or ""
for c in (data if isinstance(data, list) else [])
} - {""})
titles = sorted({
c.get("title") or c.get("initialTitle") or ""
for c in (data if isinstance(data, list) else [])
} - {""})
summary.append(f"{label}: classes={classes} titles={titles}")
machine.succeed(
"cat > /tmp/float-class-summary.txt <<'EOF'\n"
+ "\n".join(summary)
+ "\nEOF"
)
print("\n".join(summary))
# Copy into the test result via machine.copy_from_vm if available
# at minimum assert pinentry produced *some* client dump JSON.
machine.succeed("test -s /tmp/float-class-summary.txt")
'';
}