Compare commits

..

2 Commits

Author SHA1 Message Date
2c5dc3d58d feat: comprehensive NixOS deployment infrastructure
- docs/nix-container-install.md: 474-line guide covering Determinate Systems
  installer, vanilla Nix, NixOS base image, architecture notes (x86_64 vs aarch64),
  cross-compilation, container considerations, troubleshooting
- scripts/deploy.sh: 286-line deployment script with pre-flight checks, git sync,
  build validation (nix build --no-link), 5 actions (switch/boot/test/build/
  dry-activate), color-coded logging, env-based configurability
- scripts/deploy-ssh-config: SSH config for all 3 hosts with dual users for
  lazyworkhorse, reverse tunnel for cyt-pi, uConsole placeholder, Gitea entry

Full replacements of stub files from previous commit.
2026-05-20 14:29:38 -04:00
8b004c47b9 feat: add NixOS deployment infrastructure
- Nix installation guide for container (docs/nix-container-install.md)
- Deployment helper script (scripts/deploy.sh)
- SSH configuration template (scripts/deploy-ssh-config)
- Deployment skill for Hermes (skills/nixos-deploy/)

Enables remote NixOS deployment from Hermes container to target hosts
via SSH with nixos-rebuild --target-host.

Usage:
  ./scripts/deploy.sh <hostname> [branch] [action]

Supported hosts:
  - lazyworkhorse (x86_64)
  - cyt-pi (aarch64)
  - uConsole (aarch64) - config pending
2026-04-30 00:06:12 +00:00
41 changed files with 1050 additions and 2669 deletions

4
.gitmodules vendored
View File

@@ -1,7 +1,3 @@
[submodule "assets/compose"] [submodule "assets/compose"]
path = assets/compose path = assets/compose
url = ssh://git@code.lazyworkhorse.net:2222/gortium/compose.git 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

View File

@@ -5,7 +5,6 @@ This document outlines the development conventions for this NixOS-based infrastr
## Build & Deployment ## Build & Deployment
- **Build/Deploy:** Use `nixos-rebuild switch --flake .#<hostname>` to build and deploy the configuration for a specific host. - **Build/Deploy:** Use `nixos-rebuild switch --flake .#<hostname>` to build and deploy the configuration for a specific host.
- **CRITICAL — Validate before pushing:** Always `nix build --no-link '.#nixosConfigurations.<hostname>.config.system.build.toplevel'` (or `nh os build`) and confirm it succeeds before pushing any changes. Never push untested NixOS configs.
- **Development Shell:** Activate the development environment with `nix develop`. - **Development Shell:** Activate the development environment with `nix develop`.
## Linting & Formatting ## Linting & Formatting

Submodule assets/dotfiles deleted from 3c06c3938c

View File

