Compare commits

..

152 Commits

Author SHA1 Message Date
ecbf226b01 temp: remove nh to skip Haskell cross-compile 2026-06-17 20:24:22 -04:00
09add9f5e4 fix: remove duplicate extraSpecialArgs in gortium.nix 2026-06-17 08:30:01 -04:00
b4b928a985 fix: clean module and flake after merge 2026-06-17 08:29:24 -04:00
33e98f32d7 feat: add HackerGadgets AIO v2 board module + enable on uConsole CM5 2026-06-17 08:27:15 -04:00
332f1cca1a chore: update nixos-uconsole flake.lock to latest pr/dcs-panel-detection 2026-06-17 08:27:01 -04:00
102586d7e8 fix: switch nixos-uconsole to cm5_fix branch (patches OK)
- Remove local boot.kernelPatches (now in nixos-uconsole fork)
- Point to github:gortium/nixos-uconsole/cm5_fix instead of pr/dcs-panel-detection
2026-06-16 19:21:54 -04:00
43f8d8a61c fix: correct aiov2_ctl hash from actual build 2026-06-16 19:12:37 -04:00
6aca5466b6 fix: convert hash to proper SRI base64 format 2026-06-16 19:11:43 -04:00
f0ec375875 fix: set real hash for aiov2_ctl fetchFromGitHub 2026-06-16 19:11:00 -04:00
e05ef66b8f fix: correct secrets path in configuration.nix (../../secrets from hosts/uconsole-cm5/) 2026-06-16 19:09:24 -04:00
a2096efc3f fix: correct dotfiles path in home.nix (relative to repo root) 2026-06-16 19:08:49 -04:00
088a82d730 fix: pass hostName as extraSpecialArgs to home-manager
home.nix used config.networking.hostName but home-manager modules
don't have access to NixOS config. Fix by passing via extraSpecialArgs.
2026-06-16 19:08:15 -04:00
b9e89ce537 fix: use libraspberrypi instead of raspberrypi-tools for pinctrl 2026-06-16 19:07:34 -04:00
1d50b6455d fix: zsh conflicts for gortium home-manager on uConsole
- Remove duplicate .zshrc from home.file (managed by programs.zsh)
- Enable programs.zsh system-wide for gortium user
2026-06-16 19:06:44 -04:00
bcf924408b fix: remove programs.gitsign (not available in nixpkgs 25.11) 2026-06-16 19:06:11 -04:00
820de72c0f fix: remove duplicate ai-worker user definition in configuration.nix
ai-worker is now defined in users/ai-worker/ai-worker.nix module
2026-06-16 19:05:03 -04:00
6c08958730 fix: add ai-worker-restricted module to uConsole CM5 base modules
Required for services.aiWorkerAccess option used by users/ai-worker/ai-worker.nix
2026-06-16 19:04:35 -04:00
3f331e4bfb fix: add home-manager input for uConsole CM5 gortium user config
The remote branch added users/gortium/gortium.nix which uses
home-manager module option, but home-manager wasn't imported.
2026-06-16 19:03:59 -04:00
1550219e77 Merge remote changes + feat: AIO v2 board module
- Cross-compile overlays for Hyprland (libcamera, pipewire, gjs)
- Refactor uconsoleBaseModules into reusable list
- Add wireguard-client service module
- Restructure users into subdirectories
- New: hardware.uconsole-cm5-aio-v2 module (GPIO rails, aiov2_ctl, GPS UART)
- Update configuration.nix with Hyprland + AIO v2
- Add AIO v2 module to both toplevel and SD image config
2026-06-16 19:02:38 -04:00
2572f47e41 feat: add NixOS module for HackerGadgets AIO v2 board (uConsole CM5)
- New module: hardware.uconsole-cm5-aio-v2
  - GPIO rail control for GPS (27), LORA (16), SDR (7), USB (23)
  - Systemd oneshot service (aiov2-rails-boot) to apply states at boot
  - aiov2_ctl CLI tool packaged from GitHub source
  - GPS UART support (ttyAMA0, 9600 baud) with dialout group
  - Optional systemd user service for system tray GUI
- Wired into uconsole-cm5 NixOS config + SD image

All rails default OFF — activate on demand with:
  aiov2_ctl <GPS|LORA|SDR|USB> on
2026-06-16 19:00:50 -04:00
bd8b1c564e feat: add reusable wireguard-client NixOS module
- modules/nixos/services/wireguard-client.nix — optional module under
  gortium.wireguard-client namespace with enable, vpnIp, privateKeyFile,
  and presharedKeyFile options
- Added to lazyworkhorse, cyt-pi, and uconsoleBaseModules (covers both
  uconsole-cm5 toplevel and SD image)
- Migrated lazyworkhorse from inline networking.wireguard to module
- Split-tunnel: allowedIPs = [ "10.8.0.0/24" ]

Usage in a host config:
  gortium.wireguard-client = {
    enable = true;
    vpnIp = "10.8.0.X/24";
    privateKeyFile = config.age.secrets.wireguard_private_key.path;
    presharedKeyFile = config.age.secrets.wireguard_preshared_key.path;
  };
2026-06-15 10:55:40 -04:00
bd283de350 fix: place passwordFile at correct attrset level in gortium.nix 2026-06-14 21:58:57 -04:00
a6d88f2d41 Moved user ai-worker 2026-06-14 21:57:44 -04:00
6399196a2c fix: move gortium passwordFile to shared user module (applies to all hosts) 2026-06-14 21:55:48 -04:00
8651295b0a Fixed stuff maybe i guess not sure 2026-06-14 21:48:23 -04:00
e991359584 add gortium password age secret 2026-06-14 21:34:42 -04:00
fba52fa66d fix: use passwordFile instead of hashedPasswordFile (matches other secrets: plain text) 2026-06-14 21:09:10 -04:00
e95baddb96 rename users/gortium/default.nix -> gortium.nix, add to uconsole modules 2026-06-14 21:05:22 -04:00
eb3fe42542 refactor: extract shared uconsole modules to eliminate toplevel/image duplication 2026-06-14 20:56:17 -04:00
cdbb7de04d fix: properly structure uConsole config (ai-worker, gortium password, age secret) 2026-06-14 19:56:33 -04:00
9004163891 feat: add agenix secret for gortium password on uConsole
- Add gortium_password.age entry in secrets.nix
- Add age.secrets.gortium_password in uConsole config
- Add hashedPasswordFile to existing gortium user
- Add ai-worker user for Hermes SSH access
2026-06-14 19:53:40 -04:00
f06d9028f0 feat: add ai-worker user to uConsole for Hermes SSH access 2026-06-14 19:52:11 -04:00
8423a121eb rename host/ -> hosts/ in dotfiles submodule 2026-06-14 19:41:58 -04:00
7238e9d45c update dotfiles submodule: per-host hyprland config 2026-06-14 19:37:35 -04:00
f344739b94 feat: per-host Hyprland monitor config via home-manager
- Split hyprland.conf into common (keybinds, looks, animations)
  and per-host (monitors, env, workspaces) configs
- Add uconsole.conf for CM5 DSI display (720x1280)
- Add laptop.conf for NVIDIA + external monitors
- home.nix links the correct host config based on hostname
- Remove NVIDIA env vars from common config
2026-06-14 19:37:14 -04:00
02ffcdb55e feat: add dotfiles submodule and home-manager config
- Add dotfiles repo as submodule in assets/dotfiles/
- Rewrite home.nix with direct file references instead of stow service
- Remove old custom dotfiles.nix service (replaced by home-manager)
- Clean up services/default.nix import
2026-06-14 19:22:27 -04:00
ce7f74c66f remove hyperspace files accidentally committed from feat/hyperspace-pods-module
These files were mixed into commit 16acc6a which was intended
to only fix SSH options for the uConsole configuration.
2026-06-14 18:58:35 -04:00
f5d1732346 Merge remote-tracking branch 'origin/home_manager' into uconsole-cm5-incremental
# Conflicts:
#	flake.lock
#	flake.nix
#	modules/nixos/services/default.nix
2026-06-14 18:53:53 -04:00
2ee616839e chore: point nixos-uconsole input to pr/dcs-panel-detection branch 2026-06-14 18:43:04 -04:00
42202c8a40 fix: add hyprwayland-scanner native paths for xdg-desktop-portal-hyprland cross-compile 2026-06-14 10:11:44 -04:00
2476352fdf fix: skip hyprland qtutils (Qt6Quick missing in aarch64 cross-compile)
Qt6Quick and its submodules are not built in the aarch64 qtdeclarative
cross-compile output. hyprland-qt-support can't find them and fails.

