diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index e90fbb2..cc92fcb 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -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 diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index cda153d..e504d1d 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -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 diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index 02cb5d1..1a3184e 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -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/ diff --git a/flake.nix b/flake.nix index 000b456..3c8a7d9 100644 --- a/flake.nix +++ b/flake.nix @@ -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 = { ... }: { diff --git a/modules/home/hyprland.nix b/modules/home/hyprland.nix index a85adbf..6a3c006 100644 --- a/modules/home/hyprland.nix +++ b/modules/home/hyprland.nix @@ -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), diff --git a/tools/capture-float-classes.nix b/tools/capture-float-classes.nix new file mode 100644 index 0000000..08246ed --- /dev/null +++ b/tools/capture-float-classes.nix @@ -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") + ''; +}