@@ -0,0 +1,474 @@
# Nix Installation for Hermes Agent Container
This guide covers several approaches for installing Nix in the Hermes Agent Docker
container to enable remote NixOS deployment via `nixos-rebuild`. It covers both
x86_64 (lazyworkhorse) and aarch64 (cyt-pi, uConsole) architectures.
## Table of Contents
1. [Why Nix in a Container?](#why-nix-in-a-container)
2. [Prerequisites](#prerequisites)
3. [Installation Methods](#installation-methods)
- [Method A: Determinate Systems Installer](#method-a-determinate-systems-installer-recommended)
- [Method B: Vanilla Nix Installer](#method-b-vanilla-nix-installer)
- [Method C: NixOS-Based Container Image](#method-c-nixos-based-container-image)
4. [Architecture-Specific Notes](#architecture-specific-notes)
- [x86_64 (lazyworkhorse)](#x86_64-lazyworkhorse)
- [aarch64 (cyt-pi, uConsole)](#aarch64-cyt-pi-uconsole)
- [Cross-Compilation](#cross-compilation)
5. [Post-Install Configuration](#post-install-configuration)
6. [Verification](#verification)
7. [Container-Specific Considerations](#container-specific-considerations)
- [Persistence](#persistence)
- [Disk Space](#disk-space)
- [Security](#security)
- [Resource Constraints](#resource-constraints)
8. [Integration with deploy.sh](#integration-with-deploysh)
9. [Troubleshooting](#troubleshooting)
10. [References](#references)
---
## Why Nix in a Container?
The Hermes Agent container runs on an Ubuntu/Debian base. To deploy NixOS
configurations to remote hosts, we need:
- `nix` — the Nix package manager (for building configurations)
- `nixos-rebuild` — the NixOS deployment tool
- Access to the infra repo with flake configuration
Installing Nix inside the container avoids:
- Host-level Nix installation on the Docker host
- Cross-container volume mounts of /nix/store
- Dependencies on the host's Nix daemon (which may be a different version)
## Prerequisites
- Docker host running Linux (x86_64 and/or aarch64)
- Container base: Debian/Ubuntu (apt-based)
- 1-2 GB additional disk space for Nix store
- Network access to cache.nixos.org (or a local binary cache)
- Git access to the infra repository
## Installation Methods
### Method A: Determinate Systems Installer (Recommended)
The Determinate Systems installer is the recommended approach. It is non-interactive,
sets up flakes by default, and handles multi-user installation cleanly.
**Dockerfile additions:**
```dockerfile
# Install Nix (Determinate Systems installer)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
# Download and run Nix installer (non-interactive)
RUN curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix \
-o /tmp/nix-install.sh \
&& chmod +x /tmp/nix-install.sh \
&& sh /tmp/nix-install.sh install --no-confirm \
&& rm /tmp/nix-install.sh
# Configure Nix for flakes
RUN mkdir -p /root/.config/nix \
&& echo 'experimental-features = nix-command flakes' > /root/.config/nix/nix.conf
# Add Nix to PATH for all users
ENV PATH="/nix/var/nix/profiles/default/bin:$PATH"
```
**Pros:**
- Fully non-interactive (--no-confirm)
- Enables flakes automatically
- Sets up multi-user daemon
- Auto-selects correct architecture
- Handles upgrades gracefully
**Cons:**
- Downloads ~100 MB installer
- Requires systemd in container (works with --privileged or cgroupv2)
- Daemon mode may conflict with container exit semantics
**Container runtime additions:**
For the Nix daemon to work properly inside a container, you may need:
```dockerfile
# Ensure /nix is a volume for persistence
VOLUME /nix
# Or mount tmpfs for ephemeral builds:
# docker run --tmpfs /nix:exec,size=4G ...
```
### Method B: Vanilla Nix Installer
The official single-user Nix installer is lighter but requires manual flake setup.
**Dockerfile additions:**
```dockerfile
# Install Nix (single-user, official installer)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
sudo \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
# Install Nix as root (single-user)
RUN curl -L https://nixos.org/nix/install -o /tmp/nix-install.sh \
&& chmod +x /tmp/nix-install.sh \
&& sh /tmp/nix-install.sh --no-daemon \
&& rm /tmp/nix-install.sh
# Enable flakes
RUN mkdir -p /root/.config/nix \
&& echo 'experimental-features = nix-command flakes' > /root/.config/nix/nix.conf
# Source Nix in shell
RUN echo '. /root/.nix-profile/etc/profile.d/nix.sh' >> /root/.bashrc
ENV PATH="/root/.nix-profile/bin:$PATH"
```
**Pros:**
- Smaller installer
- No daemon needed (single-user mode)
- Works in containers without systemd
- Simpler container lifecycle
**Cons:**
- Manual flake configuration required
- Single-user only (no multi-user isolation)
- PATH must be set manually
- No automatic garbage collection
### Method C: NixOS-Based Container Image
For maximum isolation, use an official NixOS base image for the build stage.
**Multi-stage Dockerfile:**
```dockerfile
# Build stage: NixOS builder
FROM nixos/nix:latest AS builder
COPY infra /infra
WORKDIR /infra
# Build the configuration once
RUN nix build '.#nixosConfigurations.lazyworkhorse.config.system.build.toplevel'
# Final stage: Hermes container
FROM ubuntu:22.04
# Copy the Nix closure and binary cache
COPY --from=builder /nix /nix
# ... rest of Hermes setup
```
**Pros:**
- Purely declarative build environment
- No installation at runtime
- Easy to pin Nix version
- Good for CI/CD pipelines
**Cons:**
- Requires multi-stage Docker build
- Larger initial image build
- Harder to update Nix version at runtime
- Overkill if Nix is only needed for `nixos-rebuild`
---
## Architecture-Specific Notes
### x86_64 (lazyworkhorse)
The Hermes container likely runs on x86_64 hardware for the primary server.
Nix will download x86_64 binaries from cache.nixos.org by default.
**No special configuration needed** — the standard installer works out of the box.
If the container is running on an AMD Ryzen/EPYC or Intel Xeon, consider:
```bash
# Enable CPU-specific optimizations (optional)
echo 'extra-platforms = x86_64-v1 x86_64-v2 x86_64-v3' >> /root/.config/nix/nix.conf
```
### aarch64 (cyt-pi, uConsole)
When building for aarch64 targets from an x86_64 container, you need either:
1. Remote builder (aarch64 machine does the build), or
2. QEMU-based emulation (slower but self-contained), or
3. Build directly on the aarch64 target using `--build-host`
**For QEMU emulation in the container:**
```dockerfile
# Enable binfmt for aarch64 emulation
RUN apt-get update && apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support \
&& rm -rf /var/lib/apt/lists/*
# Register aarch64 binfmt
RUN update-binfmts --enable qemu-aarch64
```
**Container runtime (for QEMU):**
```bash
docker run --privileged --rm ... hermes-agent
# Or with specific capability:
docker run --cap-add=SYS_ADMIN --security-opt seccomp=unconfined ... hermes-agent
```
### Cross-Compilation
For native cross-compilation (without emulation), add to your Nix configuration:
```nix
# In your flake.nix or nix.conf
{
nix.settings.extra-platforms = [ "aarch64-linux" "x86_64-linux" ];
nix.settings.extra-sandbox-paths = [ ];
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
}
```
Or in `nix.conf`:
```
extra-platforms = x86_64-linux aarch64-linux
extra-sandbox-paths =
```
---
## Post-Install Configuration
### nix.conf for Container Usage
Recommended `/root/.config/nix/nix.conf`:
```ini
experimental-features = nix-command flakes
substituters = https://cache.nixos.org/
trusted-users = root
max-jobs = auto
cores = 0
sandbox = false
```
Note: `sandbox = false` is needed inside containers that lack full sandbox
support. This is safe in a single-tenant container environment.
### PATH Setup
Add to your Dockerfile:
```dockerfile
ENV PATH="/nix/var/nix/profiles/default/bin:/root/.nix-profile/bin:${PATH}"
```
### Shell Integration
```dockerfile
RUN echo 'source /root/.nix-profile/etc/profile.d/nix.sh' >> /root/.bashrc
```
---
## Verification
After installation, verify with:
```bash
# Check Nix is available
nix --version
# Check nixos-rebuild
nixos-rebuild --help | head -3
# Verify flakes are enabled
nix flake --help
# Test a build (must be in infra repo)
cd /opt/data/infra
nix build --no-link '.#nixosConfigurations.lazyworkhorse.config.system.build.toplevel'
# Check available systems
nix eval --impure --expr 'builtins.currentSystem'
```
---
## Container-Specific Considerations
### Persistence
The `/nix` directory should be a Docker volume to avoid re-downloading
packages on every container restart:
```yaml
# docker-compose.yml
volumes:
- nix-store:/nix
volumes:
nix-store:
```
Without persistence, every container restart requires re-downloading the
entire Nix store (~500 MB - 2 GB depending on packages used).
### Disk Space
The Nix store grows over time as old generations accumulate. Set up garbage
collection:
```bash
# Manual GC
nix store gc
# Remove old generations
nix-collect-garbage --delete-older-than 30d
# Automatic GC (in nix.conf)
# Currently not supported in nix.conf, but you can run a cron job:
# nix store gc --max 10G
```
In Docker, limit store growth with:
```dockerfile
# Configure max store size
RUN mkdir -p /etc/nix && \
echo 'min-free = 5368709120' > /etc/nix/nix.conf # Keep 5GB free
```
### Security
Running Nix in a container introduces some security considerations:
1. **Sandboxing:** `sandbox = false` disables build isolation. In a multi-tenant
container, this means Nix builds can affect the container filesystem.
**Mitigation:** Only build configs you trust (your own infra repo).
2. **Network access:** The container needs outbound access to cache.nixos.org.
If using a restricted network, set up a local binary cache:
```nix
substituters = https://cache.nixos.org/ https://nix-cache.internal/
```
3. **Privileged mode:** QEMU emulation for aarch64 builds may need `--privileged`
or `--security-opt seccomp=unconfined`. This reduces container isolation.
**Mitigation:** Use remote builders or build natively on the target.
4. **Supply chain:** Nix derivations pin exact inputs via hashes. Verify
flake.lock is committed and reviewed.
### Resource Constraints
Nix builds can be memory and CPU intensive:
```nix
# Limit build parallelism in nix.conf
max-jobs = 2
cores = 4
# Or set per-build:
# nix build --max-jobs 2 --cores 4
```
For containers with limited memory (< 2 GB), consider:
- Building on the target host instead (`--build-host`)
- Using the deploy script's `build` action separately
---
## Integration with deploy.sh
The deployment script at `scripts/deploy.sh` expects:
1. **Nix installed** with flakes enabled
2. **SSH key** at `/opt/data/home/.ssh/id_hermes_gitea` (or via SSH_KEY env)
3. **Infra repo** cloned at the script's parent directory
4. **Network access** to:
- `code.lazyworkhorse.net:2222` (Gitea for git operations)
- Target hosts via SSH (see deploy-ssh-config)
- `cache.nixos.org` or a local substitute
Typical usage from Hermes:
```bash
# Full deployment
./scripts/deploy.sh lazyworkhorse master switch
# Build-only check (no remote deployment)
./scripts/deploy.sh cyt-pi master build
# Dry run
./scripts/deploy.sh uConsole feat/test dry-activate
# Override SSH key
SSH_KEY=/opt/data/home/.ssh/my-custom-key ./deploy.sh lazyworkhorse
```
---
## Troubleshooting
### "nix: command not found"
- Ensure Nix is installed and PATH is set:
```bash
export PATH="/nix/var/nix/profiles/default/bin:/root/.nix-profile/bin:$PATH"
```
- Check installation: `ls -la /nix/` should exist
- Re-source profile: `. /root/.nix-profile/etc/profile.d/nix.sh`
### "error: unable to download ... cache.nixos.org"
- Check network connectivity: `ping cache.nixos.org`
- Check DNS resolution from inside the container
- If behind a proxy, set `http_proxy` / `https_proxy` environment variables
### "sandbox: cannot run build in sandbox"
- Add `sandbox = false` to nix.conf
- Or run container with `--privileged` or `--security-opt seccomp=unconfined`
### "aarch64-linux builds fail on x86_64"
- QEMU binfmt not registered. Check: `ls /proc/sys/fs/binfmt_misc/`
- Rebuild QEMU registration: `docker run --privileged --rm tonistiigi/binfmt --install all`
- Or use `--build-host` to build on the target directly
### "nixos-rebuild fails with SSH errors"
- Verify SSH key exists and has correct permissions:
```bash
ls -la /opt/data/home/.ssh/id_hermes_gitea
chmod 600 /opt/data/home/.ssh/id_hermes_gitea
```
- Test SSH manually: `ssh -p 2424 -i /opt/data/home/.ssh/id_hermes_gitea ai-worker@lazyworkhorse.net`
- Check target host is reachable: `nc -zv lazyworkhorse.net 2424`
### "git fetch fails from Gitea"
- Verify GIT_SSH_COMMAND is set: `echo $GIT_SSH_COMMAND`
- Test git SSH: `ssh -T git@code.lazyworkhorse.net -p 2222`
- Check the infra repo remote: `git remote -v`
---
## References
- [Determinate Systems Nix Installer](https://github.com/DeterminateSystems/nix-installer)
- [NixOS Manual: Installation](https://nixos.org/manual/nix/stable/installation/)
- [NixOS Wiki: Flakes](https://nixos.wiki/wiki/Flakes)
- [NixOS Wiki: nixos-rebuild](https://nixos.wiki/wiki/Nixos-rebuild)
- [NixOS Wiki: Cross Compilation](https://nixos.wiki/wiki/Cross_Compilation)
- [Multi-arch Docker with QEMU](https://github.com/multiarch/qemu-user-static)

301
flake.lock generated
View File

@@ -23,84 +23,7 @@
"type": "github" "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-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, "flake": false,
"locked": { "locked": {
"lastModified": 1751685974, "lastModified": 1751685974,
@@ -114,64 +37,6 @@
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz" "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": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -193,37 +58,16 @@
"type": "github" "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": { "lix": {
"inputs": { "inputs": {
"flake-compat": "flake-compat_2", "flake-compat": "flake-compat",
"nix2container": "nix2container", "nix2container": "nix2container",
"nix_2_18": "nix_2_18", "nix_2_18": "nix_2_18",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ],
"nixpkgs-regression": "nixpkgs-regression", "nixpkgs-regression": "nixpkgs-regression",
"pre-commit-hooks": "pre-commit-hooks_2" "pre-commit-hooks": "pre-commit-hooks"
}, },
"locked": { "locked": {
"lastModified": 1774721317, "lastModified": 1774721317,
@@ -300,80 +144,6 @@
"type": "github" "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1705033721, "lastModified": 1705033721,
@@ -406,22 +176,6 @@
"type": "github" "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": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1774386573, "lastModified": 1774386573,
@@ -439,29 +193,6 @@
} }
}, },
"pre-commit-hooks": { "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, "flake": false,
"locked": { "locked": {
"lastModified": 1769939035, "lastModified": 1769939035,
@@ -480,13 +211,8 @@
"root": { "root": {
"inputs": { "inputs": {
"agenix": "agenix", "agenix": "agenix",
"agenix-rekey": "agenix-rekey",
"home-manager": "home-manager_2",
"lix": "lix", "lix": "lix",
"nixos-raspberrypi": "nixos-raspberrypi", "nixpkgs": "nixpkgs_2"
"nixos-uconsole": "nixos-uconsole",
"nixpkgs": "nixpkgs_2",
"nixpkgs-uconsole": "nixpkgs-uconsole"
} }
}, },
"systems": { "systems": {
@@ -503,27 +229,6 @@
"repo": "default", "repo": "default",
"type": "github" "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", "root": "root",

339
flake.nix
View File

@@ -8,35 +8,14 @@
inputs.darwin.follows = ""; inputs.darwin.follows = "";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
agenix-rekey = {
url = "github:oddlama/agenix-rekey";
inputs.nixpkgs.follows = "nixpkgs";
};
lix = { lix = {
url = "git+https://git.lix.systems/lix-project/lix?ref=main"; url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
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";
};
self.submodules = true; self.submodules = true;
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
}; };
outputs = { self, nixpkgs, agenix, agenix-rekey, lix outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
, nixpkgs-uconsole, nixos-uconsole, nixos-raspberrypi
, home-manager
, ... }@inputs:
let let
system = "x86_64-linux"; system = "x86_64-linux";
keys = import ./lib/keys.nix; keys = import ./lib/keys.nix;
@@ -47,288 +26,60 @@
"/etc/ssh/ssh_host_ed25519_key" "/etc/ssh/ssh_host_ed25519_key"
"/root/.age/bootstrap.key" ]; "/root/.age/bootstrap.key" ];
}; };
overlays = [ agenix.overlays.default (import ./overlays/reticulum.nix) ]; overlays = [ agenix.overlays.default ];
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system overlays; inherit system overlays;
config.allowUnfree = true; config.allowUnfree = true;
config.permittedInsecurePackages = [ "openclaw-2026.3.12" ]; config.permittedInsecurePackages = [
"openclaw-2026.3.12"
];
}; };
devShell = import ./shells/nix_dev.nix { devShell = import ./shells/nix_dev.nix {
inherit pkgs system agenix; inherit pkgs system agenix;
}; };
in
{
############################################################################## nixosConfigurations = {
# CROSS-COMPILE WORKAROUNDS — packages that fail aarch64 cross-compile lazyworkhorse = nixpkgs.lib.nixosSystem {
# specialArgs = { inherit system self keys paths inputs; };
# These packages need NATIVE COMPILATION on the uConsole itself (aarch64). modules = [
# They cannot cross-compile from x86_64 for various reasons listed below. {
# We work around them in the overlay until we set up distributed builds nixpkgs.overlays = overlays;
# with the uConsole as a native aarch64 builder. nixpkgs.config.allowUnfree = true;
# nixpkgs.config.rocmSupport = true;
# ==== Cross-compile failures ==== nixpkgs.config.permittedInsecurePackages = [
# "openclaw-2026.3.12"
# libcamera / libcamera-rpi / libpisp: ];
# meta.platforms excludes aarch64. pipewire hard-depends on them in nixos-25.11. nix.package = lix.packages.${system}.default;
# Fix: empty meta.platforms + strip from pipewire buildInputs. }
# agenix.nixosModules.default
# gjs: ./hosts/lazyworkhorse/configuration.nix
# Need native display (GTK3/4 tests) for cross-compile configure. ./hosts/lazyworkhorse/hardware-configuration.nix
# Fix: meson -Dskip_gtk_tests=true. ./modules/nixos/filesystem/hoardingcow-mount.nix
# ./modules/nixos/services/docker_manager.nix
# hyprland: ./modules/nixos/services/open_code_server.nix
# Qt6Quick missing from aarch64 qtdeclarative, breaks hyprland-qt-support. ./modules/nixos/services/ollama_init_custom_models.nix
# Fix: wrapRuntimeDeps=false (Qt UI components disabled, WM still works). ./modules/nixos/services/openclaw_node.nix
# ./users/gortium.nix
# boost.mpi: ./users/ai-worker.nix
# Boost.Build has no b2 architecture alternatives for ARM. ];
# Fix: useMpi=false.
#
# perl-ldap (perlPackages.perlldap):
# Module::Install requires Perl dynamic loading (Fcntl) which is
# unavailable in cross-compiled Perl.
# Fix: stripped from john.s propagatedBuildInputs.
#
# john (John the Ripper):
# Indirectly affected — depends on perl-ldap for perl utility scripts.
# Fix: perl-ldap stripped from propagatedBuildInputs (john still works,
# just loses sha-dump.pl etc. LDAP support).
#
# gss (GNU Generic Security Service):
# autogen.sh fails cross-compile. Pulled by mailutils → emacs-pgtk.
# Fix: emacs withMailutils=false.
#
# emacs-pgtk:
# Indirectly affected — depends on mailutils which depends on gss.
# Fix: withMailutils=false (no mail/IMAP within emacs).
#
# qtquick3d (Qt6):
# Qt::Quick not available in aarch64 cross-compile qtdeclarative.
# cmake skips build, ninja has no install target.
# Fix: removed js8call, switched wireshark → wireshark-cli.
#
# js8call:
# REMOVED from system packages. Depends on Qt6 multimedia → qtquick3d.
#
# wireshark-qt:
# SWITCHED to wireshark-cli. Same Qt6 multimedia → qtquick3d chain.
#
# neovim:
# `libnlua0.so` built for aarch64, luajit (x86_64) tries to load it
# during codegen (preload_nlua.lua). No clean override option.
# Fix: remove from system packages + install via native build
# once uConsole is set up as remote builder.
#
# clamav:
# cmake try_run() + Rust proc-macro can't find native linker in
# cross-compile (cc crate uses cross CC, no cc in PATH for build
# scripts). Chain: clamav → system-path → etc → dbus → polkit.
# Fix: remove from system packages; clamscan available from server.
#
# ==== Remote builder setup (bidirectional) — TODO ====
# To eliminate cross-compile exceptions, set up distributed builds:
# 1. Create a dedicated `builder` user on both hosts (no shell, home=/var/empty)
# 2. Add the same SSH key to both hosts (symmetric)
# 3. On lazyworkhorse — `nix.buildMachines` pointing to uConsole for aarch64-linux
# 4. On uConsole — `nix.buildMachines` pointing to lazyworkhorse for x86_64-linux
# 5. Remove the uconsoleCrossOverlay workarounds above
# 6. Nix auto-dispatches derivations by `system` — no per-package exceptions needed
# Example buildMachines config:
# Server dispatches aarch64 builds to uConsole (4 cores, less power):
# nix.buildMachines = [{
# hostName = "uConsole.local";
# systems = ["aarch64-linux"];
# maxJobs = 4;
# sshUser = "builder";
# sshKey = "/etc/ssh/builder_key";
# }];
# uConsole dispatches x86_64 builds to server (36 cores, 256GB RAM):
# nix.buildMachines = [{
# hostName = "lazyworkhorse.net";
# port = 2424;
# systems = ["x86_64-linux"];
# maxJobs = 36;
# sshUser = "builder";
# sshKey = "/etc/ssh/builder_key";
# }];
# ==== How to build natively on uConsole ====
# To native-compile these on the uConsole:
# 1. Add uConsole as a remote builder (nix.buildMachines)
# 2. Set nix.extra-platforms = [ "aarch64-linux" ] on server
# 3. Remove the overlay workarounds below
# 4. Packages will auto-dispatch to uConsole for native builds
##############################################################################
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; };
boost = prev.boost.override { useMpi = false; };
# perl-ldap cannot cross-compile (Module::Install needs dynamic loading)
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"
'';
});
emacs-pgtk = prev.emacs-pgtk.override { withMailutils = false; };
# perl-ldap fails cross-compile (Module::Install needs dynamic loading)
# Strip it from john deps -- the perl scripts that need it are not critical
john = prev.john.overrideAttrs (old: {
nativeBuildInputs = (old.nativeBuildInputs or []) ++ [
prev.buildPackages.python3 # python3 for opencl_generate_dynamic_loader.py
];
configureFlags = (old.configureFlags or []) ++ [
"ac_cv_prog_PYTHON=${prev.buildPackages.python3}/bin/python3"
];
propagatedBuildInputs = builtins.filter
(x: x?pname && x.pname != "perl-ldap")
(old.propagatedBuildInputs or []);
});
# clamav: removed from system packages (see note above).
};
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" ];
});
};
uconsoleBaseModules = [
{
nixpkgs.buildPlatform = "x86_64-linux";
nixpkgs.hostPlatform = "aarch64-linux";
nixpkgs.config.allowUnfree = true;
boot.loader.raspberry-pi.bootloader = "kernel";
nixpkgs.overlays = [ uconsoleCrossOverlay (import ./overlays/reticulum.nix) ];
}
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-aio-v2.nix
({ 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
agenix-rekey.nixosModules.default
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.nix
./modules/nixos/services/remote-builder.nix
./modules/nixos/services/wireguard-client.nix
./modules/nixos/services/clamav.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
agenix-rekey.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/filesystem/poup-16t-disk.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/clamav.nix
./modules/nixos/services/wireguard-client.nix
./modules/nixos/services/remote-builder.nix
./modules/nixos/security/ai-worker-restricted.nix
./users/gortium/gortium.nix
./users/ai-worker/ai-worker.nix
./users/builder.nix
];
};
cyt-pi = nixpkgs.lib.nixosSystem { cyt-pi = nixpkgs.lib.nixosSystem {
specialArgs = { inherit self keys paths inputs; }; specialArgs = { inherit self keys paths inputs; };
modules = [ modules = [
{ {
nixpkgs.overlays = overlays; nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true; nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-linux"; nixpkgs.hostPlatform = "aarch64-linux";
nix.package = lix.packages."aarch64-linux".default; nix.package = lix.packages."aarch64-linux".default;
} }
inputs.home-manager.nixosModules.home-manager ./hosts/cyt-pi/configuration.nix
agenix.nixosModules.default ./hosts/cyt-pi/hardware-configuration.nix
./hosts/cyt-pi/configuration.nix ];
./hosts/cyt-pi/hardware-configuration.nix
./modules/nixos/services/remote-builder.nix
./modules/nixos/services/wireguard-client.nix
./users/gortium/gortium.nix
];
};
uConsole = 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;
}; };
agenix-rekey = agenix-rekey.configure {
userFlake = self;
nixosConfigurations = self.nixosConfigurations;
};
devShells.${system}.default = devShell;
packages.${system} = {
uConsole-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

@@ -21,7 +21,14 @@
settings.PermitRootLogin = "prohibit-password"; settings.PermitRootLogin = "prohibit-password";
}; };
# User (defined in users/gortium/gortium.nix) # User
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "kismet" ];
openssh.authorizedKeys.keys = [
# Populate with your public key
];
};
# CYT Project Dependencies (Headless) # CYT Project Dependencies (Headless)
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
@@ -43,7 +50,7 @@
User = "gortium"; User = "gortium";
Group = "kismet"; Group = "kismet";
ExecStart = '' ExecStart = ''
${pkgs.kismet}/bin/kismet -c panda --log-prefix=/home/gortium/kismet_logs --no-nc-ui ${pkgs.kismet}/bin/kismet -c panda --log-base=/home/gortium/kismet_logs --no-nc-ui
''; '';
Restart = "always"; Restart = "always";
RestartSec = "10s"; RestartSec = "10s";
@@ -70,12 +77,6 @@
}; };
}; };
# Age secrets
age.secrets.gortium_password = {
file = ../../secrets/gortium_password.age;
};
# CYT Application Service # CYT Application Service
systemd.services.cyt-app = { systemd.services.cyt-app = {
description = "Chasing Your Tail - Target Detector"; description = "Chasing Your Tail - Target Detector";

View File

@@ -11,10 +11,6 @@
# Flakesss # Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ]; nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ];
nix.settings.trusted-users = [ "root" "gortium" ]; nix.settings.trusted-users = [ "root" "gortium" ];
nix.settings.extra-platforms = [ "aarch64-linux" ];
# QEMU binfmt for cross-building aarch64 NixOS targets
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
# Garbage collection # Garbage collection
nix.gc = { nix.gc = {
@@ -40,7 +36,7 @@
"transparent_hugepage=always" # because mucho ram "transparent_hugepage=always" # because mucho ram
]; ];
# 2. Load the specific drivers found by sensors-detect # 2. Load the specific drivers found by sensors-detect
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" ]; boot.kernelModules = [ "nct6775" "lm96163" ];
# 3. Force the nct6775 driver to recognize the chip if it's stubborn # 3. Force the nct6775 driver to recognize the chip if it's stubborn
boot.extraModprobeConfig = '' boot.extraModprobeConfig = ''
options nct6775 force_id=0xd280 options nct6775 force_id=0xd280
@@ -53,14 +49,6 @@
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.hostId = "deadbeef"; 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. # Set your time zone.
time.timeZone = "America/Montreal"; time.timeZone = "America/Montreal";
@@ -170,6 +158,7 @@
settings = { settings = {
PasswordAuthentication = false; PasswordAuthentication = false;
KbdInteractiveAuthentication = false; KbdInteractiveAuthentication = false;
PermitRootLogin = "prohibit-password";
}; };
hostKeys = [ hostKeys = [
{ {
@@ -198,7 +187,6 @@
ai = { ai = {
path = self + "/assets/compose/ai"; path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path; envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
}; };
cloudstorage = { cloudstorage = {
@@ -233,11 +221,6 @@
path = self + "/assets/compose/homepage"; path = self + "/assets/compose/homepage";
}; };
vpn = {
path = self + "/assets/compose/vpn";
envFile = config.age.secrets.containers_env.path;
};
# tak = { # tak = {
# path = self + "/assets/compose/tak"; # path = self + "/assets/compose/tak";
# }; # };
@@ -251,9 +234,7 @@
# Private host ssh key managed by agenix # Private host ssh key managed by agenix
age = { age = {
identityPaths = paths.identities ++ [ identityPaths = paths.identities;
"/etc/ssh/ssh_host_ed25519_key"
];
secrets = { secrets = {
containers_env = { containers_env = {
file = ../../secrets/containers.env.age; file = ../../secrets/containers.env.age;
@@ -276,37 +257,26 @@
mode = "0600"; mode = "0600";
path = "/home/ai-worker/.ssh/ai_ssh_key"; path = "/home/ai-worker/.ssh/ai_ssh_key";
}; };
wireguard_private_key = { openclaw_gateway_token = {
file = ../../secrets/wireguard_private_key.age; file = ../../secrets/openclaw_gateway_token.age;
owner = "root"; owner = "root";
group = "root"; group = "ai-worker";
mode = "0400"; mode = "0440";
path = "/run/secrets/wireguard_private_key"; path = "/run/secrets/openclaw_gateway_token";
};
wireguard_preshared_key = {
file = ../../secrets/wireguard_preshared_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_preshared_key";
};
builder_key = {
file = ../../secrets/builder_key.age;
owner = "root";
group = "root";
mode = "0600";
path = "/etc/ssh/builder_key";
};
gortium_password = {
file = ../../secrets/gortium_password.age;
owner = "gortium";
group = "gortium";
mode = "0400";
path = "/run/secrets/gortium_password";
}; };
}; };
}; };
# OpenClaw Node service (host-side execution for Docker gateway)
services.openclaw-node = {
enable = true;
user = "ai-worker";
gatewayHost = "127.0.0.1";
gatewayPort = 18789;
gatewayTokenFile = "/run/secrets/openclaw_gateway_token";
displayName = "lazyworkhorse-host";
};
# Public host ssh key (kept in sync with the private one) # Public host ssh key (kept in sync with the private one)
environment.etc."ssh/ssh_host_ed25519_key.pub".text = environment.etc."ssh/ssh_host_ed25519_key.pub".text =
"${keys.hosts.lazyworkhorse.main}"; "${keys.hosts.lazyworkhorse.main}";
@@ -338,213 +308,6 @@
# Or disable the firewall altogether. # Or disable the firewall altogether.
# networking.firewall.enable = false; # 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
# =============================================================================
# Firewall - default deny, explicit allow
networking.firewall = {
# Enable firewall with default deny policy (NixOS firewall denies all by default)
enable = true;
allowPing = true;
# Only essential ports exposed to internet
allowedTCPPorts = [
2424 # SSH (non-standard port)
2222 # Gitea (version control)
80 # HTTP (Traefik redirect)
443 # HTTPS (Traefik)
# 8000 # Portainer - REVIEW: internal only?
# 4242 # Coms - REVIEW: internal only?
# 5000 # TAK API - REVIEW: internal only?
# 8087 # TAK Connect - REVIEW: internal only?
# 8089 # TAK Management - REVIEW: internal only?
];
allowedUDPPorts = [
51820 # WireGuard VPN
];
# Rate limiting and attack prevention
extraCommands = ''
# 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 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
iptables -A INPUT -p tcp --dport 443 -m state --state NEW -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
# Drop invalid packets
iptables -A INPUT -m state --state INVALID -j DROP
# 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
'';
};
# Fail2ban - automatic IP banning
services.fail2ban = {
enable = true;
maxretry = 3;
bantime = "1h";
banaction = "iptables-multiport";
jails = {
# SSH brute force protection (uses systemd journal backend)
sshd = {
enabled = true;
settings = {
filter = "sshd";
port = "2424";
maxretry = 3;
bantime = "1h";
};
};
# Recidive - ban repeat offenders for 1 week
recidive = {
enabled = true;
settings = {
filter = "recidive";
logpath = "/var/log/fail2ban.log";
bantime = "1w";
findtime = "1d";
maxretry = 3;
};
};
# HTTP authentication failures (Traefik)
http-auth = {
enabled = true;
settings = {
filter = "traefik-auth";
port = "80,443";
logpath = "/var/log/traefik/access.log";
maxretry = 5;
bantime = "1h";
};
};
# HTTP scanning/attacks (Traefik)
http-botsearch = {
enabled = true;
settings = {
filter = "traefik-botsearch";
port = "80,443";
logpath = "/var/log/traefik/access.log";
maxretry = 2;
bantime = "2h";
};
};
};
};
# Custom fail2ban filters for Traefik
environment.etc."fail2ban/filter.d/traefik-auth.conf".text = ''
[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD|PUT|DELETE).*" (401|403) \d+.*$
ignoreregex =
'';
environment.etc."fail2ban/filter.d/traefik-botsearch.conf".text = ''
[Definition]
failregex = ^<HOST> -.*"(GET|POST|HEAD|PUT|DELETE).*" 404 \d+.*$
^<HOST> -.*"(GET|POST|HEAD|PUT|DELETE).*/(\.|wp-|php|admin|login|xmlrpc|\.env|\.git|\.aws|\.azure).*" \d+.*$
ignoreregex =
'';
# SSH hardening
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;
PermitEmptyPasswords = "no";
ChallengeResponseAuthentication = "no";
UsePAM = true;
LogLevel = "VERBOSE";
X11Forwarding = false;
AllowTcpForwarding = "no";
AllowAgentForwarding = "no";
PermitTunnel = "no";
};
# Kernel network hardening
boot.kernel.sysctl = {
# IP Spoofing protection
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.conf.default.rp_filter" = 1;
# Ignore ICMP broadcasts
"net.ipv4.icmp_echo_ignore_broadcasts" = 1;
# Disable source routing
"net.ipv4.conf.all.accept_source_route" = 0;
"net.ipv4.conf.default.accept_source_route" = 0;
"net.ipv6.conf.all.accept_source_route" = 0;
"net.ipv6.conf.default.accept_source_route" = 0;
# Disable redirects
"net.ipv4.conf.all.send_redirects" = 0;
"net.ipv4.conf.default.send_redirects" = 0;
# SYN flood protection
"net.ipv4.tcp_syncookies" = 1;
"net.ipv4.tcp_max_syn_backlog" = 2048;
"net.ipv4.tcp_synack_retries" = 2;
"net.ipv4.tcp_syn_retries" = 5;
# Log martian packets
"net.ipv4.conf.all.log_martians" = 1;
"net.ipv4.conf.default.log_martians" = 1;
# Ignore redirects
"net.ipv4.conf.all.accept_redirects" = 0;
"net.ipv4.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.secure_redirects" = 0;
"net.ipv4.conf.default.secure_redirects" = 0;
"net.ipv6.conf.all.accept_redirects" = 0;
"net.ipv6.conf.default.accept_redirects" = 0;
# Connection tuning
"net.core.somaxconn" = 4096;
"net.core.netdev_max_backlog" = 65536;
"net.ipv4.tcp_max_orphans" = 65536;
"net.ipv4.tcp_fin_timeout" = 15;
"net.ipv4.tcp_keepalive_time" = 300;
"net.ipv4.tcp_keepalive_probes" = 5;
"net.ipv4.tcp_keepalive_intvl" = 15;
};
# Audit logging
security.auditd.enable = true;
# Fail2ban log directory
systemd.tmpfiles.rules = [
"d /var/log/fail2ban 0755 root root -"
"d /var/log/traefik 0755 root root -"
];
# Copy the NixOS configuration file and link it from the resulting system # Copy the NixOS configuration file and link it from the resulting system
# (/run/current-system/configuration.nix). This is useful in case you # (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix. # accidentally delete configuration.nix.
@@ -569,29 +332,5 @@
# For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion .
system.stateVersion = "25.05"; # Did you read the comment? system.stateVersion = "25.05"; # Did you read the comment?
# ============================================================
# Remote builder — dispatches aarch64-linux builds to uConsole
# ============================================================
services.remoteBuilder = {
enable = true;
machines = [
{
hostName = "192.168.1.120";
port = 22;
sshUser = "builder";
sshKey = config.age.secrets.builder_key.path;
systems = [ "aarch64-linux" ];
maxJobs = 4;
}
];
};
# agenix-rekey — automatic re-encryption for each target host
age.rekey = {
masterIdentities = [
"/home/gortium/.ssh/gortium_ssh_key"
];
};
} }

View File

@@ -1,240 +0,0 @@
{ 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 (native-only, needs remote build)
# fldigi # removed for bootstrap - now native (native-only, needs remote build)
pat # Winlink client
direwolf # AX.25 packet modem
# chirp # Radio programming tool - now native (native-only, needs remote build)
hamlib # Ham radio control libraries
# trustedqsl # Logbook of the World (LoTW) - now native (native-only, needs remote build)
# ===== SDR / RF =====
# sdrpp # removed for bootstrap - now native (native-only, needs remote build)
# gqrx # removed for bootstrap - now native (native-only, needs remote build)
rtl-sdr # RTL-SDR drivers & utilities
# inspectrum # removed for bootstrap - now native (native-only, needs remote build)
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 (native-only, needs remote build)
#viking # removed for bootstrap - now native (native-only, needs remote build)
#gpsbabel # GPS data conversion - now native (native-only, needs remote build)
];
# ============================================================
# 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 = [ ];
# ============================================================
# Remote builder — dispatches x86_64-linux builds to lazyworkhorse
# ============================================================
services.remoteBuilder = {
enable = true;
machines = [
{
hostName = "lazyworkhorse.net";
port = 2424;
sshUser = "builder";
sshKey = config.age.secrets.builder_key.path;
systems = [ "x86_64-linux" ];
maxJobs = 36;
supportedFeatures = [ "benchmark" "big-parallel" "nixos-test" ];
}
];
};
# Age secret for builder SSH key (remote building)
age.secrets.builder_key = {
file = ../../secrets/builder_key.age;
};
# ============================================================
age.identityPaths = [
"/etc/ssh/ssh_host_ed25519_key"
];
# 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 = "age1un8td4jzkhg5fm4jja7z3aznskc8hlcm8ky0j2f70tv74rulwgaqtpemn2"; # uConsole CM5 SSH host key
};
# Pipewire overlay: drop libcamera (fixes aarch64 cross-compile — rpi-pisp blocks)
nixpkgs.overlays = [
(final: prev: {
pipewire = prev.pipewire.override { libcamera = null; };
})
];
}

View File

@@ -1,30 +0,0 @@
{ 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

@@ -9,13 +9,6 @@
ai-worker = { ai-worker = {
main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAXeGtPPcsP2IYRQNvII41NVWhJsarEk8c4qxs/a5sXf"; main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAXeGtPPcsP2IYRQNvII41NVWhJsarEk8c4qxs/a5sXf";
}; };
builder = {
# Same key on both hosts for bidirectional remote building.
# Generate with: ssh-keygen -t ed25519 -f /etc/ssh/builder_key -N ""
# Replace the placeholder below with the public key (builder_key.pub).
main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIByG79NM/rk13q3MTow9Ezb8pe3RWoQ76Mv8atASp3Ri";
};
}; };
hosts = { hosts = {
@@ -25,9 +18,5 @@
gitea = ""; gitea = "";
bootstrap = "age1r796v2uldtspawyh863pks74sd2pwcan8j4e4pjzsvkmr3vjja9qpz5ste"; bootstrap = "age1r796v2uldtspawyh863pks74sd2pwcan8j4e4pjzsvkmr3vjja9qpz5ste";
}; };
uConsole = {
main = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICasvsmaHuNVaWnjwuBfoMXyY/6PIqGL8yxstPBb46u4";
bootstrap = "age1un8td4jzkhg5fm4jja7z3aznskc8hlcm8ky0j2f70tv74rulwgaqtpemn2";
};
}; };
} }

View File

@@ -1,121 +0,0 @@
{ 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 = ".";
};
};
};
};
};
}

View File

@@ -1,170 +0,0 @@
{ 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";
};
};
};
}

