1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
# 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/<repo>. 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/<repo> /srv/git/<repo>
# sudo chown -R yokai:ikai /srv/git/<repo> # 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/<repo> remote add sandbox /srv/git/<repo>
# git -C ~/git/<repo> fetch sandbox
# git -C ~/git/<repo> 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/<repo> checkout main
# git -C ~/git/<repo> merge --squash sandbox/main # stage, don't commit
# git -C ~/git/<repo> commit -S -m "...\n\nAssisted-by: claude-code:<model>"
# git -C ~/git/<repo> 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/<repo> checkout -b land sandbox/main # local branch at tip
# git -C ~/git/<repo> rebase -S main # replay onto main, sign each
# git -C ~/git/<repo> checkout main
# git -C ~/git/<repo> merge --ff-only land && git -C ~/git/<repo> branch -d land
# git -C ~/git/<repo> 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/<repo> checkout -b land sandbox/main
# git -C ~/git/<repo> 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/<repo> bundle create /srv/git/x.bundle main
# (hwebs) git -C ~/git/<repo> 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" ];
}
|