flake-update-20260505
1{
2 pkgs,
3 inputs,
4 config,
5 ...
6}:
7{
8 imports = [
9 inputs.daneel.nixosModules.daneel
10 ../common/programs/direnv.nix
11 ../common/programs/git.nix
12 ../common/programs/nix-ld.nix
13 ../common/programs/tmux.nix
14 ../common/services/networkmanager.nix
15 ../common/services/containers.nix
16 ../common/services/docker.nix
17 ../common/services/binfmt.nix
18
19 ../common/services/oomd.nix
20 ../../modules/laptop-keyboard-inhibit
21 # Build and cache infrastructure
22 ../../modules/harmonia
23 ../../modules/job-notify
24 ../../modules/flux-generate
25
26 ];
27
28 # Disable built-in MediaTek MT7922 WiFi (using USB TP-Link AC600 instead)
29 boot.blacklistedKernelModules = [ "mt7921e" ];
30
31 # Xbox controller support (Bluetooth)
32 hardware.xpadneo.enable = true;
33
34 # Gaming setup
35 programs.steam = {
36 enable = true;
37 remotePlay.openFirewall = true; # Steam Link support
38 dedicatedServer.openFirewall = true; # Game server hosting
39 };
40
41 programs.gamemode = {
42 enable = true;
43 settings = {
44 general = {
45 renice = 10; # Increase game process priority
46 };
47 gpu = {
48 apply_gpu_optimisations = "accept-responsibility";
49 gpu_device = 0;
50 amd_performance_level = "high"; # AMD GPU performance mode
51 };
52 };
53 };
54
55 # Greeter wallpaper (accessible to greeter user)
56 environment.etc."greetd/wallpaper.jpg".source = ../../dots/wallpapers/greeter.jpg;
57
58 # QMK/ZMK keyboard flashing support (udev rules + plugdev group)
59 hardware.keyboard.qmk.enable = true;
60
61 # Auto-disable internal keyboard when Eyelash Corne (BT) is connected
62 services.laptop-keyboard-inhibit = {
63 enable = false; # disabled: spams D-Bus with bluetooth reconnect attempts
64 keyboards = [ "Eyelash Corne" ];
65 };
66
67 # Age secrets
68 age.secrets = {
69 "opencode-password" = {
70 file = ../../secrets/okinawa/opencode-password.age;
71 mode = "400";
72 owner = "vincent";
73 };
74 "groq-api-key" = {
75 file = ../../secrets/okinawa/groq-api-key.age;
76 mode = "400";
77 owner = "vincent";
78 };
79 "openrouter-api-key" = {
80 file = ../../secrets/okinawa/openrouter-api-key.age;
81 mode = "400";
82 owner = "vincent";
83 };
84 "gemini-api-key" = {
85 file = ../../secrets/okinawa/gemini-api-key.age;
86 mode = "400";
87 owner = "vincent";
88 };
89 "xmpp-research-bot-password" = {
90 file = ../../secrets/okinawa/xmpp-research-bot-password.age;
91 mode = "400";
92 owner = "vincent";
93 };
94 # Harmonia binary cache signing key
95 "harmonia-okinawa-signing-key" = {
96 file = ../../secrets/harmonia/okinawa-signing-key.age;
97 mode = "440";
98 owner = "root";
99 group = "root";
100 };
101 # ntfy notification token (shared secret)
102 "ntfy-token" = {
103 file = ../../secrets/sakhalin/ntfy-token.age;
104 mode = "440";
105 owner = "root";
106 group = "users";
107 };
108 };
109
110 # Daneel XMPP Research Bot
111 services.daneel = {
112 enable = true;
113 xmppJid = "researchbot@xmpp.sbr.pm";
114 ownerJid = "vincent@xmpp.sbr.pm";
115 xmppPasswordFile = config.age.secrets."xmpp-research-bot-password".path;
116 geminiApiKeyFile = config.age.secrets."gemini-api-key".path;
117 inboxPath = "/home/vincent/desktop/org/inbox.org";
118 debug = true;
119 user = "vincent";
120 group = "users";
121 };
122
123 # Binary cache server (x86_64-linux)
124 services.harmonia-cache = {
125 enable = true;
126 signKeyPath = config.age.secrets."harmonia-okinawa-signing-key".path;
127 port = 5000;
128 workers = 4;
129 priority = 30;
130
131 # Nightly cache pre-population
132 builder = {
133 enable = true;
134 systems = [
135 "okinawa" # Self
136 "kyushu" # Work laptop
137 "sakhalin" # Server
138 ];
139 schedule = "02:00"; # 2 AM daily
140 notification = {
141 enable = true;
142 tokenFile = config.age.secrets."ntfy-token".path;
143 };
144 };
145 };
146
147 # Remote build system
148 services.job-notify = {
149 enable = true;
150 ntfyServer = "https://ntfy.sbr.pm";
151 ntfyTokenFile = config.age.secrets."ntfy-token".path;
152 defaultTopic = "builds";
153 };
154
155 # Website flux auto-generate and deploy (hourly)
156 services.flux-generate.enable = true;
157
158 # OpenCode web interface for remote AI coding
159 # Accessible via opencode.sbr.pm through rhea's Traefik reverse proxy
160 systemd.services.opencode-web =
161 let
162 opencode-config = pkgs.writeText "opencode.json" (
163 builtins.toJSON {
164 "$schema" = "https://opencode.ai/config.json";
165 provider = {
166 # Local llama-server (OpenAI-compatible API)
167 llama-server = {
168 npm = "@ai-sdk/openai-compatible";
169 name = "llama-server (local)";
170 options = {
171 baseURL = "http://127.0.0.1:8090/v1";
172 apiKey = "local-test";
173 };
174 models = {
175 "Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M" = {
176 name = "Qwen3 Coder 30B MoE";
177 };
178 "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M" = {
179 name = "Qwen 2.5 Coder 7B";
180 };
181 "Qwen/Qwen3-8B-GGUF:Q4_K_M" = {
182 name = "Qwen3 8B";
183 };
184 "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M" = {
185 name = "DeepSeek R1 Distill 7B";
186 };
187 "unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M" = {
188 name = "Phi-4 mini 3.8B";
189 };
190 "Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M" = {
191 name = "Qwen3 Coder Next 80B MoE";
192 };
193 };
194 };
195 # Vertex AI (Claude on Google Cloud)
196 google-vertex = {
197 options = {
198 project = "itpc-gcp-pnd-pe-eng-claude";
199 location = "global";
200 };
201 };
202 };
203 server = {
204 hostname = "0.0.0.0";
205 port = 5555;
206 };
207 }
208 );
209 in
210 {
211 description = "OpenCode web interface";
212 after = [
213 "network.target"
214 "llama-cpp.service"
215 ];
216 wantedBy = [ "multi-user.target" ];
217
218 serviceConfig = {
219 Type = "simple";
220 WorkingDirectory = "/home/vincent";
221 ExecStart = pkgs.writeShellScript "opencode-web-start" ''
222 # Load secrets into environment
223 export OPENCODE_SERVER_PASSWORD=$(cat ${config.age.secrets."opencode-password".path})
224 export GROQ_API_KEY=$(cat ${config.age.secrets."groq-api-key".path})
225 export OPENROUTER_API_KEY=$(cat ${config.age.secrets."openrouter-api-key".path})
226 export GOOGLE_GENERATIVE_AI_API_KEY=$(cat ${config.age.secrets."gemini-api-key".path})
227 exec ${pkgs.opencode}/bin/opencode web
228 '';
229 Restart = "on-failure";
230 RestartSec = 10;
231
232 User = "vincent";
233 Group = "users";
234 };
235
236 environment = {
237 HOME = "/home/vincent";
238 OPENCODE_SERVER_USERNAME = "opencode";
239 # Config file location
240 XDG_CONFIG_HOME = "/run/opencode/config";
241 GOOGLE_VERTEX_PROJECT = "itpc-gcp-pnd-pe-eng-claude";
242 GOOGLE_VERTEX_LOCATION = "global";
243 };
244
245 # Copy config file before start (--remove-destination needed because
246 # the nix store source is read-only, so cp can't overwrite in place)
247 preStart = ''
248 mkdir -p /run/opencode/config/opencode
249 cp --remove-destination ${opencode-config} /run/opencode/config/opencode/opencode.json
250 '';
251 };
252
253 # Ensure /run/opencode directory exists
254 systemd.tmpfiles.rules = [
255 "d /run/opencode 0755 vincent users -"
256 "d /run/opencode/config 0755 vincent users -"
257 ];
258
259 # Firewall: OpenCode web + llama-server (VPN access) + monitoring + Harmonia
260 networking.firewall.allowedTCPPorts = [
261 5000 # Harmonia binary cache
262 5555 # OpenCode web
263 8090 # llama-server
264 8880 # readwise-reader triage report
265 9000 # Prometheus node exporter
266 ];
267
268 # llama.cpp server for local LLM inference with Vulkan GPU (RX 6700S)
269 # Router mode: serves multiple models with on-demand loading (--models-max 1)
270 # Only one model loaded in VRAM at a time; auto-swaps on request
271 # Benchmarks on RX 6700S: 7B dense ~40 tok/s, 3B active MoE ~20-40 tok/s (Vulkan, Q4_K_M)
272 systemd.services.llama-cpp =
273 let
274 llama-cpp-vulkan = pkgs.llama-cpp.override { vulkanSupport = true; };
275
276 # Model preset INI for router mode
277 # Section names become model IDs in the API (used in models.json)
278 modelsPreset = pkgs.writeText "llama-models.ini" ''
279 version = 1
280
281 ; Shared settings for all models
282 [*]
283 n-gpu-layers = 99
284 jinja = true
285
286 ; === Coding models ===
287
288 ; Qwen3 Coder Next 80B MoE (3B active) — best coding model, needs ~48GB RAM
289 [Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M]
290
291 ; Qwen3 Coder 30B MoE (3B active) — sweet spot coding, ~19GB RAM
292 [Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M]
293
294 ; Qwen 2.5 Coder 7B dense — lightweight coding, ~5GB (current default)
295 [Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M]
296
297 ; === General purpose models ===
298
299 ; Qwen3 8B dense — best all-rounder (reasoning, tool use, multilingual)
300 [Qwen/Qwen3-8B-GGUF:Q4_K_M]
301
302 ; DeepSeek R1 Distill 7B — deep reasoning / chain-of-thought
303 [bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M]
304
305 ; Phi-4 mini 3.8B — ultra-fast utility model
306 [unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M]
307 '';
308
309 in
310 {
311 description = "llama.cpp server (Vulkan, router mode)";
312 after = [ "network.target" ];
313 wantedBy = [ "multi-user.target" ];
314
315 environment = {
316 GGML_VK_VISIBLE_DEVICES = "0"; # Use RX 6700S dGPU only
317 HIP_VISIBLE_DEVICES = ""; # Disable ROCm to avoid conflicts
318 };
319
320 serviceConfig = {
321 Type = "idle";
322 KillSignal = "SIGINT";
323 ExecStart = builtins.concatStringsSep " " [
324 "${llama-cpp-vulkan}/bin/llama-server"
325 "--log-disable"
326 "--host 0.0.0.0"
327 "--port 8090"
328 "--models-preset ${modelsPreset}"
329 "--models-max 1"
330 "-np 1"
331 "--api-key local-test"
332 ];
333 Restart = "on-failure";
334 RestartSec = 30;
335
336 # Run as vincent to access HuggingFace model cache
337 User = "vincent";
338 Group = "users";
339
340 # GPU access requires relaxed sandboxing
341 PrivateDevices = false;
342 ProtectHome = false;
343 MemoryDenyWriteExecute = false;
344 };
345 };
346
347 # Oneshot service to pre-download all LLM models
348 # Run manually: sudo systemctl start llama-download-models
349 systemd.services.llama-download-models =
350 let
351 llama-cpp-vulkan = pkgs.llama-cpp.override { vulkanSupport = true; };
352 in
353 {
354 description = "Download LLM models for llama.cpp";
355 after = [ "network-online.target" ];
356 wants = [ "network-online.target" ];
357
358 serviceConfig = {
359 Type = "oneshot";
360 RemainAfterExit = false;
361 User = "vincent";
362 Group = "users";
363 ExecStart = pkgs.writeShellScript "llama-download-models" ''
364 set -euo pipefail
365 export PATH="${llama-cpp-vulkan}/bin:$PATH"
366
367 models=(
368 "Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M"
369 "Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M"
370 "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M"
371 "Qwen/Qwen3-8B-GGUF:Q4_K_M"
372 "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M"
373 "unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M"
374 )
375
376 for model in "''${models[@]}"; do
377 echo "Checking/downloading $model..."
378 # Use llama-cli to trigger download, then exit immediately
379 timeout 120 llama-cli -hf "$model" -p "test" -n 1 --no-display-prompt 2>&1 || true
380 echo "Done: $model"
381 done
382 '';
383 };
384 };
385
386 # System packages for LLM, gaming, and tools
387 environment.systemPackages = with pkgs; [
388 # nix-flake-update wrapper with pre-baked config for the home repo
389 # Run manually: nix-flake-update-home [--dry-run] [--no-auto-fix]
390 (pkgs.writeShellScriptBin "nix-flake-update-home" ''
391 export REPO_PATH="/home/vincent/src/home"
392 export FLAKE_PATH="/home/vincent/src/home"
393 export GIT_REMOTE="origin"
394 export MAIN_BRANCH="main"
395 export BRANCH_PREFIX="flake-update-"
396 export NTFY_TOPIC="nix-updates"
397 export NTFY_SERVER="https://ntfy.sbr.pm"
398 export NTFY_TOKEN_FILE="/run/agenix/ntfy-token"
399 export INBOX_ORG="/home/vincent/desktop/org/inbox.org"
400 export BUILD_SYSTEMS="okinawa kyushu sakhalin carthage rhea aion athena demeter aix"
401 export GIT_SSH_COMMAND="ssh -F /dev/null -o IdentitiesOnly=yes -i /home/vincent/.ssh/id_passage -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/home/vincent/.ssh/known_hosts"
402
403 # AI-powered auto-fix
404 export AUTO_FIX="true"
405 export AUTO_FIX_COMMAND="pi"
406 export AUTO_FIX_EXTRA_ARGS="--provider google-vertex-claude --no-session --no-themes --no-skills"
407 export AUTO_FIX_MAX_ATTEMPTS="3"
408 export GOOGLE_CLOUD_PROJECT="itpc-gcp-pnd-pe-eng-claude"
409 export GOOGLE_CLOUD_LOCATION="global"
410
411 # Allow overriding via CLI args
412 for arg in "$@"; do
413 case "$arg" in
414 --dry-run) export DRY_RUN="true" ;;
415 --no-auto-fix) export AUTO_FIX="false" ;;
416 esac
417 done
418
419 exec ${pkgs.nix-flake-update}/bin/nix-flake-update
420 '')
421 # LLM tools (same package as the service, for CLI use)
422 (llama-cpp.override {
423 vulkanSupport = true;
424 })
425
426 # GPU monitoring and management
427 radeontop # GPU usage monitor
428 clinfo # Check OpenCL
429 vulkan-tools # vulkaninfo
430 rocmPackages.rocminfo
431 asusctl # CLI for supergfxctl
432 supergfxctl
433
434 # Gaming tools
435 mangohud # FPS and performance overlay
436 goverlay # MangoHud configuration GUI
437 protonup-qt # Install custom Proton versions (Proton-GE)
438 protontricks # Winetricks for Proton prefixes
439 heroic # GOG/Epic Games launcher
440
441 # Development
442 python3
443 uv
444
445 # Audio utilities for G14
446 alsa-utils # amixer, alsactl for AMP control
447 jamesdsp # DSP/EQ to replace Dolby Atmos (use with asus-jamesdsp presets)
448 ];
449
450 # Prometheus node exporter (configured in common module)
451 # Port and basic collectors already set in ../common/base (prometheus-exporters-node.nix)
452
453 # Keep dGPU (Navi 23 / RX 6700S) awake so HDMI hotplug is detected.
454 # The HDMI port is wired to the dGPU; runtime suspend prevents detection.
455 services.udev.extraRules = ''
456 ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x1002", ATTR{device}=="0x73ef", ATTR{power/control}="on"
457 '';
458
459 # Lid handling: ignore (desktop replacement, mostly on AC)
460 services.logind.settings.Login = {
461 HandleLidSwitch = "ignore";
462 HandleLidSwitchExternalPower = "ignore";
463 HandleLidSwitchDocked = "ignore";
464 };
465
466 # ASUS G14 2022 Audio Fix
467 # Sets ALSA mixer levels at boot and after resume
468 systemd.services.asus-g14-audio-fix = {
469 description = "ASUS G14 Audio Amplifier Initialization";
470 after = [
471 "sound.target"
472 "alsa-restore.service"
473 ];
474 wantedBy = [
475 "multi-user.target"
476 "post-resume.target"
477 ];
478
479 serviceConfig = {
480 Type = "oneshot";
481 RemainAfterExit = true;
482 ExecStart = pkgs.writeShellScript "g14-audio-init" ''
483 # Find the ALC285 audio card (internal speakers)
484 CARD=$(${pkgs.alsa-utils}/bin/aplay -l | grep "ALC285" | head -1 | sed 's/card \([0-9]\).*/\1/')
485
486 if [ -z "$CARD" ]; then
487 echo "ERROR: Could not find ALC285 audio card"
488 exit 1
489 fi
490
491 echo "Initializing G14 audio on card $CARD"
492
493 # Set Master volume to 100% and unmute
494 ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset Master 100% unmute || true
495
496 # Set Speaker volume (tweeters)
497 ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset Speaker 100% unmute || true
498
499 # Set Bass Speaker (subwoofers)
500 ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset 'Bass Speaker' unmute || true
501
502 # Set Headphone volume
503 ${pkgs.alsa-utils}/bin/amixer -c "$CARD" sset Headphone 100% unmute || true
504
505 echo "G14 audio initialization complete"
506 '';
507 };
508 };
509}