View File

@@ -1,105 +0,0 @@
# 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

@@ -1,17 +0,0 @@
{ 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

@@ -1,240 +0,0 @@
{ 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;
};
};
};
}

View File

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

View File

@@ -1,87 +1,45 @@
{ pkgs, ... }: { { pkgs, ... }: {
systemd.services.init-ollama-model = { systemd.services.init-ollama-model = {
description = "Initialize LLM models with extra context in Ollama Docker"; 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" ]; wantedBy = [ "multi-user.target" ];
script = '' script = ''
# Fonction de création asynchrone pour ne pas bloquer le démarrage # Wait for Ollama
( while ! ${pkgs.curl}/bin/curl -s http://localhost:11434/api/tags > /dev/null; do
echo "Starting asynchronous Ollama initialization..." sleep 2
done
# Attente d'Ollama (maximum 120 secondes pour éviter une boucle infinie) create_model_if_missing() {
TIMEOUT=60 local model_name=$1
COUNT=0 local base_model=$2
while ! ${pkgs.curl}/bin/curl -s -f http://127.0.0.1:11434/api/tags > /dev/null; do if ! ${pkgs.docker}/bin/docker exec ollama ollama list | grep -q "$model_name"; then
if [ $COUNT -ge $TIMEOUT ]; then echo "$model_name not found, creating from $base_model..."
echo "Ollama did not become ready in time. Exiting." ${pkgs.docker}/bin/docker exec ollama sh -c "cat <<EOF > /root/.ollama/$model_name.modelfile
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
# 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 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_ctx 131072
PARAMETER num_predict 4096 PARAMETER num_predict 4096
PARAMETER num_keep 1024 PARAMETER num_keep 1024
PARAMETER repeat_penalty 1.1 PARAMETER repeat_penalty 1.1
PARAMETER top_k 40 PARAMETER top_k 40
PARAMETER stop "[INST]" PARAMETER stop \"[INST]\"
PARAMETER stop "[/INST]" PARAMETER stop \"[/INST]\"
PARAMETER stop "</s>" PARAMETER stop \"</s>\"
EOF 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
}
# Copie et création dans le conteneur # Create Nemotron
${pkgs.docker}/bin/docker cp "$TMP_FILE" ollama:/tmp/model.modelfile create_model_if_missing "nemotron-3-nano:30b-128k" "nemotron-3-nano:30b"
${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 Devstral
create_model_if_missing "nemotron-3-nano:30b-128k" "nemotron-3-nano:30b" create_model_if_missing "devstral-small-2:24b-128k" "devstral-small-2:24b"
# Create Devstral
create_model_if_missing "devstral-small-2:24b-128k" "devstral-small-2:24b"
) &
''; '';
serviceConfig = { serviceConfig = {
Type = "forking"; # Permet à systemd de savoir que le script passe en arrière-plan via '&' Type = "oneshot";
User = "root"; RemainAfterExit = true;
}; };
}; };
} }

