From 41f362bfb982e31b14fde4efa42b30d91d8ab845 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 17 Oct 2026 09:40:06 +0800 Subject: [PATCH 1/7] feat(windows-vm): enable libvirt on nemesis --- nix/hosts.nix | 1 + nix/modules/windows-vm.nix | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 nix/modules/windows-vm.nix diff --git a/nix/hosts.nix b/nix/hosts.nix index 4eabcb81..a0cc7dd4 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -129,6 +129,7 @@ in cfg.modules.nixos.user-primary cfg.modules.nixos.agent-browser-shared cfg.modules.nixos.nvidia-graphics + cfg.modules.nixos.windows-vm cfg.modules.nixos.xremap cfg.modules.nixos.steam cfg.modules.nixos.prismlauncher diff --git a/nix/modules/windows-vm.nix b/nix/modules/windows-vm.nix new file mode 100644 index 00000000..68473811 --- /dev/null +++ b/nix/modules/windows-vm.nix @@ -0,0 +1,14 @@ +{ + config.flake.modules.nixos.windows-vm = + { primaryUser, ... }: + { + users.users.${primaryUser.name}.extraGroups = [ "libvirtd" ]; + + virtualisation.libvirtd = { + enable = true; + onBoot = "ignore"; + onShutdown = "shutdown"; + qemu.runAsRoot = false; + }; + }; +} From 8d044d192c663c3986414cde4317719f51e655b7 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 17 Oct 2026 11:12:13 +0800 Subject: [PATCH 2/7] docs(AGENTS.md): document environment variable scope --- AGENTS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index cca01d0a..4ed264e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,20 @@ The Nix config in `nix/` is organized as hosts, profiles, and atomic modules. Ho Nix flake inputs and source files must be tracked by git to be evaluated; remember to `git add` new files before evaluation/checks. +### Environment variables + +- Every user on one machine: + - NixOS: use `environment.sessionVariables`. + - nix-darwin: use `environment.variables`. +- One user's login environment: use `home.sessionVariables`. +- One service: + - systemd: + - System service: use `systemd.services..environment`. System services are not available in Home Manager. + - User service: use `systemd.user.services..Service.Environment`. + - launchd: + - System daemon: use `launchd.daemons..serviceConfig.EnvironmentVariables`. System daemons are not available in Home Manager. + - User agent: use `launchd.agents..config.EnvironmentVariables`. + Style constraints: - If a Nix module needs no arguments, use a plain attribute set. Omit `{ ... }:` and `_:`. From 6a1c2b0ad466779db1b756a459cc62f263a9e38e Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 17 Oct 2026 11:12:13 +0800 Subject: [PATCH 3/7] feat(windows-vm): define direct zagreus domain --- nix/modules/windows-vm.nix | 150 ++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/nix/modules/windows-vm.nix b/nix/modules/windows-vm.nix index 68473811..74d89303 100644 --- a/nix/modules/windows-vm.nix +++ b/nix/modules/windows-vm.nix @@ -1,7 +1,106 @@ { config.flake.modules.nixos.windows-vm = - { primaryUser, ... }: { + config, + lib, + pkgs, + primaryUser, + ... + }: + let + windowsDisk = "/dev/disk/by-id/nvme-CT2000P3SSD8_2325E6E6888E"; + windowsDomain = pkgs.writeText "zagreus.xml" '' + + zagreus + 799a733f-d0ea-4f6f-aec4-76889cf6210c + 8 + 8 + 4 + + hvm + + + + + + + + + + + + + + + + + + + + + + + + destroy + restart + destroy + + /run/libvirt/nix-emulators/qemu-system-x86_64 + + + + + + + + + + + + + + + + + + + + + + ''; + ownershipHook = pkgs.writeShellScript "zagreus-disk-ownership" '' + if [[ "$1" != "zagreus" || "$2" != "prepare" || "$3" != "begin" ]]; then + exit 0 + fi + + windows_device=$(${lib.getExe' pkgs.coreutils "readlink"} -f ${windowsDisk}) + if [[ -z "$windows_device" || ! -b "$windows_device" ]]; then + echo "Windows disk is unavailable" >&2 + exit 1 + fi + + if ${lib.getExe' pkgs.util-linux "findmnt"} --noheadings --source "$windows_device" >/dev/null; then + echo "Windows disk is mounted" >&2 + exit 1 + fi + for partition in "$windows_device"p*; do + if [[ -e "$partition" ]] && ${lib.getExe' pkgs.util-linux "findmnt"} --noheadings --source "$partition" >/dev/null; then + echo "Windows partition $partition is mounted" >&2 + exit 1 + fi + done + + if ${pkgs.findutils}/bin/find "/sys/class/block/$(${lib.getExe' pkgs.coreutils "basename"} "$windows_device")/holders" -mindepth 1 -maxdepth 1 | ${lib.getExe pkgs.gnugrep} -q .; then + echo "Windows disk still has kernel holders" >&2 + exit 1 + fi + ''; + in + { + environment.sessionVariables.LIBVIRT_DEFAULT_URI = "qemu:///system"; + networking.firewall.allowedTCPPorts = [ 5900 ]; users.users.${primaryUser.name}.extraGroups = [ "libvirtd" ]; virtualisation.libvirtd = { @@ -9,6 +108,55 @@ onBoot = "ignore"; onShutdown = "shutdown"; qemu.runAsRoot = false; + hooks.qemu.zagreus-disk-ownership = ownershipHook; + }; + + systemd.services = { + windows-vm-define = { + description = "Define the zagreus Windows VM"; + wantedBy = [ "multi-user.target" ]; + requires = [ + "libvirtd-config.service" + "libvirtd.service" + ]; + after = [ + "libvirtd-config.service" + "libvirtd.service" + ]; + path = [ + config.virtualisation.libvirtd.package + pkgs.coreutils + pkgs.gawk + pkgs.gnugrep + ]; + script = '' + virt-xml-validate ${windowsDomain} + virsh -c qemu:///system define ${windowsDomain} + ''; + serviceConfig.Type = "oneshot"; + }; + libvirt-default-network = { + description = "Start the temporary libvirt NAT network"; + wantedBy = [ "multi-user.target" ]; + requires = [ + "libvirtd-config.service" + "libvirtd.service" + ]; + after = [ + "libvirtd-config.service" + "libvirtd.service" + ]; + path = [ config.virtualisation.libvirtd.package ]; + script = '' + if ! virsh -c qemu:///system net-info default | grep -q '^Active:.*yes$'; then + virsh -c qemu:///system net-start default + fi + ''; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + }; }; }; } From ffb67d58cf74537f467f7fb6e046b867051f5bd0 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 17 Oct 2026 17:13:30 +0800 Subject: [PATCH 4/7] refactor(nemesis): retire Windows filesystem mount --- nix/hosts.nix | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/nix/hosts.nix b/nix/hosts.nix index a0cc7dd4..a9e7de92 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -185,36 +185,6 @@ in "dmask=0077" ]; }; - "/mnt/windows" = { - device = "/dev/disk/by-uuid/0EA45F71A45F59F3"; - fsType = "ntfs3"; - options = [ - "rw" - "uid=1000" - "umask=022" - "iocharset=utf8" - "windows_names" - "nofail" - "x-systemd.automount" - "x-systemd.idle-timeout=10min" - ]; - }; - }; - # Samba keeps its own password database. After rebuilding, check for an - # existing login with `sudo pdbedit -L -u rafiq`; if absent, set one - # with `sudo smbpasswd -a rafiq` (the same login password can be used). - services.samba = { - enable = true; - openFirewall = true; - settings = { - global."security" = "user"; - windows-profile = { - path = "/mnt/windows/Users/rafiq"; - "valid users" = "rafiq"; - "guest ok" = "no"; - "read only" = "no"; - }; - }; }; } ) From 32b283a7be183147ca798d374d96b6781aeee512 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 17 Oct 2026 17:23:02 +0800 Subject: [PATCH 5/7] feat(networking): bridge nemesis ethernet for zagreus --- nix/modules/windows-vm.nix | 69 ++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/nix/modules/windows-vm.nix b/nix/modules/windows-vm.nix index 74d89303..6cc5b52d 100644 --- a/nix/modules/windows-vm.nix +++ b/nix/modules/windows-vm.nix @@ -53,9 +53,9 @@ - + - + @@ -100,7 +100,48 @@ in { environment.sessionVariables.LIBVIRT_DEFAULT_URI = "qemu:///system"; - networking.firewall.allowedTCPPorts = [ 5900 ]; + networking = { + firewall.allowedTCPPorts = [ 5900 ]; + networkmanager = { + settings.main.no-auto-default = "enp12s0"; + ensureProfiles.profiles = { + lan-bridge = { + connection = { + id = "lan-bridge"; + uuid = "90f94c60-a8ea-49c4-8867-12b7ec620f77"; + type = "bridge"; + interface-name = "br0"; + autoconnect = true; + autoconnect-priority = 100; + }; + bridge = { + stp = false; + mac-address = "74:56:3C:35:AB:E5"; + }; + ipv4.method = "auto"; + ipv6 = { + method = "auto"; + addr-gen-mode = "stable-privacy"; + }; + }; + lan-bridge-port = { + connection = { + id = "lan-bridge-port"; + uuid = "19af3f2e-63f4-47ff-a4a7-91eb30648b22"; + type = "ethernet"; + interface-name = "enp12s0"; + autoconnect = true; + controller = "br0"; + port-type = "bridge"; + }; + ethernet.mac-address = "74:56:3C:35:AB:E5"; + }; + }; + }; + }; + services.udev.extraRules = '' + ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL_SHORT}=="2325E6E6888E", GROUP="qemu-libvirtd", MODE="0660" + ''; users.users.${primaryUser.name}.extraGroups = [ "libvirtd" ]; virtualisation.libvirtd = { @@ -135,28 +176,6 @@ ''; serviceConfig.Type = "oneshot"; }; - libvirt-default-network = { - description = "Start the temporary libvirt NAT network"; - wantedBy = [ "multi-user.target" ]; - requires = [ - "libvirtd-config.service" - "libvirtd.service" - ]; - after = [ - "libvirtd-config.service" - "libvirtd.service" - ]; - path = [ config.virtualisation.libvirtd.package ]; - script = '' - if ! virsh -c qemu:///system net-info default | grep -q '^Active:.*yes$'; then - virsh -c qemu:///system net-start default - fi - ''; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - }; }; }; } From a649107c3a70091015c68f5945afffdb2514fca9 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sat, 17 Oct 2026 21:49:27 +0800 Subject: [PATCH 6/7] feat(windows-vm): add guest integration devices --- nix/modules/windows-vm.nix | 64 +++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/nix/modules/windows-vm.nix b/nix/modules/windows-vm.nix index 6cc5b52d..a274190d 100644 --- a/nix/modules/windows-vm.nix +++ b/nix/modules/windows-vm.nix @@ -8,7 +8,34 @@ ... }: let + # The serial-based path remains stable when NVMe discovery order changes. windowsDisk = "/dev/disk/by-id/nvme-CT2000P3SSD8_2325E6E6888E"; + # Make the Windows driver bundle and standalone guest-agent installer + # available without a mutable download. Keep the CD small by omitting the + # package's duplicate per-version driver tree. + virtioWinIso = pkgs.runCommand "virtio-win.iso" { nativeBuildInputs = [ pkgs.xorriso ]; } '' + xorriso -as mkisofs -quiet -R -J -graft-points -o "$out" \ + virtio-win-guest-tools.exe=${pkgs.virtio-win}/virtio-win-guest-tools.exe \ + qemu-ga-x86_64.msi=${pkgs.virtio-win}/guest-agent/qemu-ga-x86_64.msi + ''; + # RDP is the normal desktop path. VNC remains a firmware/recovery console. + # Credentials stay interactive and never enter the Nix store. + zagreusRdp = pkgs.writeShellApplication { + name = "zagreus-rdp"; + runtimeInputs = [ pkgs.freerdp ]; + text = '' + exec xfreerdp \ + /v:10.10.0.96 \ + /u:rafiq \ + /d:zagreus \ + /cert:ignore \ + /f \ + /scale:180 \ + /audio-mode:redirect \ + /sound:sys:pulse \ + "$@" + ''; + }; windowsDomain = pkgs.writeText "zagreus.xml" '' zagreus @@ -46,18 +73,28 @@ destroy /run/libvirt/nix-emulators/qemu-system-x86_64 + + + + + + + + + + @@ -66,6 +103,17 @@ + + + + + + + + + + + @@ -99,11 +147,19 @@ ''; in { - environment.sessionVariables.LIBVIRT_DEFAULT_URI = "qemu:///system"; + environment = { + sessionVariables.LIBVIRT_DEFAULT_URI = "qemu:///system"; + systemPackages = [ + pkgs.cifs-utils + zagreusRdp + ]; + }; networking = { firewall.allowedTCPPorts = [ 5900 ]; networkmanager = { settings.main.no-auto-default = "enp12s0"; + # The host keeps its existing MAC and DHCP identity on the bridge; + # enp12s0 becomes a Layer 2 port shared with the VM tap device. ensureProfiles.profiles = { lan-bridge = { connection = { @@ -139,6 +195,8 @@ }; }; }; + # QEMU is deliberately unprivileged. Grant only its service group access + # to the dedicated Windows disk instead of running the whole VM as root. services.udev.extraRules = '' ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL_SHORT}=="2325E6E6888E", GROUP="qemu-libvirtd", MODE="0660" ''; @@ -149,10 +207,14 @@ onBoot = "ignore"; onShutdown = "shutdown"; qemu.runAsRoot = false; + # The hook runs for every libvirt start path and rejects concurrent + # Linux ownership before QEMU is allowed to open the physical disk. hooks.qemu.zagreus-disk-ownership = ownershipHook; }; systemd.services = { + # Keep the XML declarative while leaving VM start/stop under explicit + # user control. Redefining an active domain only changes its next boot. windows-vm-define = { description = "Define the zagreus Windows VM"; wantedBy = [ "multi-user.target" ]; From a6123d80adcd460759ab56f49001fea5b1780c15 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Sun, 18 Oct 2026 15:19:01 +0800 Subject: [PATCH 7/7] feat(windows-vm): guard native reboot ownership --- nix/hosts.nix | 4 - nix/modules/quickshell.nix | 2 +- nix/modules/windows-vm.nix | 144 ++++++++++++++++++++++++++- quickshell/nemesis-shell/BarHost.qml | 2 +- 4 files changed, 141 insertions(+), 11 deletions(-) diff --git a/nix/hosts.nix b/nix/hosts.nix index a9e7de92..9411fe6b 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -164,10 +164,6 @@ in loader.systemd-boot = { enable = true; edk2-uefi-shell.enable = true; - windows."11-pro" = { - title = "Windows 11 Pro"; - efiDeviceHandle = "HD0d"; - }; }; loader.efi.canTouchEfiVariables = true; kernelPackages = pkgs.linuxPackages_latest; diff --git a/nix/modules/quickshell.nix b/nix/modules/quickshell.nix index 82fa150d..bd74f738 100644 --- a/nix/modules/quickshell.nix +++ b/nix/modules/quickshell.nix @@ -18,7 +18,7 @@ in cp -r ${../../quickshell/nemesis-shell} "$out" chmod -R u+w "$out" substituteInPlace "$out/BarHost.qml" \ - --replace-fail "@systemctl@" "${lib.getExe' pkgs.systemd "systemctl"}" + --replace-fail "@rebootToWindows@" "/run/current-system/sw/bin/reboot-to-windows" ''; in { diff --git a/nix/modules/windows-vm.nix b/nix/modules/windows-vm.nix index a274190d..4801778f 100644 --- a/nix/modules/windows-vm.nix +++ b/nix/modules/windows-vm.nix @@ -8,6 +8,10 @@ ... }: let + # The boot entry name is shared by systemd-boot and the reboot helper so + # the top bar cannot drift to a different firmware target. + windowsBootName = "11-pro"; + windowsBootEntry = "windows_${windowsBootName}.conf"; # The serial-based path remains stable when NVMe discovery order changes. windowsDisk = "/dev/disk/by-id/nvme-CT2000P3SSD8_2325E6E6888E"; # Make the Windows driver bundle and standalone guest-agent installer @@ -118,11 +122,8 @@ ''; - ownershipHook = pkgs.writeShellScript "zagreus-disk-ownership" '' - if [[ "$1" != "zagreus" || "$2" != "prepare" || "$3" != "begin" ]]; then - exit 0 - fi - + # Both VM startup and native reboot use the same block ownership checks. + diskSafetyCheck = '' windows_device=$(${lib.getExe' pkgs.coreutils "readlink"} -f ${windowsDisk}) if [[ -z "$windows_device" || ! -b "$windows_device" ]]; then echo "Windows disk is unavailable" >&2 @@ -145,12 +146,134 @@ exit 1 fi ''; + ownershipHook = pkgs.writeShellScript "zagreus-disk-ownership" '' + if [[ "$1" != "zagreus" || "$2" != "prepare" || "$3" != "begin" ]]; then + exit 0 + fi + ${diskSafetyCheck} + ''; + # Only this immutable helper receives passwordless sudo. It refuses to + # arm native boot until libvirt and the block layer both report safety. + rebootToWindowsRoot = pkgs.writeShellApplication { + name = "reboot-to-windows-root"; + runtimeInputs = [ + config.virtualisation.libvirtd.package + pkgs.coreutils + pkgs.findutils + pkgs.glibc.bin + pkgs.gnugrep + pkgs.jq + pkgs.procps + pkgs.psmisc + pkgs.systemd + pkgs.util-linux + ]; + text = '' + mode="''${1:---reboot}" + if [[ "$mode" != "--reboot" && "$mode" != "--check" && "$mode" != "--prepare-only" ]]; then + echo "usage: reboot-to-windows-root [--reboot|--check|--prepare-only]" >&2 + exit 2 + fi + + exec 9>/run/reboot-to-windows.lock + if ! flock --nonblock 9; then + echo "Another Windows reboot request is already running" >&2 + exit 1 + fi + + state=$(virsh -c qemu:///system domstate zagreus 2>/dev/null || true) + if [[ "$state" != "shut off" ]]; then + echo "Shut down the zagreus VM first; current state: ''${state:-unknown}" >&2 + exit 1 + fi + if pgrep --full -- 'qemu-system-x86_64 .*guest=zagreus' >/dev/null; then + echo "The zagreus QEMU process is still running" >&2 + exit 1 + fi + + ${diskSafetyCheck} + devices=("$windows_device") + for partition in "$windows_device"p*; do + [[ -e "$partition" ]] && devices+=("$partition") + done + if fuser --silent "''${devices[@]}"; then + echo "A process still has the Windows disk open" >&2 + exit 1 + fi + + if ! bootctl --variables=yes list --json=short | jq --exit-status --arg entry ${lib.escapeShellArg windowsBootEntry} 'any(.[]; .id == $entry)' >/dev/null; then + echo "Windows boot entry ${windowsBootEntry} is unavailable" >&2 + exit 1 + fi + + if [[ "$mode" == "--check" ]]; then + echo "Ready to reboot into Windows 11 Pro" + exit 0 + fi + + read_oneshot() { + for variable in /sys/firmware/efi/efivars/LoaderEntryOneShot-*; do + [[ -e "$variable" ]] || continue + dd if="$variable" bs=1 skip=4 status=none | iconv -f UTF-16LE -t UTF-8 | tr -d '\0' + return + done + } + clear_oneshot() { + bootctl --variables=yes set-oneshot "" + } + + bootctl --variables=yes set-oneshot ${lib.escapeShellArg windowsBootEntry} + actual=$(read_oneshot) + if [[ "$actual" != ${lib.escapeShellArg windowsBootEntry} ]]; then + clear_oneshot + echo "Could not verify the Windows one-shot boot entry" >&2 + exit 1 + fi + + if [[ "$mode" == "--prepare-only" ]]; then + clear_oneshot + if [[ -n "$(read_oneshot)" ]]; then + echo "Could not clear the Windows one-shot boot entry" >&2 + exit 1 + fi + echo "Set, verified and cleared the Windows one-shot boot entry" + exit 0 + fi + + sync + if ! systemctl reboot; then + clear_oneshot + echo "Reboot failed; cleared the Windows one-shot boot entry" >&2 + exit 1 + fi + ''; + }; + # The user-facing wrapper turns safety failures into a desktop + # notification while keeping EFI and reboot operations inside sudo. + rebootToWindows = pkgs.writeShellApplication { + name = "reboot-to-windows"; + runtimeInputs = [ pkgs.libnotify ]; + text = '' + mode="''${1:---reboot}" + if ! output=$(/run/wrappers/bin/sudo ${lib.getExe rebootToWindowsRoot} "$mode" 2>&1); then + notify-send --urgency=critical "Cannot reboot to Windows" "$output" + printf '%s\n' "$output" >&2 + exit 1 + fi + [[ -n "$output" ]] && printf '%s\n' "$output" + ''; + }; in { + boot.loader.systemd-boot.windows.${windowsBootName} = { + title = "Windows 11 Pro"; + efiDeviceHandle = "HD0d"; + }; environment = { sessionVariables.LIBVIRT_DEFAULT_URI = "qemu:///system"; systemPackages = [ pkgs.cifs-utils + rebootToWindows zagreusRdp ]; }; @@ -200,6 +323,17 @@ services.udev.extraRules = '' ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL_SHORT}=="2325E6E6888E", GROUP="qemu-libvirtd", MODE="0660" ''; + security.sudo.extraRules = [ + { + users = [ primaryUser.name ]; + commands = [ + { + command = lib.getExe rebootToWindowsRoot; + options = [ "NOPASSWD" ]; + } + ]; + } + ]; users.users.${primaryUser.name}.extraGroups = [ "libvirtd" ]; virtualisation.libvirtd = { diff --git a/quickshell/nemesis-shell/BarHost.qml b/quickshell/nemesis-shell/BarHost.qml index 3061b46b..df0af0f3 100644 --- a/quickshell/nemesis-shell/BarHost.qml +++ b/quickshell/nemesis-shell/BarHost.qml @@ -213,7 +213,7 @@ Scope { cursorShape: Qt.PointingHandCursor onClicked: { root.powerMenuOpen = false; - Quickshell.execDetached(["/run/wrappers/bin/sudo", "@systemctl@", "reboot", "--boot-loader-entry", "windows_11-pro.conf"]); + Quickshell.execDetached(["@rebootToWindows@"]); } }