diff --git a/.gitmodules b/.gitmodules index bd90853..66f7a80 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/assets/compose b/assets/compose index d3f2e3b..3e62c70 160000 --- a/assets/compose +++ b/assets/compose @@ -1 +1 @@ -Subproject commit d3f2e3b7b9dcb03b0bd7df0278faca6b64ea9272 +Subproject commit 3e62c702de58947737b6e547c35f532e1c4fa0e5 diff --git a/assets/dotfiles b/assets/dotfiles new file mode 160000 index 0000000..504daea --- /dev/null +++ b/assets/dotfiles @@ -0,0 +1 @@ +Subproject commit 504daea61eadfbc84853b83508cc7a9b9ddddd48 diff --git a/assets/ollama/Dockerfile b/assets/ollama/Dockerfile deleted file mode 100644 index 438e607..0000000 --- a/assets/ollama/Dockerfile +++ /dev/null @@ -1,106 +0,0 @@ -# ollama-gfx906/Dockerfile -# -# Custom ollama image with ROCm 6.1 + gfx906 (MI50) support. -# The official ollama/rocm image ships ROCm 7.2 which dropped gfx906. -# This uses v0.23.2's native CMake build system with AMDGPU_TARGETS including gfx906. -# -# Build: docker build -t ollama/ollama:rocm-gfx906 ai/ollama - -FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder - -# Build dependencies (CMake, Ninja, Go) -ARG CMAKEVERSION=3.31.2 -ARG NINJAVERSION=1.12.1 -ARG GOLANG_VERSION=1.22.0 - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - curl git ccache build-essential pkg-config unzip \ - && rm -rf /var/lib/apt/lists/* - -# Install CMake from official binaries -RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-x86_64.tar.gz \ - | tar xz -C /usr/local --strip-components 1 - -# Install Ninja -RUN curl -fsSL -o /tmp/ninja.zip \ - https://github.com/ninja-build/ninja/releases/download/v${NINJAVERSION}/ninja-linux.zip \ - && unzip /tmp/ninja.zip -d /usr/local/bin && rm /tmp/ninja.zip - -# Install Go -RUN curl -fsSL https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \ - | tar xz -C /usr/local -ENV PATH=/usr/local/go/bin:$PATH - -ARG OLLAMA_VERSION=v0.23.2 -RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build -WORKDIR /build - -# ROCm paths -ENV HIP_PATH=/opt/rocm -ENV ROCM_PATH=/opt/rocm -ENV CMAKE_GENERATOR=Ninja -ENV LDFLAGS=-s - -# Step 1: Build CPU backends with GCC (no ROCm preset) -# Pre-set CMAKE_HIP_COMPILER="" to prevent check_language(HIP) from -# finding a HIP compiler (it searches /opt/rocm even without PATH). -# Remove /opt/rocm from PATH to prevent find_program from finding hipcc. -RUN mkdir -p build-cpu && \ - PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ - cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_HIP_COMPILER="" \ - -DCMAKE_INSTALL_PREFIX=/build/dist && \ - cmake --build build-cpu --target ggml-cpu -- -l $(nproc) && \ - cmake --install build-cpu --component CPU --strip && \ - echo "=== CPU install ===" && \ - (find /build/dist/lib/ollama -type f -o -type l 2>&1 | head -20 || echo "empty") - -# Step 2: Build HIP backend with ROCm preset + gfx906 target only -# The ROCm 6 preset enables HIP language detection (enable_language(HIP)) -# which ensures GPU kernels are properly compiled for gfx906. -# OLLAMA_RUNNER_DIR=rocm from the preset, so HIP goes to lib/ollama/rocm/ -# Need CMAKE_PREFIX_PATH so find_package(hip) finds hip-config.cmake -# at /opt/rocm/lib/cmake/hip/hip-config.cmake. -RUN mkdir -p build-hip && \ - cmake -B build-hip \ - --preset 'ROCm 6' \ - -DAMDGPU_TARGETS="gfx906:xnack-" \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH="/opt/rocm" && \ - cmake --build build-hip --target ggml-hip -- -l $(nproc) && \ - cmake --install build-hip --component HIP --strip && \ - echo "=== HIP install ===" && \ - find /build/dist/lib/ollama -type f -o -type l | head -20 - -# Step 3: Build Go binary (GCC for CGo linking) -ENV CGO_ENABLED=1 -RUN go build -trimpath -ldflags="-X=github.com/ollama/ollama/version.Version=${OLLAMA_VERSION}" -o /build/dist/ollama . - -# ---------- Runtime image ---------- -FROM ubuntu:24.04 - -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - ca-certificates curl libstdc++6 libgomp1 libvulkan1 libopenblas0 \ - && rm -rf /var/lib/apt/lists/* - -# Copy ROCm 6.1 runtime libraries -# These are needed at runtime by ggml-hip via LD_LIBRARY_PATH -COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ -COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ - -# Copy ollama binary + all backends (CPU + HIP) -# CPU install: /build/dist/lib/ollama/libggml-*.so -# HIP install: /build/dist/lib/ollama/rocm/libggml-hip.so -COPY --from=builder /build/dist/ollama /usr/bin/ollama -COPY --from=builder /build/dist/lib/ollama/ /usr/lib/ollama/ - -RUN ldconfig - -ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama/rocm:/usr/lib/ollama -ENV HSA_OVERRIDE_GFX_VERSION=9.0.6 -ENV HCC_AMDGPU_TARGET=gfx906 -ENV HSA_ENABLE_SDMA=0 - -EXPOSE 11434 -ENTRYPOINT ["/bin/ollama"] -CMD ["serve"] diff --git a/flake.lock b/flake.lock index e78ed64..84ace6f 100644 --- a/flake.lock +++ b/flake.lock @@ -23,7 +23,84 @@ "type": "github" } }, + "agenix-rekey": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "nixpkgs": [ + "nixpkgs" + ], + "pre-commit-hooks": "pre-commit-hooks", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1774522439, + "narHash": "sha256-GvINrdGznE7mGlDNjW0/PMgOJlC+Nl9MkfxALB4QvWs=", + "owner": "oddlama", + "repo": "agenix-rekey", + "rev": "8b9c179bc1300ab130c90f2d25426bf0e7a2b58d", + "type": "github" + }, + "original": { + "owner": "oddlama", + "repo": "agenix-rekey", + "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" + } + }, + "devshell": { + "inputs": { + "nixpkgs": [ + "agenix-rekey", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1728330715, + "narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=", + "owner": "numtide", + "repo": "devshell", + "rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { "flake": false, "locked": { "lastModified": 1751685974, @@ -37,6 +114,64 @@ "url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz" } }, + "flake-compat_3": { + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "agenix-rekey", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "agenix-rekey", + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -58,16 +193,37 @@ "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", + "flake-compat": "flake-compat_2", "nix2container": "nix2container", "nix_2_18": "nix_2_18", "nixpkgs": [ "nixpkgs" ], "nixpkgs-regression": "nixpkgs-regression", - "pre-commit-hooks": "pre-commit-hooks" + "pre-commit-hooks": "pre-commit-hooks_2" }, "locked": { "lastModified": 1774721317, @@ -144,6 +300,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_3", + "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 +406,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, @@ -193,6 +439,29 @@ } }, "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "agenix-rekey", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1735882644, + "narHash": "sha256-3FZAG+pGt3OElQjesCAWeMkQ7C/nB1oTHLRQ8ceP110=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "a5a961387e75ae44cc20f0a57ae463da5e959656", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "pre-commit-hooks_2": { "flake": false, "locked": { "lastModified": 1769939035, @@ -211,8 +480,13 @@ "root": { "inputs": { "agenix": "agenix", + "agenix-rekey": "agenix-rekey", + "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": { @@ -229,6 +503,27 @@ "repo": "default", "type": "github" } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "agenix-rekey", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1735135567, + "narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "9e09d30a644c57257715902efbb3adc56c79cf28", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 8f8b51a..50eecca 100644 --- a/flake.nix +++ b/flake.nix @@ -12,21 +12,29 @@ url = "git+https://git.lix.systems/lix-project/lix?ref=main"; inputs.nixpkgs.follows = "nixpkgs"; }; + nixos-uconsole = { + url = "github:nixos-uconsole/nixos-uconsole"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + nixos-raspberrypi = { + url = "github:nvmd/nixos-raspberrypi/v1.20260517.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + home-manager = { + url = "github:nix-community/home-manager/release-25.11"; + inputs.nixpkgs.follows = "nixpkgs"; + }; self.submodules = true; }; - outputs = { self, nixpkgs, agenix, lix, ... }@inputs: + outputs = { self, nixpkgs, agenix, lix, nixos-uconsole, nixos-raspberrypi, home-manager, ... }@inputs: let system = "x86_64-linux"; keys = import ./lib/keys.nix; - paths = { - flake = "/home/gortium/infra"; - identities = [ - "/home/gortium/.ssh/gortium_ssh_key" - "/etc/ssh/ssh_host_ed25519_key" - "/root/.age/bootstrap.key" ]; - }; - overlays = [ agenix.overlays.default ]; + paths = import ./lib/paths.nix; + overlays = [ + agenix.overlays.default + (import ./overlays/reticulum.nix) ]; pkgs = import nixpkgs { inherit system overlays; config.allowUnfree = true; @@ -61,6 +69,7 @@ ./modules/nixos/services/open_code_server.nix ./modules/nixos/services/ollama_init_custom_models.nix ./modules/nixos/services/openclaw_node.nix + ./modules/nixos/filesystem/poup-16t-disk.nix ./modules/nixos/security/ai-worker-restricted.nix ./users/gortium.nix ./users/ai-worker.nix @@ -80,6 +89,24 @@ ./hosts/cyt-pi/hardware-configuration.nix ]; }; + + uConsole = nixos-raspberrypi.lib.nixosSystem { + specialArgs = { inherit self keys paths inputs nixos-raspberrypi; }; + modules = [ + { + nixpkgs.overlays = overlays; + nixpkgs.config.allowUnfree = true; + nixpkgs.hostPlatform = "aarch64-linux"; + nix.package = lix.packages."aarch64-linux".default; + } + nixos-raspberrypi.nixosModules.raspberry-pi-5.base + nixos-uconsole.nixosModules.uconsole-cm5 + agenix.nixosModules.default + ./modules/nixos/hardware/uconsole-cm5-aio-v2.nix + ./hosts/uConsole/configuration.nix + ./hosts/uConsole/hardware-configuration.nix + ]; + }; }; devShells.${system}.default = devShell; }; diff --git a/hosts/cyt-pi/configuration.nix b/hosts/cyt-pi/configuration.nix index 2e33723..f97442f 100644 --- a/hosts/cyt-pi/configuration.nix +++ b/hosts/cyt-pi/configuration.nix @@ -50,7 +50,7 @@ User = "gortium"; Group = "kismet"; ExecStart = '' - ${pkgs.kismet}/bin/kismet -c panda --log-base=/home/gortium/kismet_logs --no-nc-ui + ${pkgs.kismet}/bin/kismet -c panda --log-prefix=/home/gortium/kismet_logs --no-nc-ui ''; Restart = "always"; RestartSec = "10s"; diff --git a/hosts/lazyworkhorse/configuration.nix b/hosts/lazyworkhorse/configuration.nix index f1afae4..c630767 100644 --- a/hosts/lazyworkhorse/configuration.nix +++ b/hosts/lazyworkhorse/configuration.nix @@ -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 @@ -49,24 +49,12 @@ networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. networking.hostId = "deadbeef"; - # WireGuard VPN client -- always up, connects to wg-easy server - # Create age-encrypted secrets before deploying (run on the host): - # echo -n "" | agenix -e secrets/wireguard_private_key.age - # echo -n "" | agenix -e secrets/wireguard_preshared_key.age - networking.wireguard.interfaces = { - wg0 = { - ips = [ "10.8.0.3/24" ]; - privateKeyFile = config.age.secrets.wireguard_private_key.path; - peers = [ - { - publicKey = "rY9zII3AOm8rog2rv02PyA3Bq7zdvTOGkZapfCV1DkE="; - presharedKeyFile = config.age.secrets.wireguard_preshared_key.path; - allowedIPs = [ "10.8.0.0/24" ]; - endpoint = "vpn.lazyworkhorse.net:51820"; - persistentKeepalive = 25; - } - ]; - }; + # 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. @@ -178,9 +166,9 @@ settings = { PasswordAuthentication = false; KbdInteractiveAuthentication = false; - # Additional hardening settings below in SERVER HARDENING section - }; - hostKeys = [ + # ============================================================ + # ClamAV antivirus — daemon, hourly updates, daily scan, on-access + # ============================================================ { path = "/etc/ssh/ssh_host_ed25519_key"; type = "ed25519"; @@ -349,6 +337,16 @@ # networking.firewall.enable = false; # ============================================================================= + # ============================================================ + # ClamAV antivirus — daemon, hourly updates, daily scan, on-access + # ============================================================ + gortium.clamav = { + enable = true; + enableDaemon = true; + onAccessScanning = true; + dailyScanTime = "03:00"; + }; + # SERVER HARDENING - Firewall, Fail2ban, SSH, Kernel # ============================================================================= @@ -571,3 +569,19 @@ } + # Remote builder — uConsole for aarch64-linux native builds + nix.distributedBuilds = true; + nix.buildMachines = [{ + hostName = "192.168.1.120"; + systems = ["aarch64-linux"]; + maxJobs = 4; + supportedFeatures = ["big-parallel" "nixos-test" "benchmark" "gccarch-armv8-a"]; + sshUser = "builder"; + sshKey = "/home/ai-worker/id_deploy"; + }]; + nix.extraOptions = ' + builders-use-substitutes = true + fallback = true + '; + + diff --git a/hosts/uconsole-cm5/configuration.nix b/hosts/uconsole-cm5/configuration.nix new file mode 100644 index 0000000..549cabe --- /dev/null +++ b/hosts/uconsole-cm5/configuration.nix @@ -0,0 +1,214 @@ +{ config, lib, pkgs, keys, ... }: +{ + networking.hostName = "uConsole"; + time.timeZone = "America/Montreal"; + i18n.defaultLocale = "en_CA.UTF-8"; + system.stateVersion = "25.11"; + # Boot & Hardware + boot.loader.raspberry-pi.bootloader = "kernel"; + # 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 + ]; + # Age secret for gortium password (file created by user) + age.secrets.gortium_password = { + file = ../../secrets/gortium_password.age; + }; + # WiFi via NetworkManager + networking.networkmanager.enable = true; + # Firmware + hardware.enableRedistributableFirmware = true; + # RealtimeKit for PipeWire audio + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = 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; + bootRails = { + GPS = false; + LORA = false; + SDR = false; + USB = false; + }; + enableGPS = false; + }; + # User + users.users.gortium = { + isNormalUser = true; + extraGroups = [ "wheel" "networkmanager" "video" "dialout" "kismet" ]; + hashedPasswordFile = config.age.secrets.gortium_password.path; + openssh.authorizedKeys.keys = [ + keys.users.gortium.main + keys.users.gortium.gitea + ]; + }; + security.sudo.extraRules = [ + { + users = [ "gortium" ]; + commands = [{ + command = "ALL"; + options = [ "NOPASSWD" ]; + }]; + } + ]; + # ============================================================ + # Package groups + # ============================================================ + # ============================================================ + # CROSS-COMPILE REMOVALS — packages removed for aarch64 bootstrap + # ============================================================ + # These packages fail to cross-compile for aarch64. + # Install them natively AFTER the first successful switch. + # + # Removed: Reason: +# inspectrum — Qt5 cross-compile cascade fails (qtsvg mismatched qtbase deps) + # hashcat — Makefile calls gcc directly (cross-compiler not used) + # neovim — Same as hashcat: Makefile calls gcc directly (cross-compiler not used) + # clamav — cmake try_run + Rust proc-macro linker for aarch64 + # sdrpp — glfw/wxPython cross-compile fails + # gqrx — Qt5 cross-compile cascade fails + # emacs-pgtk → emacs-nox — GTK3 + mailutils → gss → shishi chain + # viking — GTK3 GPS map editor + # foxtrotgps — GTK2 GPS app + # js8call — QtQuick3D dep + # wsjtx — qtbase/Qt5 linker fails (collect2: ld returned 1) + # fldigi — same: qtbase/Qt5 linker fails +# gpsbabel — qmake can't find cross-compiler g++ +# john — configure script needs python (not in PATH during cross-compile) +# trustedqsl — needs wxWidgets (unavailable in cross-compile) +# chirp — depends on wxPython (fails cross-compile: GTK3 + wx build) + # ============================================================ + environment.systemPackages = with pkgs; [ + # ===== Base ===== + # emacs-pgtk — removed for bootstrap (GTK3 cross-compile fails) + # emacs-nox — removed for bootstrap (depends on mailutils -> gss -> shishi, cross-compile fails) + git + ripgrep + fd + htop + tmux + # ===== HAM Radio ===== + wsjtx # removed for bootstrap - now native + fldigi # removed for bootstrap - now native + pat # Winlink client + direwolf # AX.25 packet modem + chirp # Radio programming tool - now native + hamlib # Ham radio control libraries + trustedqsl # Logbook of the World (LoTW) - now native + # ===== SDR / RF ===== + sdrpp # removed for bootstrap - now native + gqrx # removed for bootstrap - now native + rtl-sdr # RTL-SDR drivers & utilities + inspectrum # removed for bootstrap - now native + soapysdr-with-plugins # SoapySDR + hardware support plugins + # ===== Mesh / LoRa ===== + reticulumStack # Reticulum Network Stack + lxmf # LXMF messaging protocol + nomadnet # Nomad Network client + # ===== Security ===== + nmap + aircrack-ng + kismet # Wi-Fi monitor / IDS + bettercap # MITM/network attack framework + wireshark-cli # Packet analyzer + john # John the Ripper - now native + sqlmap # SQL injection tool + # ===== GPS / Maps ===== + foxtrotgps # removed for bootstrap - now native + viking # removed for bootstrap - now native + gpsbabel # GPS data conversion - now native + ]; + # ============================================================ + # Reticulum Service (rnsd) + # ============================================================ + systemd.services.rnsd = { + description = "Reticulum Network Stack Daemon"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "gortium"; + Group = "gortium"; + ExecStart = "${pkgs.reticulumStack}/bin/rnsd"; + Restart = "always"; + RestartSec = "10s"; + LimitNOFILE = 65536; + }; + }; + # ============================================================ + # Kismet Service (Wi-Fi monitoring / mesh node) + # ============================================================ + systemd.services.kismet = { + description = "Kismet Wi-Fi Monitor & IDS"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "gortium"; + Group = "kismet"; + ExecStart = "${pkgs.kismet}/bin/kismet -c wlan0 --log-prefix=/home/gortium/kismet_logs --no-nc-ui"; + Restart = "always"; + RestartSec = "10s"; + }; + }; + # ============================================================ + # Kernel modules for SDR and radio + # ============================================================ + boot.kernelModules = [ + "88x2bu" # Realtek 8812/8821BU USB WiFi + "rtl8xxxu" # RTL8188/8192/8723 USB WiFi + "rtl2832_sdr" # RTL-SDR kernel module + "dvb_usb_rtl28xxu" # RTL-SDR DVB-T + ]; + # ============================================================ + # Extra udev rules for SDR and HAM radio devices + # ============================================================ + services.udev.packages = with pkgs; [ rtl-sdr ]; + # ============================================================ + # Enable IPv6 for Reticulum mesh + # ============================================================ + networking.enableIPv6 = true; + # ============================================================ + # Firewall + # ============================================================ + networking.firewall.allowedTCPPorts = [ 22 ]; + networking.firewall.allowedUDPPorts = [ ]; + # ============================================================ + # agenix-rekey — automatic secret re-encryption at deploy time + # ============================================================ + age.rekey = { + # Master identities for encrypting secrets (on Thierry's laptop) + masterIdentities = [ + "/home/gortium/.ssh/gortium_ssh_key" + ]; + # uConsole SSH host pubkey — for automatic rekey at build time + # Once uConsole is deployed, replace with actual pubkey from: + # ssh-keyscan uConsole.local | ssh-to-age + hostPubkey = "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqs3290gq"; # dummy — replace after bootstrap + }; + + # Pipewire overlay: drop libcamera (fixes aarch64 cross-compile — rpi-pisp blocks) + nixpkgs.overlays = [ + (final: prev: { + pipewire = prev.pipewire.override { libcamera = null; }; + }) + ]; +} diff --git a/hosts/uconsole-cm5/hardware-configuration.nix b/hosts/uconsole-cm5/hardware-configuration.nix new file mode 100644 index 0000000..c952ffb --- /dev/null +++ b/hosts/uconsole-cm5/hardware-configuration.nix @@ -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"; +} diff --git a/modules/nixos/filesystem/poup-16t-disk.nix b/modules/nixos/filesystem/poup-16t-disk.nix new file mode 100644 index 0000000..1cd377c --- /dev/null +++ b/modules/nixos/filesystem/poup-16t-disk.nix @@ -0,0 +1,121 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.gortium.poup16t; + luksName = cfg.luksName; +in +with lib; + +{ + options.gortium.poup16t = { + enable = mkEnableOption "Poup_16T storage disk (btrfs + LUKS + btrbk snapshots)"; + + luksUuid = mkOption { + type = types.str; + description = '' + UUID of the LUKS partition on the 16TB disk (WD Red Pro). + + Find this by running as root when the disk is connected: + blkid /dev/sdb # or wherever the disk appears + lsblk -o NAME,SIZE,FSTYPE,UUID + + Since btrfs is inside LUKS, the FS UUID is hidden — use the + LUKS partition UUID from blkid (it'll show TYPE=\"crypto_LUKS\"). + ''; + example = "00000000-0000-0000-0000-000000000000"; + }; + + luksName = mkOption { + type = types.str; + default = "poup_16t"; + description = "Name for the LUKS /dev/mapper/ mapping"; + }; + + mountPoint = mkOption { + type = types.str; + default = "/mnt/Poup_16T"; + description = "Mount point for the 16TB data disk"; + }; + + btrfsOptions = mkOption { + type = types.listOf types.str; + default = [ "defaults" "noatime" "compress=zstd:3" "nofail" ]; + description = "Mount options for the btrfs filesystem. 'nofail' ensures boot succeeds when disk is disconnected."; + }; + + btrbk = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable btrbk snapshot management on this volume"; + }; + + schedule = mkOption { + type = types.str; + default = "daily"; + description = "systemd calendar event for btrbk (e.g. 'daily', 'hourly', '*-*-* 00:00:00')"; + }; + + preserveMin = mkOption { + type = types.str; + default = "2d"; + description = "btrbk snapshot_preserve_min — minimum age before pruning"; + }; + + preserve = mkOption { + type = types.str; + default = "14d 4w 3m"; + description = "btrbk snapshot_preserve — retention policy (daily, weekly, monthly)"; + }; + + snapshotDir = mkOption { + type = types.str; + default = ".snapshots"; + description = "Directory name for snapshots relative to volume root"; + }; + }; + }; + + config = mkIf cfg.enable { + # Enable btrfs kernel support (no DKMS needed — it's in-tree) + boot.supportedFilesystems = [ "btrfs" ]; + + # Install btrfs administration tools + environment.systemPackages = with pkgs; [ + btrfs-progs # mkfs.btrfs, btrfs, fsck, balance, scrub + btrbk # Snapshot management + rotation + ]; + + # LUKS2 unlock at boot (uses keyfile or prompts if unavailable) + # Since the disk may be disconnected, initrd times out gracefully (~30s) + boot.initrd.luks.devices.${luksName} = { + device = "/dev/disk/by-uuid/${cfg.luksUuid}"; + preLVM = false; + allowDiscards = true; + }; + + # Mount the unlocked mapper device as btrfs + fileSystems.${cfg.mountPoint} = { + device = "/dev/mapper/${luksName}"; + fsType = "btrfs"; + options = cfg.btrfsOptions; + }; + + # btrbk — automated snapshot creation and rotation + services.btrbk = mkIf cfg.btrbk.enable { + instances.poup16t = { + onCalendar = cfg.btrbk.schedule; + settings = { + snapshot_preserve_min = cfg.btrbk.preserveMin; + snapshot_preserve = cfg.btrbk.preserve; + + volume.${cfg.mountPoint} = { + snapshot_create = "always"; + snapshot_dir = cfg.btrbk.snapshotDir; + subvolume = "."; + }; + }; + }; + }; + }; +} diff --git a/modules/nixos/hardware/uconsole-cm5-aio-v2.nix b/modules/nixos/hardware/uconsole-cm5-aio-v2.nix new file mode 100644 index 0000000..cd4047f --- /dev/null +++ b/modules/nixos/hardware/uconsole-cm5-aio-v2.nix @@ -0,0 +1,170 @@ +{ 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.raspberrypi-utils}/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; + buildInputs = [ pkgs.python3 ]; + + 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 + raspberrypi-utils # 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"; + }; + }; + }; +} diff --git a/modules/nixos/services/clamav.nix b/modules/nixos/services/clamav.nix new file mode 100644 index 0000000..a93b162 --- /dev/null +++ b/modules/nixos/services/clamav.nix @@ -0,0 +1,240 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.gortium.clamav; + clamavPkg = pkgs.clamav; + + clamdConfig = pkgs.writeText "clamd.conf" '' + LogFile /var/log/clamav/clamd.log + LogTime yes + LogVerbose yes + LogSyslog yes + LocalSocket /run/clamav/clamd.sock + TCPSocket 3310 + TCPAddr 127.0.0.1 + User clamav + AllowSupplementaryGroups yes + ${cfg.clamdExtraConfig} + ''; + + freshclamConfig = pkgs.writeText "freshclam.conf" '' + DatabaseDirectory /var/lib/clamav + LogFile /var/log/clamav/freshclam.log + LogTime yes + LogVerbose yes + LogSyslog yes + User clamav + AllowSupplementaryGroups yes + ${cfg.freshclamExtraConfig} + ''; + + # Daily scan — logging only, no auto-quarantine/delete + scanScript = pkgs.writeShellScript "clamav-daily-scan" '' + set -e + PATHS="${concatStringsSep " " cfg.scanPaths}" + if [ -z "$PATHS" ]; then + echo "No paths configured for daily scan" + exit 0 + fi + echo "=== ClamAV daily scan started: $(date) ===" + ${clamavPkg}/bin/clamdscan --fdpass --log=/var/log/clamav/daily-scan.log --no-summary $PATHS + echo "=== ClamAV daily scan finished: $(date) ===" + ''; +in +{ + ##### Options ##### + options.gortium.clamav = { + enable = mkEnableOption "ClamAV antivirus — installs clamav CLI tools"; + + enableDaemon = mkOption { + type = types.bool; + default = true; + description = '' + Run clamd daemon + freshclam updater + daily scheduled scan. + Set to false on machines where you only want the CLI tools + (clamscan, clamdscan) for manual on-demand scanning. + ''; + }; + + onAccessScanning = mkOption { + type = types.bool; + default = false; + description = '' + Enable on-access scanning via clamonacc (fanotify-based). + Resource-heavy; server use only. Requires enableDaemon = true. + ''; + }; + + scanPaths = mkOption { + type = types.listOf types.str; + default = [ + "/home" + "/nix/store" + "/var/lib" + "/etc" + "/tmp" + "/var/tmp" + ]; + description = "Paths for the daily scheduled scan."; + }; + + dailyScanTime = mkOption { + type = types.str; + default = "daily"; + description = '' + When to run the daily scan. systemd calendar expression + or shortcuts like "daily", "weekly", "04:00". + ''; + }; + + clamdExtraConfig = mkOption { + type = types.lines; + default = ""; + description = "Extra lines appended to clamd.conf"; + }; + + freshclamExtraConfig = mkOption { + type = types.lines; + default = ""; + description = "Extra lines appended to freshclam.conf"; + }; + }; + + ##### Implementation ##### + config = mkIf cfg.enable { + # 1. Package — always installed when enable = true + environment.systemPackages = [ clamavPkg ]; + + # Everything below uses mkIf cfg.enableDaemon — conditionalized per attribute + + # 2. Users/groups (only if daemon runs) + users.users.clamav = mkIf cfg.enableDaemon { + isSystemUser = true; + group = "clamav"; + home = "/var/lib/clamav"; + createHome = true; + description = "ClamAV daemon user"; + }; + users.groups.clamav = mkIf cfg.enableDaemon {}; + + # 3. Directories (only if daemon runs) + systemd.tmpfiles.rules = mkIf cfg.enableDaemon [ + "d /var/lib/clamav 0750 clamav clamav -" + "d /var/log/clamav 0750 clamav clamav -" + "d /run/clamav 0755 clamav clamav -" + ]; + + # 4. ClamAV daemon (clamd) + systemd.services.clamav-daemon = mkIf cfg.enableDaemon { + description = "ClamAV Anti-Virus Daemon"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ clamavPkg ]; + + preStart = '' + mkdir -p /var/lib/clamav /var/log/clamav /run/clamav + chown clamav:clamav /var/lib/clamav /var/log/clamav /run/clamav + ''; + + serviceConfig = { + Type = "simple"; + ExecStart = "${clamavPkg}/bin/clamd --config-file=${clamdConfig}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + Restart = "on-failure"; + RestartSec = "10s"; + User = "clamav"; + Group = "clamav"; + RuntimeDirectory = "clamav"; + RuntimeDirectoryMode = "0755"; + StateDirectory = "clamav"; + StateDirectoryMode = "0750"; + LogsDirectory = "clamav"; + LogsDirectoryMode = "0750"; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ + "/var/lib/clamav" + "/var/log/clamav" + "/run/clamav" + ]; + NoNewPrivileges = true; + }; + }; + + # 5. freshclam (database updater) — hourly via timer + systemd.services.clamav-freshclam = mkIf cfg.enableDaemon { + description = "ClamAV Virus Database Updater"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ clamavPkg pkgs.curl ]; + + serviceConfig = { + Type = "oneshot"; + ExecStart = "${clamavPkg}/bin/freshclam --config-file=${freshclamConfig} --daemon-notify=${clamdConfig}"; + User = "clamav"; + Group = "clamav"; + PrivateTmp = true; + ProtectSystem = "full"; + NoNewPrivileges = true; + }; + }; + systemd.timers.clamav-freshclam = mkIf cfg.enableDaemon { + description = "ClamAV database update timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "hourly"; + Persistent = true; + RandomizedDelaySec = "1800"; + }; + }; + + # 6. Daily scan — logging only, no auto-quarantine + systemd.services.clamav-daily-scan = mkIf cfg.enableDaemon { + description = "ClamAV Daily Scheduled Scan"; + after = [ "clamav-daemon.service" ]; + requires = [ "clamav-daemon.service" ]; + path = [ clamavPkg ]; + + serviceConfig = { + Type = "oneshot"; + ExecStart = "${scanScript}"; + User = "clamav"; + Group = "clamav"; + PrivateTmp = true; + ProtectSystem = "strict"; + ReadWritePaths = [ "/var/log/clamav" ]; + }; + }; + systemd.timers.clamav-daily-scan = mkIf cfg.enableDaemon { + description = "ClamAV daily scan timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.dailyScanTime; + Persistent = true; + }; + }; + + # 7. On-access scanning (clamonacc) — needs enableDaemon + systemd.services.clamav-onaccess = mkIf (cfg.enableDaemon && cfg.onAccessScanning) { + description = "ClamAV On-Access Scanner (clamonacc)"; + after = [ "clamav-daemon.service" ]; + requires = [ "clamav-daemon.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ clamavPkg ]; + + serviceConfig = { + Type = "simple"; + ExecStart = "${clamavPkg}/bin/clamonacc --config-file=${clamdConfig} --fdpass --log=/var/log/clamav/clamonacc.log"; + Restart = "on-failure"; + RestartSec = "10s"; + User = "root"; # clamonacc needs root for fanotify + Group = "root"; + PrivateTmp = true; + NoNewPrivileges = true; + }; + }; + }; +} diff --git a/modules/nixos/services/default.nix b/modules/nixos/services/default.nix new file mode 100644 index 0000000..f741778 --- /dev/null +++ b/modules/nixos/services/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./systemd + ]; +} diff --git a/modules/nixos/services/remote-builder.nix b/modules/nixos/services/remote-builder.nix new file mode 100644 index 0000000..1dfab7f --- /dev/null +++ b/modules/nixos/services/remote-builder.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.remoteBuilder; +in { + options.services.remoteBuilder = { + enable = lib.mkEnableOption "remote Nix build machine (lazyworkhorse server)"; + + buildMachine = { + host = lib.mkOption { + type = lib.types.str; + default = "lazyworkhorse.net"; + description = "Hostname or IP of the remote build machine."; + }; + sshUser = lib.mkOption { + type = lib.types.str; + default = "ai-worker"; + description = "SSH user on the remote build machine."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 2424; + description = "SSH port — added via ~root/.ssh/config since nix.buildMachines has no sshPort option."; + }; + systems = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "aarch64-linux" "x86_64-linux" ]; + description = "System types the remote builder can build for."; + }; + maxJobs = lib.mkOption { + type = lib.types.int; + default = 16; + description = "Max parallel jobs on the remote builder."; + }; + supportedFeatures = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "big-parallel" "nixos-test" "benchmark" ]; + description = "Features the remote builder supports."; + }; + }; + + fallbackLocal = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Fall back to local build when remote builder is unreachable."; + }; + }; + + config = lib.mkIf cfg.enable { + nix.distributedBuilds = true; + nix.buildMachines = [{ + hostName = cfg.buildMachine.host; + sshUser = cfg.buildMachine.sshUser; + systems = cfg.buildMachine.systems; + maxJobs = cfg.buildMachine.maxJobs; + supportedFeatures = cfg.buildMachine.supportedFeatures; + }]; + + nix.extraOptions = lib.optionalString cfg.fallbackLocal '' + builders-use-substitutes = true + fallback = true + ''; + + # SSH config for the remote builder (since nix.buildMachines has no port option) + programs.ssh.extraConfig = '' + Host ${cfg.buildMachine.host} + HostName ${cfg.buildMachine.host} + Port ${toString cfg.buildMachine.port} + User ${cfg.buildMachine.sshUser} + ''; + }; +} diff --git a/modules/nixos/services/staging-vm.nix.bak b/modules/nixos/services/staging-vm.nix.bak new file mode 100755 index 0000000..91bf667 --- /dev/null +++ b/modules/nixos/services/staging-vm.nix.bak @@ -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 = '' + + default + 2b8f7a3c-9e5d-4a1f-bc3d-6e7a8f9b0c1d + + + + + + + + + + + + + + ''; + # 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 + '') ]; + }; +} diff --git a/modules/nixos/services/wireguard-client.nix b/modules/nixos/services/wireguard-client.nix new file mode 100644 index 0000000..0ed3951 --- /dev/null +++ b/modules/nixos/services/wireguard-client.nix @@ -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 ]; + }; +} diff --git a/overlays/reticulum.nix b/overlays/reticulum.nix new file mode 100644 index 0000000..e646559 --- /dev/null +++ b/overlays/reticulum.nix @@ -0,0 +1,81 @@ +final: prev: let + python3 = final.python3; + pyPkgs = python3.pkgs; +in { + reticulumStack = python3.pkgs.buildPythonApplication rec { + pname = "reticulum"; + version = "1.2.9"; + format = "setuptools"; + src = pyPkgs.fetchPypi { + pname = "rns"; + inherit version; + sha256 = "554814231c237b9caacf8df669312e57dd7d3f84b6d4810125087d1a79a75d75"; + }; + propagatedBuildInputs = with pyPkgs; [ cryptography pyserial ]; + doCheck = false; + pythonImportsCheck = [ "RNS" ]; + meta = with final.lib; { + description = "Self-configuring, encrypted and resilient mesh networking stack"; + homepage = "https://reticulum.network/"; + license = licenses.mit; + platforms = platforms.linux; + }; + }; + + lxmf = python3.pkgs.buildPythonApplication rec { + pname = "lxmf"; + version = "0.9.8"; + format = "setuptools"; + src = pyPkgs.fetchPypi { + inherit pname version; + sha256 = "30f39f3a975a049c12ee2cfceb3261d24cb5adec881c6821f7354464b3f3650c"; + }; + propagatedBuildInputs = [ final.reticulumStack ]; + doCheck = false; + pythonImportsCheck = [ "LXMF" ]; + meta = with final.lib; { + description = "Lightweight Extensible Message Format for Reticulum"; + homepage = "https://github.com/markqvist/lxmf"; + license = licenses.mit; + platforms = platforms.linux; + }; + }; + + nomadnet = python3.pkgs.buildPythonApplication rec { + pname = "nomadnet"; + version = "1.1.1"; + format = "setuptools"; + src = pyPkgs.fetchPypi { + inherit pname version; + sha256 = "fa13b64a10e75b705a58024815ab72451700aa726af96d415ba99dec28dfc40a"; + }; + propagatedBuildInputs = with pyPkgs; [ final.reticulumStack final.lxmf urwid qrcode ]; + doCheck = false; + pythonImportsCheck = [ "nomadnet" ]; + meta = with final.lib; { + description = "Nomad Network — resilient mesh communications platform"; + homepage = "https://github.com/markqvist/NomadNet"; + license = licenses.mit; + platforms = platforms.linux; + }; + }; + + rnsh = python3.pkgs.buildPythonApplication rec { + pname = "rnsh"; + version = "0.1.7"; + format = "setuptools"; + src = pyPkgs.fetchPypi { + inherit pname version; + sha256 = "9cb72f25abb1c6d300f8014b264184ff78f592fe88e36094938012990b797c93"; + }; + propagatedBuildInputs = [ final.reticulumStack ]; + doCheck = false; + pythonImportsCheck = [ "rnsh" ]; + meta = with final.lib; { + description = "Remote shell over Reticulum"; + homepage = "https://github.com/acehoss/rnsh"; + license = licenses.mit; + platforms = platforms.linux; + }; + }; +} diff --git a/patches/0008-panel-cwu50-fix-init-seq1.patch b/patches/0008-panel-cwu50-fix-init-seq1.patch new file mode 100644 index 0000000..e2bb8fc --- /dev/null +++ b/patches/0008-panel-cwu50-fix-init-seq1.patch @@ -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)) { \ No newline at end of file diff --git a/result-uconsole b/result-uconsole new file mode 120000 index 0000000..e5d8532 --- /dev/null +++ b/result-uconsole @@ -0,0 +1 @@ +/nix/store/7y7rfksqcf5smz59jjixyl56bxq50j9g-nixos-system-uConsole-25.11.20260608.e820eb4 \ No newline at end of file diff --git a/secrets/gortium_password.age b/secrets/gortium_password.age new file mode 100644 index 0000000..3594b19 --- /dev/null +++ b/secrets/gortium_password.age @@ -0,0 +1,10 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA4MlFz +SHFjYjJMVHRlTWNGVGI2bHQxc0xRd2tlaExlM0NFMWhlbkR2bVg0CkxxenVTaXkr +eWxybDdCeUM0ejRvZWI4cFZCWm5VczRvZkNnT0d5Y1oyYmsKLT4gK1NmRzVtLWdy +ZWFzZSB3UDI6TyNaCnF4Ylk0QWduaXZxRFBFbDBOZ0dxeGxiWTVCYjRtZTJBRkFC +YU5qaytYWWI4OWl1K1FSdXNlY2JXZjkzak9tTHkKVFlCRlRqY1FVSzFmNS9yZmxF +aEUxelUwNEpKN3VXYi9KUWN4bXFscm5oUEFOajhRZDlERWVYcFgvQQotLS0gK1JI +VERTQjB6d1k3NDQwbjNveXBqcFk1WE96cHlaTTVkTWRMZENPamFJZwpcT1CP/KvU +CsunvfX9RBlSSKuw4eem9N9s3JqJNj4FRQizNx6QzlE1vSME +-----END AGE ENCRYPTED FILE----- diff --git a/secrets/home_wifi.age b/secrets/home_wifi.age new file mode 100644 index 0000000..0e41fb7 --- /dev/null +++ b/secrets/home_wifi.age @@ -0,0 +1,10 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSAycE1Y +YmMvUWZpK2VKQVlqaHFtaERBRGROcFIyL0d6dEVRQmFxLzlqdFZNCkYxWkNIUXRZ +V0dQOG4zY3U3Nk1JelBtY0cwUGdxaEI3dmZaVTZId04rVTQKLT4geV1cZC4wMnst +Z3JlYXNlIDYgOG1IME1xCkQ0RGN1NU1FUWk0Y1RmamNEY0tJWmFQNGdoMkROcGVy +aU5UYVFobVRLMVVUQ1JicUM2c0tSVzRQdEZ0VE5YamQKZUxPeVpLWDZJR0hqemdD +cmkyUUdFZEZKZjBDNGhmNFR6bVUKLS0tIDRQUGR5RGI5UEhGNk5EQWw4dFk0R01k +TUJWOFpleXBUajFPckFmem52cGsKHzn+QnuYLI2NEh5WWZQHrNuvVzYk+kVjsAsn +KNS2dHjvadAopVY2Gypldf1p2RRtmgZkDHaPlNzv5Hk= +-----END AGE ENCRYPTED FILE----- diff --git a/secrets/secrets.nix b/secrets/secrets.nix index d5c44d6..cf42b6c 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -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; diff --git a/users/ai-worker.nix b/users/ai-worker/ai-worker.nix similarity index 98% rename from users/ai-worker.nix rename to users/ai-worker/ai-worker.nix index 6308151..8354681 100644 --- a/users/ai-worker.nix +++ b/users/ai-worker/ai-worker.nix @@ -22,7 +22,6 @@ # - 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/ - services.aiWorkerAccess = true; # Restricted sudo for ai-worker - security checks only security.sudo.extraRules = [ diff --git a/users/gortium.nix b/users/gortium/gortium.nix similarity index 68% rename from users/gortium.nix rename to users/gortium/gortium.nix index 20df0ea..b25d877 100644 --- a/users/gortium.nix +++ b/users/gortium/gortium.nix @@ -1,4 +1,8 @@ { pkgs, inputs, config, keys, ... }: { + home-manager.extraSpecialArgs = { + inherit (config.networking) hostName; + }; + home-manager.users.gortium = import ./home.nix; users.users.gortium = { isNormalUser = true; extraGroups = [ "wheel" "docker" "video" "render"]; @@ -6,9 +10,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 ]; diff --git a/users/gortium/home.nix b/users/gortium/home.nix new file mode 100644 index 0000000..166d9e2 --- /dev/null +++ b/users/gortium/home.nix @@ -0,0 +1,91 @@ +{ pkgs, lib, config, inputs, hostName, ... }: + +let + isUconsole = hostName == "uConsole"; + dotfiles = ../../assets/dotfiles; +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 + + # wallpapers + ".config/wallpapers".source = "${dotfiles}/wallpapers/.config/wallpapers"; + ".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/hosts/monitors.conf".source = + if isUconsole + then "${dotfiles}/hypr/.config/hypr/hosts/uconsole.conf" + else "${dotfiles}/hypr/.config/hypr/hosts/laptop.conf"; + }; + + programs.bash.enable = true; + programs.zsh = { + enable = true; + initExtra = builtins.readFile "${dotfiles}/zsh/.zshrc"; + }; + + home.packages = with pkgs; [ + git zsh tmux starship + neovim kitty + btop yazi ripgrep fd fzf + htop unzip wget jq + hyprland hyprlock hypridle hyprpaper + waybar wofi dunst + libnotify mako + swaynotificationcenter + swww + emacs + udiskie + hyprshade + networkmanagerapplet + pavucontrol + ]; + + xdg.userDirs = { + enable = true; + createDirectories = true; + desktop = "$HOME/desktop"; + documents = "$HOME/documents"; + download = "$HOME/downloads"; + music = "$HOME/music"; + pictures = "$HOME/pictures"; + publicShare = "$HOME/public"; + templates = "$HOME/templates"; + videos = "$HOME/videos"; + }; +}