View File

@@ -0,0 +1,64 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.openclaw-node;
openclawPkg = pkgs.openclaw;
in {
options.services.openclaw-node = {
enable = lib.mkEnableOption "OpenClaw Node service";
user = lib.mkOption {
type = lib.types.str;
default = "ai-worker";
description = "User to run the OpenClaw headless node as.";
};
gatewayHost = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Gateway host (IP or hostname).";
};
gatewayPort = lib.mkOption {
type = lib.types.int;
default = 18789;
description = "Gateway WebSocket port.";
};
gatewayTokenFile = lib.mkOption {
type = lib.types.str;
default = "";
description = "Path to file containing the gateway auth token.";
};
displayName = lib.mkOption {
type = lib.types.str;
default = "lazyworkhorse-host";
description = "Display name for this node (shown in pairing).";
};
};
config = lib.mkIf cfg.enable {
systemd.services.openclaw-node = {
description = "OpenClaw Headless Node Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "exec";
User = cfg.user;
Group = cfg.user;
WorkingDirectory = "/home/${cfg.user}";
ExecStart = ''
${pkgs.bash}/bin/bash -c 'export OPENCLAW_GATEWAY_TOKEN=$(cat ${cfg.gatewayTokenFile}) && exec ${openclawPkg}/bin/openclaw node run --host ${cfg.gatewayHost} --port ${toString cfg.gatewayPort} --display-name "${cfg.displayName}"'
'';
Restart = "always";
RestartSec = 5;
};
environment = {
NODE_ENV = "production";
};
};
};
}

