# enzo — laptop. Gets the shared common/ base plus laptop-only settings. { inputs, pkgs, ... }: { imports = [ # Shared desktop base + enzo-only extras. Add ../../profiles/dev.nix or # apps.nix here, or one-off packages below, as needed. ../../profiles/desktop.nix ../../profiles/dev.nix ./desktop.nix inputs.disko.nixosModules.disko ./disko.nix ./hardware-configuration.nix ]; networking.hostName = "enzo"; # Set this to the NixOS release the laptop is first installed from, then # leave it. (See the comment in machines/kusanagi/default.nix.) system.stateVersion = "26.05"; # --- User account (hwebs) --- # (Desktop groups audio/input come from ./desktop.nix.) users.users.hwebs = { isNormalUser = true; description = "Henry J. Webster"; extraGroups = [ "networkmanager" "wheel" "ikai" ]; packages = with pkgs; [ git tmux neovim # TODO figure out #ripgrep wl-clipboard bc psmisc usbutils stow claude-code ]; }; # --- Sandbox account (yokai) --- # Model: LOCAL CLONES, not a shared working tree. yokai owns its own clone of # each repo under /srv/git; hwebs keeps the canonical repo in ~. Work moves # between the two over git remotes — never by sharing one tree. # # Why not one shared tree: git executes hooks and config out of .git, so a # tree yokai can write must never be one hwebs runs `git` *inside* — a planted # hook/config would run as hwebs and escape the sandbox. Git's "dubious # ownership" warning is exactly that guard; don't defeat it with safe.directory # on a yokai-writable repo. # # CARDINAL RULE: hwebs never runs git with CWD inside /srv/git/. Review # the agent's work by FETCHING its commits into your own repo and reading them # there. # # /srv/git is created below as 2770 root:ikai — group-writable + setgid, so # hwebs and yokai (both in ikai) can each drop clones without sudo and new # files inherit the ikai group. It sits outside ~ so /home/hwebs stays 700 # and yokai never needs to traverse into home. (Tradeoff: yokai has write on # the /srv/git dir itself, i.e. its own play area — fine for a sandbox.) # # Seed the agent's clone (hand a fresh clone to yokai): # git clone ~/git/ /srv/git/ # sudo chown -R yokai:ikai /srv/git/ # yokai now owns its tree # TODO: wrap seed (clone + chown) in a one-shot helper (shell fn / just). # # Pull the agent's work back into YOUR repo (stay in your trusted tree): # git -C ~/git/ remote add sandbox /srv/git/ # git -C ~/git/ fetch sandbox # git -C ~/git/ log --oneline sandbox/main # review, then merge # # Land + SIGN on main (signatures attest that hwebs reviewed+vouches, so hwebs # signs — never put a signing key in yokai's home, and never give yokai yours). # Squash the agent's WIP into one commit you author + sign; satisfies "require # signed commits" branch protection trivially and keeps history clean: # git -C ~/git/ checkout main # git -C ~/git/ merge --squash sandbox/main # stage, don't commit # git -C ~/git/ commit -S -m "...\n\nAssisted-by: claude-code:" # git -C ~/git/ push origin main # Alt — keep granular commits instead of squashing: rebase re-commits each # one signed by hwebs (also all-signed, but review commit-by-commit): # git -C ~/git/ checkout -b land sandbox/main # local branch at tip # git -C ~/git/ rebase -S main # replay onto main, sign each # git -C ~/git/ checkout main # git -C ~/git/ merge --ff-only land && git -C ~/git/ branch -d land # git -C ~/git/ push origin main # Alt — INTERACTIVE, for exploring/curating the agent's history by hand: opens # a todo list (pick/squash/fixup/reword/drop/reorder per commit), then the # message editor. Good when you want to hand-pick what lands. Run it in a real # terminal — interactive git can't be driven through the agent harness: # git -C ~/git/ checkout -b land sandbox/main # git -C ~/git/ rebase -i -S main # -S signs each resulting commit # # ...then merge --ff-only land into main + push, as above. # (Signing config lives in hwebs's dotfiles, not here: commit.gpgsign + # user.signingkey.) # # Airtight variant — exchange via bundles (inert data, no hooks/config run): # (yokai) git -C /srv/git/ bundle create /srv/git/x.bundle main # (hwebs) git -C ~/git/ fetch /srv/git/x.bundle main:sandbox/main # # Run the agent as yokai: sudo -u yokai claude # # Let hwebs drop to yokai without a password. This grants NO new privilege — # it's a de-escalation to a weaker account — so NOPASSWD here is low-risk, # unlike a run-as-root rule. Scoped to runAs=yokai only; root stays gated. security.sudo.extraRules = [ { users = [ "hwebs" ]; runAs = "yokai"; commands = [ { command = "ALL"; options = [ "NOPASSWD" "SETENV" ]; } ]; } ]; users.groups.ikai = { }; users.users.yokai = { isNormalUser = true; description = "Sandbox account for coding agents"; group = "ikai"; packages = with pkgs; [ git claude-code pi-coding-agent ]; }; # Drop dir for the agent's local clones. Created at activation as 2770 # root:ikai — setgid (new entries inherit ikai) + group-writable so hwebs and # yokai can each place clones here without sudo; world sees nothing. Sits # outside ~ so /home/hwebs stays a sealed 700. Trailing "-" = no age-cleaning. # Each clone's own ownership is set when seeded (see the notes above). systemd.tmpfiles.rules = [ "d /srv/git 2770 root ikai -" ]; nix.settings.trusted-users = [ "root" "hwebs" ]; # Wifi is managed from the CLI (nmcli/nmtui); trim the rest. networking.modemmanager.enable = false; # no cellular modem # NOTE: iwd backend was tried but caused interface rename (wlp8s0 -> wlan0), # stale-profile and secret-passing breakage. Back on the default wpa_supplicant. # networking.networkmanager.wifi.backend = "iwd"; # auto-enables iwd, drops wpa_supplicant # --- Disk encryption / hibernation / swap --- # systemd in initrd is required for TPM2 unlock and clean hibernate resume. boot.initrd.systemd.enable = true; # TPM2 auto-unlock. Enroll the key WITH A PIN post-install: # sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 \ # --tpm2-with-pin=yes /dev/nvme0n1p2 # The original LUKS passphrase stays as a fallback keyslot. disko already # declares boot.initrd.luks.devices."crypted".device. boot.initrd.luks.devices."crypted".crypttabExtraOpts = [ "tpm2-device=auto" ]; # zram for everyday paging; the encrypted LVM swap from disko.nix is the # hibernation target (boot.resumeDevice comes from disko's resumeDevice=true). zramSwap.enable = true; # Suspend-to-RAM on lid close, auto-hibernate after a delay so a dying battery # doesn't lose the session. Everyday resume needs only the login password; the # LUKS PIN appears only on cold boot / hibernate resume. services.logind.settings.Login.HandleLidSwitch = "suspend-then-hibernate"; systemd.sleep.settings.Sleep.HibernateDelaySec = "60min"; # Idle trigger. Bare niri has no power daemon watching for inactivity, so with # the lid open the box never sleeps on its own. swayidle (a Wayland idle # client) fires the same suspend-then-hibernate after 15 min of no input. niri # binds it to graphical-session.target and exports WAYLAND_DISPLAY to the user # manager, so this Just Works in the session. `-w` waits for the resume to # finish before re-arming the timer. systemd.user.services.swayidle = { description = "Suspend-then-hibernate after idle"; wantedBy = [ "graphical-session.target" ]; partOf = [ "graphical-session.target" ]; after = [ "graphical-session.target" ]; serviceConfig = { ExecStart = "${pkgs.swayidle}/bin/swayidle -w timeout 900 'systemctl suspend-then-hibernate'"; Restart = "on-failure"; }; }; # btrfs + SSD upkeep. services.btrfs.autoScrub.enable = true; services.fstrim.enable = true; # --- Laptop power management --- # power-profiles-daemon integrates with GNOME's power settings. If you prefer # finer-grained control, disable this and enable services.tlp instead. services.power-profiles-daemon.enable = true; powerManagement.enable = true; # Backlight control from the CLI / keybinds (programs.light was removed from # nixpkgs; acpilight provides the udev rules and brightnessctl the CLI). hardware.acpilight.enable = true; environment.systemPackages = [ pkgs.brightnessctl ]; # --- GPU (AMD integrated) --- # Load amdgpu in the initrd (early KMS) so the native mode is set before the # console font is applied. Otherwise amdgpu loads late, resets the fbcon to # the kernel's 8x16 font, and the greeter shows the wrong (tiny) font while # the Terminus font only survives in the pre-driver LUKS prompt. # (Merges with the dm-snapshot entry in hardware-configuration.nix. If the # driver check shows `radeon` instead of `amdgpu`, swap the name.) boot.initrd.kernelModules = [ "amdgpu" ]; }