Hyprland only needs qtutils at runtime (added to PATH via wrapProgram).
Setting wrapRuntimeDeps = false skips the wrapping entirely, letting
Hyprland build without its QML UI support package.
2026-06-14 10:01:28 -04:00
8afca7315d fix: correct qtdeclarative attr to qt6.qtdeclarative 2026-06-14 09:56:31 -04:00
0372b37950 fix: set Qt6Qml_DIR for hyprland-qt-support cross-compile 2026-06-14 09:52:35 -04:00
11a4969028 fix: skip GTK tests in gjs cross-compile for Hyprland 2026-06-14 09:30:50 -04:00
6a1c26cac2 fix: remove libcamera from pipewire buildInputs (both overlays)
meta.platforms = [] on libcamera doesn't help because nixos-25.11 pipewire
has libcamera unconditionally in buildInputs. Must overrideAttrs to:
- filter libcamera out of buildInputs
- clear existing libcamera meson flags and set -Dlibcamera=disabled
2026-06-14 09:03:44 -04:00
9978ea36f4 fix: disable libcamera in pipewire via mesonFlags for both pkgs and rpi 2026-06-14 00:56:31 -04:00
f00477dacc fix: force -Dlibcamera=disabled in pipewire mesonFlags for cross-compile 2026-06-14 00:16:09 -04:00
86d8b7bf8b fix: disable libcamera in pipewire for cross-compile (rpi-pisp blocks) 2026-06-13 23:45:58 -04:00
4610a08072 feat: add Hyprland Wayland compositor from archive/uconsole-cm5-v3 2026-06-13 23:26:15 -04:00
3f985c72de switch to gortium/nixos-uconsole fork 2026-06-13 23:15:53 -04:00
67aafe37db fix: add DSI_INIT0 lane config to old panel init seq + fix mode_flags 2026-06-13 18:59:29 -04:00
cb2b535fde fix: correct patch format — blank line between hunks 2026-06-13 18:51:39 -04:00
2c9136d1dc fix: add DSI_INIT0 lane config to old panel init_sequence + fix mode_flags 2026-06-13 18:49:36 -04:00
6fac886598 revert: remove failed CWU50 display fix patches 2026-06-13 18:04:06 -04:00
a4f4891236 try: no-burst-no-sync-pulse (VIDEO only) 2026-06-13 17:12:51 -04:00
3a809938c9 try: no-sync-pulse variant (keep BURST, remove SYNC_PULSE) 2026-06-13 16:38:24 -04:00
0f765d99cb feat: add CWU50 display patch (no-burst) + fix flake syntax
Remove extra '};' that broke flake.nix parsing.
Apply kernel patch '0008-panel-cwu50-no-burst.patch' to remove
MIPI_DSI_MODE_VIDEO_BURST flag in panel-cwu50.c.
Switch nixos-uconsole module to consolidated uconsole-cm5 module.
Keep patches/0008-panel-cwu50-remove-sync-pulse.patch as variant.
2026-06-13 16:27:32 -04:00
4d8087badf fix: apply DSI burst mode fix as kernel patch overlay 2026-06-13 13:47:35 -04:00
6543de3a45 fix: correct sd-image module to nixosModules.sd-image 2026-06-12 21:57:49 -04:00
0db8071300 fix: correct sd-image module path 2026-06-12 21:56:22 -04:00
9038863728 fix: remove dead rpi-pkgs line 2026-06-12 21:48:48 -04:00
7e3b2520eb fix: use nixos-raspberrypi.lib.nixosSystem + sd-image module directly 2026-06-12 21:47:29 -04:00
80efb68428 feat(uconsole): add flashable SD image package (SSH+WiFi+keys) 2026-06-12 21:42:51 -04:00
3d86af76b9 fix: remove non-existent ssh opts for nixpkgs-25.11 2026-06-12 20:55:42 -04:00
656570b39e fix: use plain string for bootloader setting 2026-06-12 20:54:19 -04:00
8b6990ceee deploy1(uconsole): revert rasberry-pi-5.base removal — keep minimal SSH+WiFi config 2026-06-12 20:52:11 -04:00
a312c29221 fix: remove boot.loader.raspberry-pi reference (option removed with rasberry-pi-5.base) 2026-06-12 20:48:30 -04:00
053dd535d3 deploy1(uconsole): minimal config — no rasberry-pi-5.base, just SSH + WiFi + keys 2026-06-12 20:47:11 -04:00
35e4155b8c fix(uconsole): remove configtxt module (conflicting overlays) — use extra-config only 2026-06-12 20:20:39 -04:00
e8218c322a fix(uconsole): set ignore_lcd=0 + disable conflicting dt-overlays 2026-06-12 20:19:21 -04:00
931ed2ac27 fix(uconsole): clean config.txt — clear conflicting defaults, single [pi5] section 2026-06-12 20:16:50 -04:00
052081616c test: remove self.submodules to check Lix compatibility 2026-06-12 19:24:43 -04:00
d3d7cdff44 Revert "fix: remove self.submodules (not supported by Lix)"
This reverts commit 5202bc1fcb.
2026-06-12 18:59:04 -04:00
5202bc1fcb fix: remove self.submodules (not supported by Lix) 2026-06-12 18:56:44 -04:00
9319e32683 fix(uconsole): cross-compile Lix instead of using native aarch64 flake package 2026-06-12 18:41:44 -04:00
7da46d5769 refactor(uconsole): use standard inject-overlays helpers instead of manual overlay list 2026-06-12 18:21:45 -04:00
8ea6be7ac1 fix: remove rpi-cross-overlay import from uconsole-cm5 modules 2026-06-12 17:11:17 -04:00
b455bf6866 chore: remove rpi-cross-overlay — fork nixpkgs-rpi.nix already handles cross-compile 2026-06-12 17:10:19 -04:00
ce7c594562 feat: enable ca-derivations experimental feature on lazyworkhorse 2026-06-12 16:50:16 -04:00
eb5e64ec67 Revert "chore: ignore hyperspace files from feat/hyperspace-pods-module"
This reverts commit ec44012a64.
2026-06-12 16:47:15 -04:00
ec44012a64 chore: ignore hyperspace files from feat/hyperspace-pods-module 2026-06-12 16:46:17 -04:00
16acc6a153 fix(uconsole): resolve conflicting SSH options + properly override nixos-uconsole's nixos-raspberrypi input
- mkForce on PermitRootLogin and PasswordAuthentication
- nixos-uconsole.inputs.nixos-raspberrypi follows our fork
2026-06-12 16:43:33 -04:00
5ee644e9dd feat(uconsole): add rpi-cross-overlay module + Lix
- rpi-cross-overlay.nix: override pkgs.rpi with cross-compilation
  when buildPlatform != hostPlatform (0 QEMU)