View File

@@ -1,73 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.remoteBuilder;
in {
options.services.remoteBuilder = {
enable = lib.mkEnableOption "remote Nix build machine";
machines = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
hostName = lib.mkOption {
type = lib.types.str;
description = "Hostname or IP of the remote build machine.";
};
port = lib.mkOption {
type = lib.types.port;
default = 22;
description = "SSH port.";
};
sshUser = lib.mkOption {
type = lib.types.str;
default = "builder";
description = "SSH user on the remote build machine.";
};
sshKey = lib.mkOption {
type = lib.types.str;
description = "Path to SSH private key for the builder.";
};
systems = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "aarch64-linux" ];
description = "System types the remote builder can build for.";
};
maxJobs = lib.mkOption {
type = lib.types.int;
default = 4;
description = "Max parallel jobs on the remote builder.";
};
supportedFeatures = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "benchmark" "big-parallel" "nixos-test" ];
description = "Features the remote builder supports.";
};
};
});
default = [];
description = "List of remote Nix build machines.";
};
};
config = lib.mkIf cfg.enable {
nix.distributedBuilds = true;
nix.buildMachines = map (m: {
hostName = m.hostName;
sshUser = m.sshUser;
sshKey = m.sshKey;
systems = m.systems;
maxJobs = m.maxJobs;
supportedFeatures = m.supportedFeatures;
}) cfg.machines;
# SSH config for port + key (nix.buildMachines has no port option)
programs.ssh.extraConfig = lib.concatStringsSep "\n" (map (m: ''
Host ${m.hostName}
HostName ${m.hostName}
Port ${toString m.port}
User ${m.sshUser}
IdentityFile ${m.sshKey}
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
'') cfg.machines);
};
}

View File

@@ -1,275 +0,0 @@
{ 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

@@ -1,54 +0,0 @@
{ 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

@@ -1,81 +0,0 @@
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;
};
};
}

View File

@@ -1,19 +0,0 @@
--- 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)) {

View File

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

63
scripts/deploy-ssh-config Normal file
View File

@@ -0,0 +1,63 @@
# Hermes Container SSH Configuration
# For NixOS deployment to remote hosts
#
# Usage:
# cp scripts/deploy-ssh-config ~/.ssh/config.d/hermes-include
# Or: cat scripts/deploy-ssh-config >> ~/.ssh/config
#
# This config covers all NixOS hosts managed from the Hermes container.
# Lazyworkhorse has two users: ai-worker (primary automation) and gortium (admin).
# Cyt-pi connects via reverse SSH tunnel on port 19999.
# uConsole is a placeholder until LAN-hostname resolution is confirmed.
# ── Global defaults ──────────────────────────────────────────────────
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
Compression yes
CompressionLevel 6
ControlMaster auto
ControlPath ~/.ssh/controlmasters/%r@%h:%p
ControlPersist 10m
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
# ── Hosts ──────────────────────────────────────────────────────────────
# Lazyworkhorse — x86_64 main server (ai-worker@lazyworkhorse.net:2424)
Host lazyworkhorse
HostName lazyworkhorse.net
User ai-worker
Port 2424
IdentityFile /opt/data/home/.ssh/id_hermes_gitea
# Lazyworkhorse — admin access (gortium@lazyworkhorse.net:2425)
Host lazyworkhorse-admin
HostName lazyworkhorse.net
User gortium
Port 2425
IdentityFile /opt/data/home/.ssh/id_hermes_gitea
# Cyt-pi — aarch64 Pi Zero 2 W
# Connected via reverse SSH tunnel (gortium directs tunnel to :19999)
Host cyt-pi
HostName localhost
User gortium
Port 19999
IdentityFile /opt/data/home/.ssh/id_hermes_gitea
# uConsole — aarch64 ClockworkPi (placeholder hostname)
# Replace uconsole.lan with actual IP/hostname when deployed
Host uConsole uconsole
HostName uconsole.lan
User gortium
Port 22
IdentityFile /opt/data/home/.ssh/id_hermes_gitea
# ── Gitea host — for git operations ──────────────────────────────────
Host code
HostName code.lazyworkhorse.net
Port 2222
User gortium
IdentityFile /opt/data/home/.ssh/id_hermes_gitea

286
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,286 @@
#!/usr/bin/env bash
# NixOS Deployment Helper Script
# Remote NixOS deployment from Hermes container to target hosts.
#
# Usage: ./deploy.sh <hostname> [branch] [action]
#
# Actions:
# switch Activate configuration now (default)
# boot Activate on next reboot
# test Activate without switching generations
# build Build locally only, no remote activation
# dry-activate Show what would change without applying
#
# Examples:
# ./deploy.sh lazyworkhorse # deploy master/switch to lazyworkhorse
# ./deploy.sh cyt-pi feat/test boot # deploy feat/test branch, activate on boot
# ./deploy.sh uConsole master build # just build, don't deploy
# NO_BUILD_CHECK=1 ./deploy.sh uConsole # skip the pre-flight nix build
#
# Environment variables:
# SSH_USER SSH user (default: auto-detected per host)
# SSH_PORT SSH port (default: auto-detected per host)
# SSH_KEY SSH identity file
# BUILD_HOST Build flake for this host (default: same as target host)
# NO_BUILD_CHECK Set to 1 to skip local nix build before deployment
set -euo pipefail
# ── Colors ──────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; }
# ── Cleanup trap ───────────────────────────────────────────────────────
cleanup() {
local ec=$?
if [ $ec -ne 0 ]; then
error "Deployment failed with exit code $ec"
fi
exit $ec
}
trap cleanup EXIT
# ── Usage / Help ───────────────────────────────────────────────────────
show_usage() {
cat <<EOF
Usage: $0 <hostname> [branch] [action]
Remote NixOS deployment from Hermes container to target hosts.
HOSTNAME (required):
lazyworkhorse x86_64 main server
cyt-pi aarch64 Pi Zero 2 W (via reverse tunnel)
uConsole aarch64 ClockworkPi
BRANCH (optional, default: master):
Git branch or tag to deploy. Fetched from origin.
ACTION (optional, default: switch):
switch Activate configuration now (default)
boot Activate on next reboot
test Activate without switching generations
build Build locally only, skip remote deployment
dry-activate Show what would change without applying
Environment variables:
SSH_USER SSH username override
SSH_PORT SSH port override
SSH_KEY SSH identity file path
BUILD_HOST Build flake hostname (default: same as HOSTNAME)
NO_BUILD_CHECK Skip local nix build validation (set to 1)
Examples:
$0 lazyworkhorse # deploy master/switch
$0 cyt-pi feat/test boot # deploy feature branch, boot
$0 uConsole master build # just build, no remote
NO_BUILD_CHECK=1 $0 uConsole # skip build check
EOF
exit 0
}
# ── Argument parsing ───────────────────────────────────────────────────
HOSTNAME="${1:-}"
BRANCH="${2:-master}"
ACTION="${3:-switch}"
NO_BUILD_CHECK="${NO_BUILD_CHECK:-0}"
if [ "$HOSTNAME" = "--help" ] || [ "$HOSTNAME" = "-h" ] || [ -z "$HOSTNAME" ]; then
show_usage
fi
# ── Host configuration ─────────────────────────────────────────────────
case "$HOSTNAME" in
lazyworkhorse)
DEFAULT_SSH_USER="ai-worker"
DEFAULT_SSH_PORT="2424"
ARCH="x86_64-linux"
;;
cyt-pi)
DEFAULT_SSH_USER="gortium"
DEFAULT_SSH_PORT="19999"
ARCH="aarch64-linux"
;;
uConsole)
DEFAULT_SSH_USER="gortium"
DEFAULT_SSH_PORT="22"
ARCH="aarch64-linux"
;;
*)
error "Unknown host: $HOSTNAME"
echo "Supported hosts: lazyworkhorse, cyt-pi, uConsole"
exit 1
;;
esac
SSH_USER="${SSH_USER:-$DEFAULT_SSH_USER}"
SSH_PORT="${SSH_PORT:-$DEFAULT_SSH_PORT}"
SSH_KEY="${SSH_KEY:-/opt/data/home/.ssh/id_hermes_gitea}"
BUILD_HOST="${BUILD_HOST:-$HOSTNAME}"
SSH_OPTS="-p $SSH_PORT -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
SSH_TARGET="${SSH_USER}@${HOSTNAME}"
export GIT_SSH_COMMAND="ssh -i $SSH_KEY -p 2222 -o StrictHostKeyChecking=no"
export PATH="/nix/var/nix/profiles/default/bin:$PATH"
# ── Banner ─────────────────────────────────────────────────────────────
echo "╔══════════════════════════════════════════════╗"
echo "║ NixOS Remote Deployment ║"
echo "╚══════════════════════════════════════════════╝"
info "Host: $HOSTNAME ($ARCH)"
info "Branch: $BRANCH"
info "Action: $ACTION"
info "SSH: ${SSH_USER}@${HOSTNAME}:${SSH_PORT}"
echo ""
# ── Pre-flight checks ─────────────────────────────────────────────────
step "Pre-flight checks"
# 1. Check required tools
for cmd in nix git ssh; do
if ! command -v "$cmd" &>/dev/null; then
error "Required tool not found: $cmd"
exit 1
fi
done
ok "Required tools available (nix, git, ssh)"
# 2. Check infra repo
INFRA_DIR="$(cd "$(dirname "$0")/.." && pwd)"
if [ ! -d "$INFRA_DIR/.git" ]; then
error "Not a git repository: $INFRA_DIR"
exit 1
fi
ok "Infra repo found at $INFRA_DIR"
# 3. Check SSH connectivity (skip for build-only actions)
if [ "$ACTION" != "build" ]; then
if ssh $SSH_OPTS -o ConnectTimeout=5 "$SSH_TARGET" "echo connected" &>/dev/null; then
ok "SSH connectivity to $HOSTNAME verified"
else
warn "Cannot reach $HOSTNAME via SSH — deployment step will fail later"
fi
fi
# ── Git sync ───────────────────────────────────────────────────────────
step "Git sync"
cd "$INFRA_DIR"
# Stash local changes if any
if ! git diff --quiet HEAD; then
warn "Local changes detected, stashing..."
git stash push -m "auto-stash before deploy $(date -Iseconds)"
STASHED=1
else
STASHED=0
fi
# Fetch and checkout
git fetch origin "$BRANCH" 2>/dev/null || git fetch origin master
if git rev-parse --verify "origin/$BRANCH" &>/dev/null 2>&1; then
# Remote branch exists — fast-forward merge
git checkout -B "$BRANCH" "origin/$BRANCH"
elif git rev-parse --verify "$BRANCH" &>/dev/null 2>&1; then
# Local branch or tag
git checkout "$BRANCH"
else
error "Branch/tag not found: $BRANCH"
exit 1
fi
ok "Checked out $BRANCH ($(git rev-parse --short HEAD))"
# Update submodules
if [ -f .gitmodules ]; then
git submodule update --init --recursive
ok "Submodules updated"
fi
# ── Build validation ──────────────────────────────────────────────────
if [ "$NO_BUILD_CHECK" != "1" ]; then
step "Build validation"
info "Building nixosConfigurations.$BUILD_HOST (no link)..."
if nix build --no-link --print-build-logs \
".#nixosConfigurations.${BUILD_HOST}.config.system.build.toplevel" 2>&1; then
ok "Build succeeded for $BUILD_HOST"
else
error "Build failed for $BUILD_HOST"
exit 1
fi
else
warn "Build check skipped (NO_BUILD_CHECK=1)"
fi
# ── Deployment ─────────────────────────────────────────────────────────
if [ "$ACTION" = "build" ]; then
step "Build complete (no deployment)"
info "Use one of: switch, boot, test, dry-activate to deploy"
exit 0
fi
step "Deployment ($ACTION)"
# Build the nixos-rebuild command
case "$ACTION" in
switch|boot|test)
nixos-rebuild "$ACTION" \
--flake ".#$HOSTNAME" \
--target-host "$SSH_TARGET" \
--build-host "localhost" \
--use-remote-sudo \
--max-jobs 4
;;
dry-activate)
nixos-rebuild dry-activate \
--flake ".#$HOSTNAME" \
--target-host "$SSH_TARGET" \
--build-host "localhost" \
--use-remote-sudo
;;
*)
error "Unknown action: $ACTION"
echo "Valid actions: switch, boot, test, build, dry-activate"
exit 1
;;
esac
# ── Check result ───────────────────────────────────────────────────────
DEPLOY_EXIT=$?
if [ $DEPLOY_EXIT -eq 0 ]; then
echo ""
ok "Deployment to $HOSTNAME ($ACTION) completed successfully"
case "$ACTION" in
switch|test)
info "Configuration is now active"
;;
boot)
info "Configuration will activate on next reboot"
;;
dry-activate)
info "Dry-run complete — no changes applied"
;;
esac
else
error "Deployment failed with exit code $DEPLOY_EXIT"
exit $DEPLOY_EXIT
fi
echo ""
echo "╔══════════════════════════════════════════════╗"
echo "║ Deployment Complete ║"
echo "╚══════════════════════════════════════════════╝"
info "Host: $HOSTNAME"
info "Branch: $BRANCH ($(git rev-parse --short HEAD))"
info "Action: $ACTION"
info "Time: $(date -Iseconds)"