- Lix nix daemon for uConsole (aarch64-linux)
- Remove broken inline overlay from flake.nix
2026-06-12 16:36:49 -04:00
efc50d23c4 Added home wifi infos 2026-06-12 16:19:57 -04:00
a527b65eae fix(uconsole): rename secret to home_wifi (shared across hosts, not uconsole-specific) 2026-06-12 16:17:48 -04:00
698d3f91eb feat(uconsole): add agenix secret for WiFi credentials
- age.secrets.uconsole-wifi (SSID+password in encrypted file)
- systemd service ensure-wifi reads decrypted secret and configures NM
- agenix.nixosModules.default imported for uconsole-cm5
- uconsole-wifi.age declared in secrets/secrets.nix
2026-06-12 16:15:37 -04:00
1f99ca0d37 feat(uconsole): add cm5 cross-compiled nixosConfiguration
- New host: uconsole-cm5 (aarch64-linux, cross-built from x86_64)
- SSH authorizedKeys: gortium.main + ai-worker.main
- NetworkManager enabled (WiFi password via agenix later)
- Display: vc4/panel_cwu50/rp1_dsi with empty initrd
- Config.txt [pi5] section (not [cm5])
- Backlight fix service
- nixos-raspberrypi → gortium/cm5-cross-v1 fork (PR #197)
- nixpkgs-uconsole pinned to nixos-25.11 (kernel patch compat)

V3 branch saved as archive/uconsole-cm5-v3 (Reticulum/SDR/HAM config).
2026-06-12 16:02:13 -04:00
36359de6aa Merge pull request 'feat: add Syncthing firewall port and update compose submodule' (#47) from feat/syncthing-org-sync into master
Reviewed-on: #47
2026-05-19 00:34:42 +00:00
Robert
10b8565fd6 Merge branch 'master' into feat/syncthing-org-sync 2026-05-18 20:33:29 -04:00
Robert
f672696b8e Update submodule for syncthing 2026-05-18 20:31:07 -04:00
0980dca455 fix: update compose submodule to Traefik-routed Syncthing 2026-05-14 21:40:12 -04:00
96bc20ab70 feat: add Syncthing firewall port and update compose submodule 2026-05-14 21:36:26 -04:00
670ae4f002 Merge pull request 'fix: update compose submodule — use ln -sf for iptables-nft' (#46) from fix/vpn-iptables-nft-v3 into master
Reviewed-on: #46
2026-05-13 17:00:16 +00:00
f785abfd49 fix: update compose submodule — use ln -sf for iptables-nft 2026-05-13 12:59:04 -04:00
6f44aa7f76 Merge pull request 'fix: update compose submodule — remove apk add iptables-nft' (#45) from fix/vpn-iptables-nft-v2 into master
Reviewed-on: #45
2026-05-13 16:49:39 +00:00
8d40f1691f fix: update compose submodule — remove apk add iptables-nft 2026-05-13 12:49:14 -04:00
Robert
2dd2e64986 Merge remote-tracking branch 'origin/master' 2026-05-13 12:42:54 -04:00
Robert
23fc5e0597 Give a little more ssh room for tramp 2026-05-13 12:41:09 -04:00
0c9c33d735 Merge pull request 'fix: update wg-easy to official ghcr image with iptables-nft' (#44) from fix/vpn-iptables-nft-upstream into master
Reviewed-on: #44
2026-05-13 16:39:56 +00:00
0bb6890f1c chore: merge master into branch 2026-05-13 12:39:05 -04:00
9d5434425f fix: update compose submodule for wg-easy iptables-nft fix
Updates the assets/compose submodule to point to the fix/vpn-iptables-nft-upstream
branch which contains:
- Switch FROM weejewel/wg-easy:latest (Alpine 3.11, stale 4yr) to
  ghcr.io/wg-easy/wg-easy:latest (actively maintained, Alpine krypton)
- Use update-alternatives instead of raw ln -sf to flip iptables
  from legacy to nftables backend
- Fix compose build context: ./vpn -> . (Dockerfile is at same level)
2026-05-13 12:30:47 -04:00
1fb4320dd1 Merge pull request 'feat: update compose submodule for custom tools startup' (#43) from feat/update-compose-submodule-custom-tools into master
Reviewed-on: #43
2026-05-13 13:58:27 +00:00
51e9f47fd4 feat: update compose submodule for custom tools startup 2026-05-13 09:56:24 -04:00
06b3eb840f fix: update compose submodule for wg-easy iptables-nft fix 2026-05-12 16:29:51 -04:00
28ab52209c Merge pull request 'Add restricted AI worker access with deployment capabilities' (#1) from ai-worker-restricted-access into master
Reviewed-on: #1
2026-05-11 00:48:29 +00:00
Robert
e6f7f0c263 Merge branch 'ai-worker-restricted-access' of ssh://code.lazyworkhorse.net:2222/gortium/infra into ai-worker-restricted-access 2026-05-10 18:06:46 -04:00
Robert
5c136e0765 Merge remote-tracking branch 'origin/master' into ai-worker-restricted-access 2026-05-10 17:06:58 -04:00
Robert
f722af7803 New ollama model creator module version 2026-05-10 16:56:09 -04:00
Robert
c07debf088 Added wireguard keys 2026-05-10 16:51:32 -04:00
6806898f04 feat: update compose submodule for ollama-gfx906 (v0.23.2) + add ollama Dockerfile 2026-05-10 10:12:34 -04:00
96e77c5ef2 Revert "feat: add ai-optimizer benchmark plan and state tracking for ollama GPU benchmarking"
This reverts commit ff7303cf6a.
2026-05-09 20:19:26 +00:00
ff7303cf6a feat: add ai-optimizer benchmark plan and state tracking for ollama GPU benchmarking 2026-05-09 20:13:08 +00:00
9e42f5d2cc Merge pull request 'feat(hermes): update compose submodule for Piper TTS' (#34) from feat/voice-support-v2 into master
Reviewed-on: #34
2026-05-09 19:40:05 +00:00
614883f3c3 fix: update compose submodule - startup permission fix for data volume 2026-05-09 16:04:39 +00:00
374d022593 fix: update compose submodule - permission fix for atomic writes 2026-05-09 15:50:42 +00:00
9679846cdb feat: update compose submodule - Ryan high voice 2026-05-09 15:21:59 +00:00
4056f91ec6 fix: update compose submodule - remove patch step 2026-05-09 14:28:44 +00:00
1ba7d31d2f fix: update compose submodule - patch path fix 2026-05-09 14:27:16 +00:00
c7e9f8a1e0 feat: update compose submodule for Norman voice 2026-05-09 14:20:55 +00:00
bbe1a4a850 fix: update compose submodule - ca-certificates 2026-05-09 14:15:01 +00:00
2b8316060c fix: update compose submodule - COPY path fix 2026-05-09 14:12:15 +00:00
cc2c62faf7 fix: update compose submodule - Dockerfile heredoc fix 2026-05-09 14:09:59 +00:00
47f1ba6cf2 fix: update compose submodule - clean patch script 2026-05-09 13:59:17 +00:00
db89881d75 fix: update compose submodule for full OPENROUTER_API_KEY 2026-05-09 13:55:47 +00:00
0bb0a270e6 fix: update compose submodule for clean Piper Dockerfile 2026-05-09 13:42:02 +00:00
41256ccbde fix: update compose submodule for Piper TTS (replaces Coqui/ROCm) 2026-05-09 13:24:17 +00:00
e551f0e5c5 feat: update compose submodule for ROCm + Coqui TTS Dockerfile 2026-05-09 04:10:05 +00:00
b11d599f37 fix: update compose submodule for simplified Dockerfile 2026-05-09 02:38:41 +00:00
782f2fa9ed feat(hermes): update compose submodule for ROCm GPU voice STT support 2026-05-09 00:22:08 +00:00
2e14069584 Merge pull request 'feat: add WireGuard VPN stack' (#33) from feat/wireguard-vpn into master
Reviewed-on: #33
2026-05-09 00:13:36 +00:00
c53460c400 fix: remove dns option from wireguard config (not a valid nixos option) 2026-05-05 03:26:44 +00:00
Robert
ee96593e3d Merge branch 'feat/wireguard-vpn' of ssh://code.lazyworkhorse.net:2222/gortium/infra into feat/wireguard-vpn 2026-05-04 23:22:35 -04:00
Robert
030125ab01 Added wireguard pass 2026-05-04 23:21:36 -04:00
Robert
5935747902 Security fixes 2026-05-04 23:20:57 -04:00
Robert
9ae0f6ad62 Submodule update 2026-05-04 23:20:03 -04:00
5c481d664a fix: split tunnel on host VPN - only route 10.8.0.0/24 2026-05-05 02:41:29 +00:00
94a7c7195a fix: remove exposed keys from comments 2026-05-05 02:12:55 +00:00
cf279c4fb0 feat: add host-level WireGuard client via networking.wireguard
- Add wg0 interface config with agenix-managed secrets
- Revert compose submodule to remove NET_ADMIN from Hermes
- WireGuard runs at host level, all containers inherit the tunnel
2026-05-05 02:11:41 +00:00
b9289a149d chore: update compose submodule for Hermes NET_ADMIN + WireGuard Dockerfile 2026-05-05 01:48:24 +00:00
e0068260cb chore: move Hermes Dockerfile to compose repo, add WireGuard tools
- Move Dockerfile.full from infra/docker/hermes to compose/ai/Dockerfile
- Add wireguard-tools and openresolv to Hermes image
- Remove stray docker/hermes directory from infra
2026-05-05 01:43:42 +00:00
a42b2ff65d chore: update compose submodule to wireguard-vpn (fix ref) 2026-05-05 01:21:34 +00:00
92bcf1cc04 chore: update compose submodule to wireguard-vpn 2026-05-05 01:21:19 +00:00
7d0b72a513 chore: update compose submodule to linuxserver/wireguard 2026-05-05 01:18:13 +00:00
48245518a1 fix: load iptables kernel modules for WireGuard NAT
wg-easy needs iptable_nat and iptable_filter to set up
masquerading for VPN traffic. These modules must be loaded
at boot for the container to access iptables.
2026-05-05 01:17:14 +00:00
1673a56439 feat: add WireGuard VPN stack
- Add vpn stack to services.dockerStacks
- Open UDP port 51820 for WireGuard protocol
- Update compose submodule to include vpn stack
2026-05-04 22:49:06 +00:00
Robert
7d3d072961 Merge branch 'master' into ai-worker-restricted-access 2026-05-03 05:28:39 -04:00
4cceab05d0 Merge pull request 'security: harden lazyworkhorse with firewall, fail2ban, SSH hardening' (#28) from feature/server-hardening-clean into master
Reviewed-on: #28
2026-05-03 09:11:56 +00:00
Robert
bcf5cadaa0 olllama template fix to remove currenttime 2026-04-30 21:54:47 -04:00
f0e21d95e4 fix: ai-worker docker-only access for ollama benchmarking
Remove infra repo bind mount and sudo access from ai-worker user.
Now ai-worker can only:
- SSH into host from Hermes container
- Run docker commands via docker group membership
- Execute ollama benchmarks via docker exec

Results saved to /opt/data/ai-optimizer/ in Hermes container.
2026-04-29 19:55:19 +00:00
18df45819d Add restricted AI worker access with deployment capabilities
- New module: modules/nixos/security/ai-worker-restricted.nix
  - Bind mount for infra repo access (RW)
  - Whitelisted sudo commands: nh, nixos-rebuild, nixpkgs-fmt, nix
  - Audit logging for infra changes
  - Documentation in README-ai-worker.md

- Updated users/ai-worker.nix:
  - Enable services.aiWorkerAccess
  - Lock password (SSH key only)
  - Security documentation comments

- Updated flake.nix:
  - Include new security module

SECURITY: AI must ask for user confirmation before running nh os switch
2026-04-28 15:34:38 +00:00
1ca58d3da3 Merge branch 'master' into home_manager 2025-08-24 22:26:12 -04:00
0ca7a74653 WIP on home manager 2025-08-19 17:32:38 -04:00
28 changed files with 1312 additions and 187 deletions

4
.gitmodules vendored
View File

@@ -1,3 +1,7 @@
[submodule "assets/compose"]
path = assets/compose
url = ssh://git@code.lazyworkhorse.net:2222/gortium/compose.git
[submodule "assets/dotfiles"]
path = assets/dotfiles
url = ssh://git@code.lazyworkhorse.net:2222/gortium/dotfiles.git
branch = master

1
assets/dotfiles Submodule

Submodule assets/dotfiles added at f45387456b

View File

@@ -1,71 +0,0 @@
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source
FROM tianon/gosu:1.19-trixie@sha256:3b176695959c71e123eb390d427efc665eeb561b1540e82679c15e992006b8b9 AS gosu_source
FROM debian:13.4
# Disable Python stdout buffering to ensure logs are printed immediately
ENV PYTHONUNBUFFERED=1
# Store Playwright browsers outside the volume mount so the build-time
# install survives the /opt/data volume overlay at runtime.
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
# Install system dependencies in one layer, clear APT cache
# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.)
# that would otherwise accumulate when hermes runs as PID 1. See #15012.
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \
curl poppler-utils imagemagick \
chromium xvfb fonts-noto-color-emoji fonts-unifont fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf \
libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \
texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \
qemu-user-static binfmt-support qemu-user-binfmt \
emacs-nox \
libportaudio2 && \
rm -rf /var/lib/apt/lists/*
# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime
RUN useradd -u 10000 -m -d /opt/data hermes
COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/
COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
WORKDIR /opt/hermes
# ---------- Layer-cached dependency install ----------
# Copy only package manifests first so npm install + Playwright are cached
# unless the lockfiles themselves change.
COPY package.json package-lock.json ./
COPY web/package.json web/package-lock.json web/
RUN npm install --prefer-offline --no-audit && \
npx playwright install --with-deps chromium --only-shell && \
(cd web && npm install --prefer-offline --no-audit) && \
npm cache clean --force
# ---------- Source code ----------
# .dockerignore excludes node_modules, so the installs above survive.
COPY --chown=hermes:hermes . .
# Build web dashboard (Vite outputs to hermes_cli/web_dist/)
RUN cd web && npm run build
# ---------- Permissions ----------
# Make install dir world-readable so any HERMES_UID can read it at runtime.
# The venv needs to be traversable too.
USER root
RUN chmod -R a+rX /opt/hermes
# Start as root so the entrypoint can usermod/groupmod + gosu.
# If HERMES_UID is unset, the entrypoint drops to the default hermes user (10000).
# ---------- Python virtualenv ----------
RUN uv venv && \
uv pip install --no-cache-dir -e ".[all]" && \
uv pip install --no-cache-dir sounddevice numpy faster-whisper
# ---------- Runtime ----------
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
ENV HERMES_HOME=/opt/data
ENV PATH="/opt/data/.local/bin:${PATH}"
VOLUME [ "/opt/data" ]
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ]

148
flake.lock generated
View File

@@ -23,6 +23,22 @@
"type": "github"
}
},
"argononed": {
"flake": false,
"locked": {
"lastModified": 1729566243,
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
"owner": "nvmd",
"repo": "argononed",
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
"type": "github"
},
"original": {
"owner": "nvmd",
"repo": "argononed",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
@@ -37,6 +53,21 @@
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
}
},
"flake-compat_2": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -58,6 +89,27 @@
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixpkgs-uconsole"
]
},
"locked": {
"lastModified": 1779506708,
"narHash": "sha256-QOD/CNm196nCJRheux/URi4/HE66fthdOMqCJoPP1Y0=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "3ee51fbdac8c8bdfe1e7e1fcaba6520a563f394f",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.11",
"repo": "home-manager",
"type": "github"
}
},
"lix": {
"inputs": {
"flake-compat": "flake-compat",
@@ -144,6 +196,80 @@
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-stable": [
"nixos-raspberrypi",
"nixpkgs"
],
"nixos-unstable": [
"nixos-raspberrypi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747747741,
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
"owner": "nvmd",
"repo": "nixos-images",
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "sdimage-installer",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-raspberrypi": {
"inputs": {
"argononed": "argononed",
"flake-compat": "flake-compat_2",
"nixos-images": "nixos-images",
"nixpkgs": [
"nixpkgs-uconsole"
]
},
"locked": {
"lastModified": 1781324200,
"narHash": "sha256-JWqxN2Yle86+4Q+GFh12SvB92ZyLeqalVsN9lfMh6eQ=",
"owner": "gortium",
"repo": "nixos-raspberrypi",
"rev": "721a6e9e67dca3a23133db650b87018646bca3e6",
"type": "github"
},
"original": {
"owner": "gortium",
"ref": "cm5-cross-v1",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixos-uconsole": {
"inputs": {
"nixos-raspberrypi": [
"nixos-raspberrypi"
],
"nixpkgs": [
"nixpkgs-uconsole"
]
},
"locked": {
"lastModified": 1781476310,
"narHash": "sha256-jY6ujqLXNAWJGvt+pAuw1Wg/OiHRGd1B1Z7Czhiq7Q4=",
"owner": "gortium",
"repo": "nixos-uconsole",
"rev": "38a7fcbffbf2d2e122bc1e1c634fe25f66ecda13",
"type": "github"
},
"original": {
"owner": "gortium",
"ref": "pr/dcs-panel-detection",
"repo": "nixos-uconsole",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705033721,
@@ -176,6 +302,22 @@
"type": "github"
}
},
"nixpkgs-uconsole": {
"locked": {
"lastModified": 1780952837,
"narHash": "sha256-Fwd1+spDtQ0hDyBwme6ufG3n4mY0UrjjFdYHv+G/Hds=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e820eb4a444b46a19b2e03e8dfd2359439ff30fe",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1774386573,
@@ -211,8 +353,12 @@
"root": {
"inputs": {
"agenix": "agenix",
"home-manager": "home-manager_2",
"lix": "lix",
"nixpkgs": "nixpkgs_2"
"nixos-raspberrypi": "nixos-raspberrypi",
"nixos-uconsole": "nixos-uconsole",
"nixpkgs": "nixpkgs_2",
"nixpkgs-uconsole": "nixpkgs-uconsole"
}
},
"systems": {

207
flake.nix
View File

@@ -12,10 +12,26 @@
url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
self.submodules = true;
nixpkgs-uconsole.url = "github:NixOS/nixpkgs/nixos-25.11";
nixos-uconsole = {
url = "github:gortium/nixos-uconsole/pr/dcs-panel-detection";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
inputs.nixos-raspberrypi.follows = "nixos-raspberrypi";
};
nixos-raspberrypi = {
url = "github:gortium/nixos-raspberrypi/cm5-cross-v1";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
};
outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
outputs = { self, nixpkgs, agenix, lix
, nixpkgs-uconsole, nixos-uconsole, nixos-raspberrypi
, home-manager
, ... }@inputs:
let
system = "x86_64-linux";
keys = import ./lib/keys.nix;
@@ -30,56 +46,151 @@
pkgs = import nixpkgs {
inherit system overlays;
config.allowUnfree = true;
config.permittedInsecurePackages = [
"openclaw-2026.3.12"
];
config.permittedInsecurePackages = [ "openclaw-2026.3.12" ];
};
devShell = import ./shells/nix_dev.nix {
inherit pkgs system agenix;
};
in
{
nixosConfigurations = {
lazyworkhorse = nixpkgs.lib.nixosSystem {
specialArgs = { inherit system self keys paths inputs; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.config.rocmSupport = true;
nixpkgs.config.permittedInsecurePackages = [
"openclaw-2026.3.12"
];
nix.package = lix.packages.${system}.default;
}
agenix.nixosModules.default
./hosts/lazyworkhorse/configuration.nix
./hosts/lazyworkhorse/hardware-configuration.nix
./modules/nixos/filesystem/hoardingcow-mount.nix
./modules/nixos/services/docker_manager.nix
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/openclaw_node.nix
./users/gortium.nix
./users/ai-worker.nix
];
};
cyt-pi = nixpkgs.lib.nixosSystem {
specialArgs = { inherit self keys paths inputs; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-linux";
nix.package = lix.packages."aarch64-linux".default;
}
./hosts/cyt-pi/configuration.nix
./hosts/cyt-pi/hardware-configuration.nix
];
};
};
devShells.${system}.default = devShell;
# Cross-compile overlay fixes for Hyprland and deps on aarch64
uconsoleCrossOverlay = final: prev: {
libcamera = prev.libcamera.overrideAttrs (_: { meta.platforms = []; });
libcamera-rpi = prev.libcamera-rpi.overrideAttrs (_: { meta.platforms = []; });
libpisp = prev.libpisp.overrideAttrs (_: { meta.platforms = []; });
pipewire = prev.pipewire.overrideAttrs (old: {
buildInputs = builtins.filter
(x: !(x?pname && x.pname == "libcamera"))
(old.buildInputs or []);
mesonFlags = builtins.filter
(flag: !(builtins.isString flag && builtins.match ".*libcamera.*" flag != null))
(old.mesonFlags or []) ++ [ "-Dlibcamera=disabled" ];
});
gjs = prev.gjs.overrideAttrs (old: {
mesonFlags = (old.mesonFlags or []) ++ [ "-Dskip_gtk_tests=true" ];
});
hyprland = prev.hyprland.override { wrapRuntimeDeps = false; };
xdg-desktop-portal-hyprland = prev.xdg-desktop-portal-hyprland.overrideAttrs (old: {
preConfigure = (old.preConfigure or "") + ''
cmakeFlags="$cmakeFlags -Dhyprwayland-scanner_DIR=${prev.buildPackages.hyprwayland-scanner}/lib/cmake/hyprwayland-scanner" 2>/dev/null || true
export PKG_CONFIG_PATH="${prev.buildPackages.hyprwayland-scanner}/lib/pkgconfig:$PKG_CONFIG_PATH"
'';
});
};
# RPI-specific pipewire libcamera fix (separate nixpkgs instance)
uconsoleRpiPipewireOverlay = final: prev: {
pipewire = prev.pipewire.overrideAttrs (old: {
buildInputs = builtins.filter
(x: !(x?pname && x.pname == "libcamera"))
(old.buildInputs or []);
mesonFlags = builtins.filter
(flag: !(builtins.isString flag && builtins.match ".*libcamera.*" flag != null))
(old.mesonFlags or []) ++ [ "-Dlibcamera=disabled" ];
});
};
# Shared uConsole CM5 module set — used by both toplevel and SD image
uconsoleBaseModules = [
{
nixpkgs.buildPlatform = "x86_64-linux";
nixpkgs.hostPlatform = "aarch64-linux";
nixpkgs.config.allowUnfree = true;
boot.loader.raspberry-pi.bootloader = "kernel";
nixpkgs.overlays = [ uconsoleCrossOverlay ];
}
nixos-raspberrypi.nixosModules.nixpkgs-rpi
({ config, lib, pkgs, ... }: {
nixpkgs.overlays = [ uconsoleRpiPipewireOverlay ];
})
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-raspberrypi.lib.inject-overlays
nixos-raspberrypi.lib.inject-overlays-global
nixos-uconsole.nixosModules.uconsole-cm5
./modules/nixos/hardware/uconsole-cm5-aio-v2.nix
# Cross-compiled Lix for uConsole
({ config, lib, pkgs, inputs, ... }: let
lixCross = import inputs.nixpkgs-uconsole {
localSystem = { system = "x86_64-linux"; };
crossSystem = { system = "aarch64-linux"; };
overlays = [ inputs.lix.overlays.default ];
};
in { nix.package = lixCross.lix; })
inputs.home-manager.nixosModules.home-manager
agenix.nixosModules.default
./hosts/uconsole-cm5/configuration.nix
./hosts/uconsole-cm5/hardware-configuration.nix
./modules/nixos/services/wireguard-client.nix
./modules/nixos/security/ai-worker-restricted.nix
./users/gortium/gortium.nix
./users/ai-worker/ai-worker.nix
];
in {
nixosConfigurations = {
lazyworkhorse = nixpkgs.lib.nixosSystem {
specialArgs = { inherit system self keys paths inputs; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.config.rocmSupport = true;
nixpkgs.config.permittedInsecurePackages = [ "openclaw-2026.3.12" ];
nix.package = lix.packages.${system}.default;
}
inputs.home-manager.nixosModules.home-manager
agenix.nixosModules.default
./hosts/lazyworkhorse/configuration.nix
./hosts/lazyworkhorse/hardware-configuration.nix
./modules/nixos/filesystem/hoardingcow-mount.nix
./modules/nixos/services/docker_manager.nix
./modules/nixos/services/wireguard-client.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/security/ai-worker-restricted.nix
./users/gortium/gortium.nix
./users/ai-worker/ai-worker.nix
];
};
cyt-pi = nixpkgs.lib.nixosSystem {
specialArgs = { inherit self keys paths inputs; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-linux";
nix.package = lix.packages."aarch64-linux".default;
}
./hosts/cyt-pi/configuration.nix
./hosts/cyt-pi/hardware-configuration.nix
./modules/nixos/services/wireguard-client.nix
./users/gortium/gortium.nix
];
};
uconsole-cm5 = nixpkgs-uconsole.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self keys paths inputs;
nixos-raspberrypi = nixos-raspberrypi;
isCM4 = false;
};
modules = uconsoleBaseModules;
};
};
devShells.${system}.default = devShell;
packages.${system} = {
uconsole-cm5-image = (nixos-raspberrypi.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = {
inherit self keys inputs;
nixos-raspberrypi = nixos-raspberrypi;
isCM4 = false;
};
modules = uconsoleBaseModules ++ [
nixos-raspberrypi.nixosModules.sd-image
];
}).config.system.build.sdImage;
};
};
}

View File

@@ -9,7 +9,7 @@
hoardingcow-mount.enable = true;
# Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ];
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" "ca-derivations" ];
nix.settings.trusted-users = [ "root" "gortium" ];
# Garbage collection
@@ -36,7 +36,7 @@
"transparent_hugepage=always" # because mucho ram
];
# 2. Load the specific drivers found by sensors-detect
boot.kernelModules = [ "nct6775" "lm96163" ];
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" ];
# 3. Force the nct6775 driver to recognize the chip if it's stubborn
boot.extraModprobeConfig = ''
options nct6775 force_id=0xd280
@@ -49,6 +49,14 @@
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.hostId = "deadbeef";
# WireGuard VPN client -- module, always up, connects to wg-easy server
gortium.wireguard-client = {
enable = true;
vpnIp = "10.8.0.3/24";
privateKeyFile = config.age.secrets.wireguard_private_key.path;
presharedKeyFile = config.age.secrets.wireguard_preshared_key.path;
};
# Set your time zone.
time.timeZone = "America/Montreal";
@@ -187,6 +195,7 @@
ai = {
path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
};
cloudstorage = {
@@ -221,6 +230,11 @@
path = self + "/assets/compose/homepage";
};
vpn = {
path = self + "/assets/compose/vpn";
envFile = config.age.secrets.containers_env.path;
};
# tak = {
# path = self + "/assets/compose/tak";
# };
@@ -264,6 +278,20 @@
mode = "0440";
path = "/run/secrets/openclaw_gateway_token";
};
wireguard_private_key = {
file = ../../secrets/wireguard_private_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_private_key";
};
wireguard_preshared_key = {
file = ../../secrets/wireguard_preshared_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_preshared_key";
};
};
};
@@ -332,14 +360,17 @@
];
allowedUDPPorts = [
# Add UDP ports if required
51820 # WireGuard VPN
];
# Rate limiting and attack prevention
extraCommands = ''
# Rate limit SSH connections (max 4 new connections per 60 seconds)
# 1. Wipe the INPUT chain clean at the start of every activation
iptables -F INPUT
# Rate limit SSH connections (max 20 new connections per 60 seconds)
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
iptables -A INPUT -p tcp --dport 2424 -m state --state NEW -m recent --update --seconds 60 --hitcount 20 -j DROP
# Rate limit HTTP/HTTPS (protects Traefik)
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
@@ -350,6 +381,10 @@
# Log dropped packets (rate limited)
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPTables-Dropped: " --log-level 4
# 3. CRITICAL: Re-link the NixOS default firewall chain
# Without this line, the 'allowedTCPPorts' in your Nix config will be ignored!
iptables -A INPUT -j nixos-fw
'';
};
@@ -428,7 +463,7 @@
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 5;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;

View File

@@ -0,0 +1,58 @@
{ config, lib, pkgs, keys, ... }:
{
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
system.stateVersion = "25.11";
# SSH — root access avec clés gortium + ai-worker
services.openssh = {
enable = true;
settings = {
PermitRootLogin = lib.mkForce "prohibit-password";
PasswordAuthentication = lib.mkForce false;
};
};
users.users.root.openssh.authorizedKeys.keys = with keys; [
users.gortium.main
users.ai-worker.main
];
# AI worker user (Hermes SSH access)
# Age secret for gortium password (file created by user)
age.secrets.gortium_password = {
file = ../../secrets/gortium_password.age;
};
# Password file for gortium (merges with users/gortium/default.nix)
# WiFi via NetworkManager + secret agenix
networking.networkmanager.enable = true;
# Firmware
hardware.enableRedistributableFirmware = true;
# Hyprland Wayland compositor (manual start — no SDDM)
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
# HackerGadgets AIO v2 board
hardware.uconsole-cm5-aio-v2 = {
enable = true;
# Rails actifs au boot
bootRails = {
GPS = false;
LORA = false;
SDR = false;
USB = false;
};
enableGPS = false;
};
}

View File

@@ -0,0 +1,30 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" "sdhci_pci" "nvme" ];
boot.initrd.kernelModules = [ ];
boot.extraModulePackages = [ ];
# SD card partitions (nixos-uconsole layout)
fileSystems."/" = {
device = "/dev/disk/by-label/NIXOS_SD";
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot/firmware" = {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -0,0 +1,169 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.hardware.uconsole-cm5-aio-v2;
# GPIO pin map matching the AIO v2 board hardware
# SDR (RTL-SDR): GPIO 7
# LoRa (SX1262) : GPIO 16
# USB Hub Interne: GPIO 23
# GPS (GNSS) : GPIO 27
gpioMap = {
GPS = 27;
LORA = 16;
SDR = 7;
USB = 23;
};
# Generate a script that applies boot rail states via pinctrl
applyRailsScript = pkgs.writeShellScript "apply-aio-v2-rails" (
''
set -e
PINCTRL=${pkgs.libraspberrypi}/bin/pinctrl
''
+ concatStringsSep "" (mapAttrsToList (name: pin: ''
if [ "${if cfg.bootRails.${name} then "1" else "0"}" = "1" ]; then
echo "AIO v2: ${name} (GPIO${toString pin}) -> ON"
$PINCTRL set ${toString pin} op dh
else
echo "AIO v2: ${name} (GPIO${toString pin}) -> OFF"
$PINCTRL set ${toString pin} op dl
fi
'') gpioMap)
);
# aiov2_ctl CLI tool -- fetched from GitHub, available as `aiov2_ctl`
aiov2CtlPkg = pkgs.stdenv.mkDerivation rec {
pname = "aiov2_ctl";
version = "0-unstable-2026-06-16";
src = pkgs.fetchFromGitHub {
owner = "hackergadgets";
repo = "aiov2_ctl";
rev = "main";
hash = "sha256-hqOvS1K5pDVXAroUE50i5R9YqRgC2U3fzby6uuB67K0=";
};
dontUnpack = true;
installPhase = ''
mkdir -p $out/bin $out/share/aiov2_ctl/img
cp $src/aiov2_ctl.py $out/bin/aiov2_ctl
chmod +x $out/bin/aiov2_ctl
patchShebangs $out/bin/aiov2_ctl
substituteInPlace $out/bin/aiov2_ctl \
--replace-fail '"/usr/local/share/aiov2_ctl/img/' '"'$out'/share/aiov2_ctl/img/'
cp -r $src/img/* $out/share/aiov2_ctl/img/
'';
meta = {
description = "HackerGadgets uConsole AIO v2 GPIO control and telemetry tool";
homepage = "https://github.com/hackergadgets/aiov2_ctl";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ ];
platforms = [ "aarch64-linux" ];
};
};
in {
options.hardware.uconsole-cm5-aio-v2 = {
enable = mkEnableOption "HackerGadgets uConsole AIO v2 board support";
bootRails = {
GPS = mkOption {
type = types.bool;
default = false;
description = "Enable GPS module at boot (GPIO 27)";
};
LORA = mkOption {
type = types.bool;
default = false;
description = "Enable LoRa module at boot (GPIO 16)";
};
SDR = mkOption {
type = types.bool;
default = false;
description = "Enable SDR module at boot (GPIO 7)";
};
USB = mkOption {
type = types.bool;
default = false;
description = "Enable internal USB hub at boot (GPIO 23)";
};
};
package = mkOption {
type = types.package;
default = aiov2CtlPkg;
defaultText = literalExpression "aiov2CtlPkg";
description = "aiov2_ctl package to use";
};
enableGPS = mkOption {
type = types.bool;
default = false;
description = ''
Enable GPS UART (/dev/ttyAMA0 at 9600 baud).
Requires enabling UART on the CM5 via boot.kernelParams.
'';
};
enableGUI = mkOption {
type = types.bool;
default = false;
description = ''
Enable the system tray GUI for aiov2_ctl.
Requires a desktop environment with system tray support.
'';
};
};
config = mkIf cfg.enable {
# Package the aiov2_ctl tool + pinctrl
environment.systemPackages = with pkgs; [
cfg.package
libraspberrypi # provides pinctrl
];
# Boot rail systemd oneshot service
systemd.services.aiov2-rails-boot = {
description = "Apply AIO v2 GPIO rail boot states";
after = [ "local-fs.target" ];
wants = [ "local-fs.target" ];
before = [ "multi-user.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${applyRailsScript}";
RemainAfterExit = true;
};
};
# GPS configuration
boot.kernelParams = mkIf cfg.enableGPS [ "uart0=on" ];
users.users = mkIf cfg.enableGPS {
gortium = {
extraGroups = [ "dialout" ];
};
};
# GUI autostart (XDG)
systemd.user.services.aiov2-ctl-gui = mkIf cfg.enableGUI {
description = "AIO v2 System Tray Controller";
after = [ "graphical-session.target" ];
wants = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/aiov2_ctl --gui";
Restart = "on-failure";
RestartSec = 5;
};
environment = {
AIOV2_CTL_DEBUG = "0";
};
};
};
}

View File

@@ -0,0 +1,105 @@
# AI Worker Restricted Access
This module provides SSH access for the AI worker (hermes-agent) to run ollama benchmarks on the host.
## Security Model
The `ai-worker` user has:
### Filesystem Access
- **Home directory**: `/home/ai-worker` (standard user home)
- **No bind mounts**: Cannot access `/home/gortium/infra` or other host files
- **Cannot access**: Any files outside standard system paths
### Sudo Access
- **NONE**: ai-worker has no sudo privileges
- Cannot run `nh`, `nixos-rebuild`, `nixpkgs-fmt`, or `nix` with elevated permissions
### Docker Access
- Member of `docker` group - can run `docker` and `docker exec` commands
- Primary use: `docker exec ollama ollama ...` for benchmarking
- Can run `docker exec --privileged ollama rocm-smi ...` for VRAM monitoring
## Workflow: SSH + Docker Benchmarking
The AI worker connects from the Hermes container to the host via SSH, runs ollama benchmarks, then returns to save results.
### Example Workflow
```bash
# From Hermes container, SSH to host
ssh -i /path/to/ssh/key ai-worker@host.docker.internal
# On host, run ollama benchmarks via docker
docker exec ollama ollama pull devstral-small-2:24b
# Create test modelfile
docker exec ollama bash -c 'cat <<EOF > /root/.ollama/test.modelfile
FROM devstral-small-2:24b
PARAMETER num_ctx 65536
PARAMETER num_gpu 99
PARAMETER flash_attn true
EOF'
# Create and test model
docker exec ollama ollama create test-model -f /root/.ollama/test.modelfile
docker exec ollama ollama run test-model "Write a Python async function"
# Check VRAM usage
docker exec --privileged ollama rocm-smi --showmeminfo vram
# Cleanup
docker exec ollama ollama rm test-model
# Exit SSH, return to Hermes container
exit
# Save results in Hermes container
# /opt/data/ai-optimizer/state.json
# /opt/data/ai-optimizer/results.csv
```
## SSH Access
Connect as:
```bash
ssh ai-worker@lazyworkhorse
```
The working directory will be `/home/ai-worker`. No infra repo access.
## Verification
Check ai-worker permissions:
```bash
# On the host, as root or gortium:
sudo -u ai-worker sudo -l
# Should show: no sudo access
# Check docker group membership
groups ai-worker
# Should show: ai-worker docker
```
## Troubleshooting
If ai-worker cannot run docker commands:
```bash
# Check docker group membership
groups ai-worker
# Verify ollama container is running
docker ps | grep ollama
# Test docker access
sudo -u ai-worker docker exec ollama ollama list
```
If SSH connection fails:
```bash
# Check SSH key is authorized
cat /home/ai-worker/.ssh/authorized_keys
# Check SSH service
systemctl status sshd
```

View File

@@ -0,0 +1,17 @@
{ config, pkgs, lib, ... }:
with lib;
{
options.services.aiWorkerAccess = mkOption {
type = types.bool;
default = false;
description = "Enable AI worker SSH access with docker group membership for ollama benchmarking";
};
config = mkIf config.services.aiWorkerAccess {
# ai-worker is member of docker group - can run docker commands via SSH
# No bind mounts, no sudo access - docker-only for ollama benchmarking
users.groups.docker.members = [ "ai-worker" ];
};
}

View File

@@ -0,0 +1,5 @@
{
imports = [
./systemd
];
}

View File

@@ -1,45 +1,87 @@
{ pkgs, ... }: {
systemd.services.init-ollama-model = {
description = "Initialize LLM models with extra context in Ollama Docker";
after = [ "docker-ollama.service" ];
# On s'assure que Docker tourne avant de lancer ce script
after = [ "docker.service" ];
wantedBy = [ "multi-user.target" ];
script = ''
# Wait for Ollama
while ! ${pkgs.curl}/bin/curl -s http://localhost:11434/api/tags > /dev/null; do
sleep 2
done
# Fonction de création asynchrone pour ne pas bloquer le démarrage
(
echo "Starting asynchronous Ollama initialization..."
# Attente d'Ollama (maximum 120 secondes pour éviter une boucle infinie)
TIMEOUT=60
COUNT=0
while ! ${pkgs.curl}/bin/curl -s -f http://127.0.0.1:11434/api/tags > /dev/null; do
if [ $COUNT -ge $TIMEOUT ]; then
echo "Ollama did not become ready in time. Exiting."
exit 1
fi
echo "Waiting for Ollama API to be reachable..."
sleep 5
COUNT=$((COUNT + 5))
done
create_model_if_missing() {
local model_name=$1
local base_model=$2
if ! ${pkgs.docker}/bin/docker exec ollama ollama list | grep -q "$model_name"; then
echo "$model_name not found, creating from $base_model..."
${pkgs.docker}/bin/docker exec ollama sh -c "cat <<EOF > /root/.ollama/$model_name.modelfile
create_model_if_missing() {
local model_name=$1
local base_model=$2
# Vérification robuste via l'API HTTP d'Ollama plutôt que docker exec (évite les conflits de tty)
if ! ${pkgs.curl}/bin/curl -s http://127.0.0.1:11434/api/tags | ${pkgs.jq}/bin/jq -e ".models[] | select(.name == \"$model_name\")" > /dev/null; then
echo "$model_name not found, creating from $base_model..."
# Utilisation d'un fichier temporaire sur l'hôte pour l'injecter proprement dans Docker
TMP_FILE=$(mktemp)
cat <<EOF > "$TMP_FILE"
FROM $base_model
TEMPLATE """{{- if .System }}
[SYSTEM_PROMPT]
{{ .System }}
[/SYSTEM_PROMPT]
{{- end }}
{{- range .Messages }}
{{- if eq .Role "user" }}
[INST]
{{ .Content }}
[/INST]
{{- else if eq .Role "assistant" }}
{{ .Content }}
{{- end }}
{{- end }}"""
PARAMETER num_ctx 131072
PARAMETER num_predict 4096
PARAMETER num_keep 1024
PARAMETER repeat_penalty 1.1
PARAMETER top_k 40
PARAMETER stop \"[INST]\"
PARAMETER stop \"[/INST]\"
PARAMETER stop \"</s>\"
EOF"
${pkgs.docker}/bin/docker exec ollama ollama create "$model_name" -f "/root/.ollama/$model_name.modelfile"
else
echo "$model_name already exists, skipping."
fi
}
PARAMETER stop "[INST]"
PARAMETER stop "[/INST]"
PARAMETER stop "</s>"
EOF
# Create Nemotron
create_model_if_missing "nemotron-3-nano:30b-128k" "nemotron-3-nano:30b"
# Create Devstral
create_model_if_missing "devstral-small-2:24b-128k" "devstral-small-2:24b"
# Copie et création dans le conteneur
${pkgs.docker}/bin/docker cp "$TMP_FILE" ollama:/tmp/model.modelfile
${pkgs.docker}/bin/docker exec ollama ollama create "$model_name" -f /tmp/model.modelfile
${pkgs.docker}/bin/docker exec ollama rm /tmp/model.modelfile
rm -f "$TMP_FILE"
else
echo "$model_name already exists, skipping."
fi
}
# Create Nemotron
create_model_if_missing "nemotron-3-nano:30b-128k" "nemotron-3-nano:30b"
# Create Devstral
create_model_if_missing "devstral-small-2:24b-128k" "devstral-small-2:24b"
) &
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Type = "forking"; # Permet à systemd de savoir que le script passe en arrière-plan via '&'
User = "root";
};
};
}

View File

@@ -0,0 +1,275 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.stagingVm;
in
{
options.services.stagingVm = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable KVM/libvirt staging VM for compose PR testing";
};
vmName = mkOption {
type = types.str;
default = "compose-test-vm";
description = "Name of the staging VM";
};
memory = mkOption {
type = types.str;
default = "4096";
description = "RAM allocated to the staging VM (MB)";
};
vcpus = mkOption {
type = types.int;
default = 2;
description = "Number of vCPUs for the staging VM";
};
storagePath = mkOption {
type = types.str;
default = "/var/lib/libvirt/images";
description = "Path for libvirt storage pool";
};
dataPath = mkOption {
type = types.str;
default = "/var/lib/staging-vm";
description = "Path for compose test data (PR checkouts, test results)";
};
};
config = mkIf cfg.enable {
# Enable libvirt daemon
virtualisation.libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
ovmf = {
enable = true;
packages = [ pkgs.OVMFFull.fd ];
};
};
};
# Kernel modules + groups already handled in configuration.nix
# libvirt NAT network (192.168.122.0/24)
environment.etc."libvirt/qemu/networks/default.xml" = {
text = ''
<network>
<name>default</name>
<uuid>2b8f7a3c-9e5d-4a1f-bc3d-6e7a8f9b0c1d</uuid>
<forward mode='nat'>
<nat>
<port start='1024' end='65535'/>
</nat>
</forward>
<bridge name='virbr0' stp='on' delay='0'/>
<mac address='52:54:00:12:34:56'/>
<ip address='192.168.122.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.122.2' end='192.168.122.254'/>
</dhcp>
</ip>
</network>
'';
# Autostart the network so it comes up on boot
mode = "0644";
};
# Ensure the default network is defined and autostarted
systemd.services.libvirtd = {
postStart = ''
${pkgs.libvirt}/bin/virsh net-define /etc/libvirt/qemu/networks/default.xml 2>/dev/null || true
${pkgs.libvirt}/bin/virsh net-autostart default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh net-start default 2>/dev/null || true
'';
};
# Storage directory for VM images
systemd.tmpfiles.rules = [
"d ${cfg.storagePath} 0755 root root -"
"d ${cfg.dataPath} 0755 root root -"
];
# Ensure storage pool exists in libvirt
systemd.services.libvirtd.postStart = mkAfter ''
${pkgs.libvirt}/bin/virsh pool-define-as default dir --target "${cfg.storagePath}" 2>/dev/null || true
${pkgs.libvirt}/bin/virsh pool-autostart default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh pool-start default 2>/dev/null || true
'';
# Firewall: allow traffic from virbr0 to host and outbound NAT
networking.firewall = {
extraCommands = ''
# Allow inbound DHCP/DNS from libvirt guests
iptables -I INPUT -i virbr0 -p udp --dport 67:68 -j ACCEPT
iptables -I INPUT -i virbr0 -p tcp --dport 53 -j ACCEPT
iptables -I INPUT -i virbr0 -p udp --dport 53 -j ACCEPT
# Allow established/related traffic back to guests
iptables -I FORWARD -i virbr0 -o virbr0 -j ACCEPT
iptables -I FORWARD -o virbr0 -j ACCEPT
iptables -I FORWARD -i virbr0 -j ACCEPT
'';
};
# Packages needed for VM management
environment.systemPackages = with pkgs; [
libvirt
qemu_kvm
virt-manager # optional GUI for manual management
OVMFFull
swtpm
];
# Enable docker in the host (already enabled, but ensure for compose testing)
virtualisation.docker.enable = true;
# Helper script: pr-test-vm
# Usage:
# pr-test-vm build — build the staging VM derivation
# pr-test-vm start — boot the VM with a compose PR branch
# pr-test-vm stop — graceful shutdown
# pr-test-vm destroy — force stop + delete VM
# pr-test-vm ssh — SSH into the running VM
systemd.tmpfiles.rules = mkAfter [
"d ${cfg.dataPath}/scripts 0755 root root -"
];
environment.systemPackages = [ (pkgs.writeShellScriptBin "pr-test-vm" ''
set -euo pipefail
DATA="${cfg.dataPath}"
VM_NAME="${cfg.vmName}"
VM_IMAGE="''${DATA}/''${VM_NAME}.qcow2"
VM_PORT=2223
build_vm() {
echo "==> Building NixOS staging VM for compose testing..."
# Build the VM config inline a minimal NixOS with Docker + SSH
cat > /tmp/staging-vm-config.nix << 'NIXEOF'
{ config, pkgs, lib, ... }: {
boot.loader.grub.devices = [ "/dev/vda" ];
boot.loader.timeout = 0;
# Minimal kernel
boot.kernelParams = [ "console=ttyS0" ];
boot.initrd.kernelModules = [ "virtio_blk" "virtio_net" "virtio_pci" ];
# SSH access
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.PermitRootLogin = "prohibit-password";
};
# Docker for compose testing
virtualisation.docker.enable = true;
# Network (DHCP via virbr0)
networking.useDHCP = true;
networking.firewall.enable = false;
# Users
users.users.root.openssh.authorizedKeys.keys = [
"$(cat /root/.ssh/authorized_keys 2>/dev/null || echo 'ssh-ed25519 AAAAC3... placeholder')"
];
users.users.testrunner = {
isNormalUser = true;
extraGroups = [ "docker" ];
openssh.authorizedKeys.keys = [
"$(cat /root/.ssh/authorized_keys 2>/dev/null || echo 'ssh-ed25519 AAAAC3... placeholder')"
];
};
# Git + compose tools
environment.systemPackages = with pkgs; [ git docker-compose curl ];
system.stateVersion = "24.11";
}
NIXEOF
nixos-rebuild build-vm -I nixpkgs=channel:nixos-unstable \
--arg configuration 'import /tmp/staging-vm-config.nix' \
--out-link "''${DATA}/vm-result"
echo "==> VM built. Run 'pr-test-vm start' to boot."
}
start_vm() {
if [ -f "''${VM_IMAGE}" ]; then
echo "==> Booting existing VM..."
else
echo "==> Creating VM image..."
${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 "''${VM_IMAGE}" 20G
fi
# Check if already running
if ${pkgs.libvirt}/bin/virsh list --name 2>/dev/null | grep -q "''${VM_NAME}"; then
echo "==> VM already running."
exit 0
fi
${pkgs.qemu_kvm}/bin/qemu-system-x86_64 \
-name "''${VM_NAME}" \
-machine q35,accel=kvm \
-cpu host \
-smp ${toString cfg.vcpus} \
-m ${cfg.memory} \
-drive file="''${VM_IMAGE}",if=virtio,format=qcow2 \
-netdev user,id=net0,hostfwd=tcp::''${VM_PORT}-:22 \
-device virtio-net-pci,netdev=net0 \
-nographic \
-serial mon:stdio \
-pidfile "''${DATA}/''${VM_NAME}.pid" \
-daemonize
echo "==> VM booting... SSH on port ''${VM_PORT}"
echo "==> Wait for it: ssh -p ''${VM_PORT} testrunner@localhost"
}
stop_vm() {
PIDFILE="''${DATA}/''${VM_NAME}.pid"
if [ -f "''${PIDFILE}" ]; then
PID=$(cat "''${PIDFILE}")
kill "''${PID}" 2>/dev/null || true
rm -f "''${PIDFILE}"
echo "==> VM stopped."
else
${pkgs.libvirt}/bin/virsh destroy "''${VM_NAME}" 2>/dev/null || true
echo "==> VM destroyed."
fi
}
ssh_vm() {
exec ssh -p "''${VM_PORT}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "testrunner@localhost" "$@"
}
# Main dispatch
case "''${1:-help}" in
build) build_vm ;;
start) start_vm ;;
stop) stop_vm ;;
destroy) stop_vm; rm -f "''${VM_IMAGE}"; echo "==> VM deleted." ;;
ssh) shift; ssh_vm "$@" ;;
*)
echo "Usage: pr-test-vm {build|start|stop|destroy|ssh}"
echo ""
echo " build build the NixOS VM derivation"
echo " start boot the VM (create image if needed)"
echo " stop graceful VM shutdown"
echo " destroy stop + delete VM image"
echo " ssh SSH into the running VM"
;;
esac
'') ];
};
}

View File

@@ -0,0 +1,54 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.gortium.wireguard-client;
in
{
##### Options #####
options.gortium.wireguard-client = {
enable = mkEnableOption "WireGuard VPN client to lazyworkhorse VPN server";
vpnIp = mkOption {
type = types.str;
description = "Assigned VPN IP with CIDR, e.g. \"10.8.0.4/24\"";
example = "10.8.0.4/24";
};
privateKeyFile = mkOption {
type = types.path;
description = "Path to the WireGuard private key (age-encrypted, via agenix)";
};
presharedKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to the WireGuard preshared key (optional, age-encrypted)";
};
};
##### Config #####
config = mkIf cfg.enable {
networking.wireguard.interfaces = {
wg0 = {
ips = [ cfg.vpnIp ];
privateKeyFile = cfg.privateKeyFile;
peers = [
{
# Server public key (lazyworkhorse wg-easy)
publicKey = "rY9zII3AOm8rog2rv02PyA3Bq7zdvTOGkZapfCV1DkE=";
presharedKeyFile = cfg.presharedKeyFile;
# Split-tunnel: only route the VPN subnet
allowedIPs = [ "10.8.0.0/24" ];
endpoint = "vpn.lazyworkhorse.net:51820";
persistentKeepalive = 25;
}
];
};
};
environment.systemPackages = with pkgs; [ wireguard-tools ];
};
}

View File

@@ -0,0 +1,19 @@
--- a/drivers/gpu/drm/panel/panel-cwu50.c
+++ b/drivers/gpu/drm/panel/panel-cwu50.c
@@ -58,5 +58,8 @@
dcs_write_seq(0x72,0x06);
dcs_write_seq(0x75,0x03);
+ /* DSI_INIT0: set 4 lanes (bits[1:0]=11) */
+ dcs_write_seq(0x80,0x03);
dcs_write_seq(0xE0,0x01);
+
dcs_write_seq(0x00,0x00);
dcs_write_seq(0x01,0x47);//VCOM0x47
@@ -721,6 +723,6 @@
dsi->lanes = 4;
dsi->format = MIPI_DSI_FMT_RGB888;
- dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST | MIPI_DSI_MODE_VIDEO_SYNC_PULSE;
+ dsi->mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_SYNC_PULSE;
ctx->id_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_IN);
if (IS_ERR(ctx->id_gpio)) {

1
result Symbolic link
View File

@@ -0,0 +1 @@
/nix/store/z86r4awsbrc5q9qhwwi757wxixcqgn31-nixos-system-uConsole-25.11.20260608.e820eb4

1
result-uconsole Symbolic link
View File

@@ -0,0 +1 @@
/nix/store/7y7rfksqcf5smz59jjixyl56bxq50j9g-nixos-system-uConsole-25.11.20260608.e820eb4

View File

@@ -1,34 +1,36 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBOL29w
eGk1N2xxTHJtaUEvWWZmbkh1bk11Tjk3anNnMDB1cCtPYUMzdTNJCkdhQ08vblNG
UlV1K2xVTGZVTzFWYXAzcjZaMWs0RTFWdStKSmlSTURvK1EKLT4gLC1zKU8zVkgt
Z3JlYXNlIFUiXFcpS302IHByVn5jOy0gRDMKQjV3SHpDWUIybGFyQUg3ZlR0R2hV
eWM3SFlCVW5mdlpBVUF3a0xpNlZCeGNUd1oxTTlkc1RkTXdZS0lFTmN3Ci0tLSA3
VlBqM1VLWllZc0JnOTMvUFRjMU13OTdzMmhsdGJubkk5eGpERVVLYUk4Cnzh5UbU
FlgqpM8jkJ6XlsaIDCw/G3D6uJ/GRJW4gIekuhAUxpZJrc8eOA8ZuHfGrBbH3acV
tVafX5F0Kr2oOblqZ6gduZOUS52KmWH8stiBJM+e5ZZ7zRQVE4PJUKUPCzi+WdcH
zr295T//FOdicrYHdsjfziKEHzBtUCFiATW05+O2zMjYjO6cPzePcCzPWinwiID6
V+f6ngfkkQaj3wBGkzaieQJzRcdSwky21aVhGCCX/bvqx61iW2d5QAKxGbtQ2RcG
X1okr+xunAM94nzDMv46vyN97KxY7cZd4pAaOxoICc2Tfhtw6F+iS6QkQh1odJzO
7ZH+sSQCvndG+8z9shXGiHalASF5tdguM+JlEvAGljcaiAUtsQWxr9CoWiEkC6c6
NCaECSYO8Il+SXBQnSZSGJSNDhuPYCYrsjXGSAONFixuyeslAkq9x2WUaUS4H063
1QvRF7XO2tBPtgCLsSjdiGp0h+ImUaGdu6fDR7zrDsGsaAFCSFeH/rGNNXRQ2vP2
CSfPfDDCqpUSCn0WuA30BtaPLxGmZT6OjFevKzYMNDmdeq9ia/q8K0hmjLUBdN3k
tdYWbwoaf4gYbUWxSleD768b0Jgxss9Vod+sFQ+NYRksdGIeyND+aQIc312XehfA
qHFBS8nlj7eUF5bdvCYQ64z741mH4cNlGxyjPBH1x8FHnEOocJXYt1l2AZSRJmJA
c3z0QGXyuCbsrLBXWK1EKa/Juo4PGGsEVoLRhwJAQy9+i1JN0yrfRvSPyzvD4px6
wRPzlZ80MQdb2lv84WS/zcOEZmZzlLntszTRRdIfAsuaavP2Rquh4rEXABYeTZwp
5dem79s8bdW2nFsGMNz1OQKQwocyjYu1jJMHu6Gp7Ngdl1xyW7xfg0dezE1c0cIh
xt1aLER9YJp4n5to5cOH16l3mjDHnAvABx38xE9loNL3399J/evw7LxpTYQ4v2Xv
x8xnDHcqJ+deFSwyuUnMS5DkUeYuHmUl0Q2WYcfY+ibCmcgCb2ObTtuN1/ZxNYrL
OKrnmfuSvBgyuIOj5e6uWW0+Zs8dHKXu2TgV8WignxOhl5zQgCpCBlqVfO0t+NCu
Gi26hU/fhGWQ/1oQa3VkpGsypZbJpgQvfWxfcGHP/MMhnl01zzlP8/aexSY3pAxf
fz9v0IVh6xxtu3zbiiVzUsXbfG7t+xY98jMphf4AS2mWva3GWVmhhu0lS3J3P+go
YEEP4rOFHeU0Y1/6kLydTXvz4jMH0H92XQIzshd7vzQnEJPUPAzqRmw3LKYGgCI+
wZEnxJ6ckqTkGBFnxTpy9LLllwmnz2Ky87nY3XAmqxlhb2Ap1XFAlfgszmGjc+Il
KkIgoWQHTUm6QM9ta++oUTIDneOvxGd0zZsqoEhiC/7E01BNNZ6E58TeJU3fDlA3
mX6n05XjwPRpgXZfayPoAgBlZc2H4KeiynxwNZ/dWu7qz7L6Ppk6Nvtly8giTbFx
CA+tto7vq+D+CAEJ4bgyq4BCH4GL4APrhPcWp98Mko1WCiRTIKgkZxQCYvlg/LZq
LNhMacP9T1qTvNC+yR1NEMiegE3APzk6CkDpVaO9+5f/sqifNPINCMothenI9ePw
zjQLI3Mo1m73bkomytUZ7i1VstP5sEZ5LF72Sq7BpR3oQ3Gp0CAN9w==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBWNEpt
cGFNeVBBaDRqb3pLSEZGQW0wb3VmVnBoZCswUFkzbnBLUnJ0QTNrClRqVkk4RUVO
d29KYjd5YUcwankvaTFmVHUxQVpDT2ZYWHRaY3JXTUtQMU0KLT4gKXBtQ3UsXi1n
cmVhc2UgNnwxYCBXVyA/KCQmIHt9NAo3OTZVUHR2UXkvaEFwY0ZBdEJsaFpsbHJ0
cklKcDVHcEdWMEdPSkpnN0FiRU43RW5hUWFMdjR3WFRRSFBLSGlmClM3cTNJWlNM
TExkdHdXUHJISkNIaE1TTUxUc1NUWkV1a09HeFU3bVZwQXMKLS0tIGhOcXFTUElS
azJJNnEreUhMWTJBaVZGSTJPRUFqQkVYS01KRENUVVpZSDgK8+8onFejroBo7MeO
dW+so4lOsq4zJKn3f0cxmCFg1f0X8zt6h4Uc3A5Cvr1uU+6yw1FWmJ7xa3jJz3lO
EEaKQJXYC+xIIKGcA7qILa0SFp4a/4OuYjcg27HrlPhg7u5wDhQrd0LdVEe1Xngp
ZivX7P7HwIna3X8C+TL+K2v/AG2N/z86cdKfRvxyMKNbHhYw+CfHEnWgh8tJ++4h
G9evNniuNqte6cQaRe7jODfPNW4FuY/Sb7barlJ/M9iAQdYAdyLAzU1LABeHeUfD
wtHjxy9DUZ55Vg8bB8M2JJU9MkoRT4ewiVd9LeC1GWeVmKsm93wsmrov714i7U2j
wHtDkjqEF2MmzuQc18sjNaAHiwz8j6o5xU2L/Q4+Q707yISWG7RGZYh389Cr1rnw
siUq/Vunqw2wk13+J/4vu9nqt5mMktBaCtp+QiWIurjwB5LUAyChrSm+dg5lb0Mt
UhSc0lq1+E3vxAXM2Hmk+vP86VD+6WJvAU82VFApF1s6zG2FU1/AcOVVf54nan/q
f+rgSFfASHQCYSblUJHyEtwLNsWEmTGmOEn1buUKD/H0zatPQnc0rYpjlx2V0Sjd
6yB5+wPrZ0AkN1pjcsPKOv8Kaog2DzqIjib+SaSTaRxWHQEb9uzvaReAcYI5HOpE
gkC040HN33BItATbo4+hz70Im8Ni/VXD+g6yzM6Hj1hJL+PinTKeg5keQRFIZjMx
grzievB2wVBBgLgN3qMdTFmpplaL7iL702JjXZUTTK9Izp+9wiCsV1fTa53FWDht
ylFL5SWElqXjK+QBXxAe+Jk6VQov5HI21YDXL67S554ABeRok23wxrQ31TCI4xq9
PQV7VtNRjyVud7S29m3OwpWOsgTZhn+JclHj2v4bNJzJkJnZRTmcvGPktzRI5+R4
e5vxVhGnJDzI71txaHl8+xS1lu9VzCQUrxX6TXyTRV4KjIOz0g06JOBgmBRBvJca
7MZbC65xpisl/gyLRbgkVga3t94dPV+dpZsn8eq6427IyRbKslJefatggR9//c6I
5N5fl0fR3gJQMB+HRbipBH2YsdbdWJyb4Nn6STZxIfrqoG/xC6C1raF0xK7hUx6i
4DUDSPohM8fOIswQPfE+FH3eygfzu/Ln5+ghsgHTEhgFvmgMvyxaAt6kHIzIUhMX
M3dASr4VPDpIXuXsRWwYLEifhzxsuvwVxfwtsnCaR6XKijsYECWGDdYOWHdleeqx
wDPhxEesfFVhKxhrKY9Ir8k9/FFBKQU/3GjW4+SMAg5Al1YEzxshP9vKuVcsei7W
JDwAwotNXaCm6NBckiyZJE53ou6+gckPY7V9cOfnuH74Z9ywkFzB3HW3ZlonaGyM
oGmLGcccavFtyhg5s/As4i6X8ARIpDiwe59Pn3GNXMctySqIrrr2ogUoXgrfFCie
6GOTdeMW7GeOSdJUxCofghlspS/nq01Og77VI/beWYrIwLubSka6Zaltww9zgObk
/FGEMgFkEpq7iyCvYSPA8F46pJKvnMP3S84AWCPmcTcHeg4lwGPvs6btexXBGdoz
nkCyq7wdH5Nngm7jUbl88LtaLZPAQkuqXphBVTnrF9Ofbnb4iRZ2Op4xpx9rGyvx
mO6UEhL6V1i2YZFNkNMg/W8aoMiUgBdqbkxaxblT9L0aNdlFU9+LbWYolURVEadd
Qjv0Z1gMA+tsuBbVszwsMfneZ5+B9Q==
-----END AGE ENCRYPTED FILE-----

View File

@@ -0,0 +1,10 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA4MlFz
SHFjYjJMVHRlTWNGVGI2bHQxc0xRd2tlaExlM0NFMWhlbkR2bVg0CkxxenVTaXkr
eWxybDdCeUM0ejRvZWI4cFZCWm5VczRvZkNnT0d5Y1oyYmsKLT4gK1NmRzVtLWdy
ZWFzZSB3UDI6TyNaCnF4Ylk0QWduaXZxRFBFbDBOZ0dxeGxiWTVCYjRtZTJBRkFC
YU5qaytYWWI4OWl1K1FSdXNlY2JXZjkzak9tTHkKVFlCRlRqY1FVSzFmNS9yZmxF
aEUxelUwNEpKN3VXYi9KUWN4bXFscm5oUEFOajhRZDlERWVYcFgvQQotLS0gK1JI
VERTQjB6d1k3NDQwbjNveXBqcFk1WE96cHlaTTVkTWRMZENPamFJZwpcT1CP/KvU
CsunvfX9RBlSSKuw4eem9N9s3JqJNj4FRQizNx6QzlE1vSME
-----END AGE ENCRYPTED FILE-----

10
secrets/home_wifi.age Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSAycE1Y
YmMvUWZpK2VKQVlqaHFtaERBRGROcFIyL0d6dEVRQmFxLzlqdFZNCkYxWkNIUXRZ
V0dQOG4zY3U3Nk1JelBtY0cwUGdxaEI3dmZaVTZId04rVTQKLT4geV1cZC4wMnst
Z3JlYXNlIDYgOG1IME1xCkQ0RGN1NU1FUWk0Y1RmamNEY0tJWmFQNGdoMkROcGVy
aU5UYVFobVRLMVVUQ1JicUM2c0tSVzRQdEZ0VE5YamQKZUxPeVpLWDZJR0hqemdD
cmkyUUdFZEZKZjBDNGhmNFR6bVUKLS0tIDRQUGR5RGI5UEhGNk5EQWw4dFk0R01k
TUJWOFpleXBUajFPckFmem52cGsKHzn+QnuYLI2NEh5WWZQHrNuvVzYk+kVjsAsn
KNS2dHjvadAopVY2Gypldf1p2RRtmgZkDHaPlNzv5Hk=
-----END AGE ENCRYPTED FILE-----

View File

@@ -8,6 +8,8 @@ let
in
{
"containers.env.age".publicKeys = authorizedKeys;
"gortium_password.age".publicKeys = authorizedKeys;
"home_wifi.age".publicKeys = authorizedKeys;
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys;
"openclaw_gateway_token.age".publicKeys = authorizedKeys;

View File

@@ -0,0 +1,9 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA3VG9Z
MVFPVFc2VVJ3d0h0dmtBUnI3WHl2SzUxTkRZbjFCaGloWmV3dnd3ClcxdnVPeGd6
SU4zR0Q0K1dtVjRRVHd0VW5XSFI0dVFpTjZnYk1DNjRxTVEKLT4gQzlgRy1ncmVh
c2UKeUozOWgyUytSTVF0NjY2STBEb2VadwotLS0gblI3bmJCUWxxU3QrYTEyVFBI
Snc4NC9rTkh0NnZYbUtxUE9hRWRkelpmMAq58fmH6cK13GeD7wGLxKmx10hmJeW4
b7KqnCD1ZP7uG85s32xzVRwRG8RrG4xZo5nR9Mrtg1CoTSFfUGeFnf5xveN+Ej0X
wDVB1LwC+Q==
-----END AGE ENCRYPTED FILE-----

View File

@@ -0,0 +1,11 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA5dzVG
WUNvT3NlRmcrWS81bzJqSWlTekVYaDFFTE10SkI2dEgzaGpxcUI4Cmk5Y0FGYTRZ
K0NGYzY3VUp4aS9ZZGRmWTgybDJFUURva2pZNmVOS3QxdEUKLT4gPnVRTCtldGMt
Z3JlYXNlCk04OTJZeFRNeDI5aGpMVTk1ZTE0Y2FMMnFEMjlJalJpMHRlaTE4ZWIx
d2lCRGQ5RHVjcktOMGJCb1VERlNWcTYKaSt0L1Z6dVJ0QWIyZkhsYzFEVjZSQWUr
ZWpwVlo1TmhoUFJZdkEvR0gxNlVhcXF2ZTRnCi0tLSBLcmM2MThNVkdWclpHUXRr
VTF6QVk2WUZlTXpZMVNLMlpBOFc3M1o5WjZzCs9xbPlIX+u5vRSQ/z9utu+I9S2c
02DOsIb1kzxzb1OK91b8Kh4JucQSq3qkyEvRucsNn5QW8hIHDnRuND6EbPyN7p4S
YB/F0dxSqgnq
-----END AGE ENCRYPTED FILE-----

View File

@@ -9,8 +9,19 @@
openssh.authorizedKeys.keys = [
keys.users.ai-worker.main
];
# No password login - SSH key only
hashedPassword = "!";
};
users.groups.ai-worker = {};
# Enable restricted AI worker SSH access for ollama benchmarking
# SECURITY: ai-worker can only:
# - SSH into host from Hermes container
# - Run docker commands (docker exec ollama ...) via docker group
# - Run specific security audit commands
# - NO access to infra repo (no bind mount)
# - NO sudo access (no nh, nixos-rebuild, nixpkgs-fmt, nix)
# WORKFLOW: SSH from Hermes container, run docker benchmarks, return and save results to /opt/data/ai-optimizer/
# Restricted sudo for ai-worker - security checks only
security.sudo.extraRules = [

View File

@@ -1,4 +1,6 @@
{ pkgs, inputs, config, keys, ... }: {
home-manager.extraSpecialArgs = { inherit (config.networking) hostName; dotfiles = ../../assets/dotfiles; };
home-manager.users.gortium = import ./home.nix;
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" "video" "render"];
@@ -6,9 +8,10 @@
packages = with pkgs; [
tree
btop
nh
];
shell = pkgs.zsh;
passwordFile = config.age.secrets.gortium_password.path;
ignoreShellProgramCheck = true;
openssh.authorizedKeys.keys = [
keys.users.gortium.main
];

65
users/gortium/home.nix Normal file
View File

@@ -0,0 +1,65 @@
{ pkgs, lib, config, inputs, hostName, ... }:
let
dotfiles = ../../assets/dotfiles;
isUconsole = hostName == "uConsole";
in {
home.username = "gortium";
home.homeDirectory = "/home/gortium";
home.stateVersion = "23.11";
programs.home-manager.enable = true;
home.file = {
# tmux
".tmux.conf".source = "${dotfiles}/tmux/.tmux.conf";
# kitty
".config/kitty/kitty.conf".source = "${dotfiles}/kitty/.config/kitty/kitty.conf";
# nvim
".config/nvim/init.lua".source = "${dotfiles}/nvim/.config/nvim/init.lua";
# starship
".config/starship.toml".source = "${dotfiles}/starship/.config/starship.toml";
# btop
".config/btop/btop.conf".source = "${dotfiles}/btop/.config/btop/btop.conf";
# waybar
".config/waybar/style.css".source = "${dotfiles}/waybar/.config/waybar/style.css";
".config/waybar/config.jsonc".source = "${dotfiles}/waybar/.config/waybar/config.jsonc";
# wofi
".config/wofi/style.css".source = "${dotfiles}/wofi/.config/wofi/style.css";
".config/wofi/config".source = "${dotfiles}/wofi/.config/wofi/config";
# yazi
".config/yazi/yazi.toml".source = "${dotfiles}/yazi/.config/yazi/yazi.toml";
# hyprland — common config
".config/hypr/hyprland.conf".source = "${dotfiles}/hypr/.config/hypr/hyprland.conf";
".config/hypr/hypridle.conf".source = "${dotfiles}/hypr/.config/hypr/hypridle.conf";
".config/hypr/hyprlock.conf".source = "${dotfiles}/hypr/.config/hypr/hyprlock.conf";
".config/hypr/hyprpaper.conf".source = "${dotfiles}/hypr/.config/hypr/hyprpaper.conf";
".config/hypr/mocha.conf".source = "${dotfiles}/hypr/.config/hypr/mocha.conf";
# hyprland — host-specific monitor config
".config/hypr/host/monitors.conf".source =
if isUconsole
then "${dotfiles}/hypr/.config/hypr/hosts/uconsole.conf"
else "${dotfiles}/hypr/.config/hypr/hosts/laptop.conf";
};
home.packages = with pkgs; [
git zsh tmux starship
neovim kitty
btop yazi ripgrep fd fzf
] ++ lib.optionals (!isUconsole) [
waybar wofi swww hyprshot
] ++ lib.optionals isUconsole [
brightnessctl
];
programs.zsh.enable = true;
programs.starship.enable = true;
}