Binary file not shown.

View File

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

Binary file not shown.

View File

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

View File

@@ -0,0 +1,11 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBCWEpO
cG9yNnFpcHFqTkNzTngxU1MxN0NYK0hrZFhUTjVORWFrK3JNd2tZCmtMTGpwQk1E
WlUwL3N6SGRWblpnNEkrWkkyU2hQMkRIK0M3R0pOVEREV3MKLT4gY2osLWdyZWFz
ZSBacSozVVQgUCAxRS1OQSAuKXxDPCoKbStWNW1BZjBZQzNDaTlDbU5EZkxsRWxM
cXJ3dDU1RDNpOXRlV0tzdEp2NUo3S1lhRG5Md0RHTGlJdkFSYmt5YQo4R1hiQWRG
V2VxekJKZwotLS0geG1XSi9VbkhXZHQzcEFVS3hKNzVueXFLa2xnZTc3Q2tJTVZ5
eXJabWk5Ywp6bJCP3s0xxzjE+eTR+cv7ZUnkoliT/n7uIprq1BTn/LIRLkUTUqs3
NiDwrXcoq4/QKd0Dt+8ap3vFAuusjGxRlnYMaRrZie2AGtTV8U7Q7durm9o2K+/4
QzRQ/MtumIQm
-----END AGE ENCRYPTED FILE-----

View File

@@ -8,11 +8,7 @@ let
in in
{ {
"containers.env.age".publicKeys = authorizedKeys; "containers.env.age".publicKeys = authorizedKeys;
"gortium_password.age".publicKeys = authorizedKeys;
"home_wifi.age".publicKeys = authorizedKeys;
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys; "lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys; "n8n_ssh_key.age".publicKeys = authorizedKeys;
"builder_key.age".publicKeys = authorizedKeys ++ [ "openclaw_gateway_token.age".publicKeys = authorizedKeys;
keys.hosts.uConsole.bootstrap
];
} }

View File

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

View File

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

14
users/ai-worker.nix Normal file
View File

@@ -0,0 +1,14 @@
{ pkgs, inputs, config, keys, ... }: {
users.users.ai-worker = {
isSystemUser = true;
group = "ai-worker";
home = "/home/ai-worker";
createHome = true;
extraGroups = [ "docker" ];
shell = pkgs.bashInteractive;
openssh.authorizedKeys.keys = [
keys.users.ai-worker.main
];
};
users.groups.ai-worker = {};
}

View File

@@ -1,92 +0,0 @@
{ pkgs, inputs, config, keys, ... }: {
users.users.ai-worker = {
isSystemUser = true;
group = "ai-worker";
home = "/home/ai-worker";
createHome = true;
extraGroups = [ "docker" ];
shell = pkgs.bashInteractive;
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 = [
{
users = [ "ai-worker" ];
commands = [
# Firewall checks
{
command = "/run/wrappers/bin/sudo iptables -L -n -v";
options = [ "NOPASSWD" ];
}
{
command = "/run/wrappers/bin/sudo iptables -S";
options = [ "NOPASSWD" ];
}
# Fail2ban status
{
command = "/run/current-system/sw/bin/fail2ban-client status";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/fail2ban-client status *";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/fail2ban-client get * banned";
options = [ "NOPASSWD" ];
}
# Log inspection
{
command = "/run/current-system/sw/bin/journalctl -t kernel -n 100";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/journalctl -u fail2ban -n 50";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/journalctl -u firewall -n 50";
options = [ "NOPASSWD" ];
}
# SSH config verification
{
command = "/run/current-system/sw/bin/sshd -T";
options = [ "NOPASSWD" ];
}
# Docker service checks
{
command = "/run/current-system/sw/bin/docker ps";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/docker inspect *";
options = [ "NOPASSWD" ];
}
# Network diagnostics
{
command = "/run/current-system/sw/bin/ss -tlnp";
options = [ "NOPASSWD" ];
}
{
command = "/run/current-system/sw/bin/cat /proc/net/tcp";
options = [ "NOPASSWD" ];
}
];
}
];
}

View File

@@ -1,13 +0,0 @@
{ config, lib, pkgs, keys, ... }: {
users.users.builder = {
isSystemUser = true;
group = "builder";
home = "/var/empty";
createHome = false;
shell = "${pkgs.shadow}/bin/nologin";
openssh.authorizedKeys.keys = with keys; [
users.builder.main
];
};
users.groups.builder = {};
}

View File

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

View File

@@ -1,91 +0,0 @@
{ 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";
};
}