Compare commits

..

63 Commits

Author SHA1 Message Date
Robert
ef7f06166b Removed double file
Some checks failed
Build NixOS config / build (pull_request) Failing after 1s
2026-05-13 10:04:57 -04:00
Robert
e70516e78b Added access to the secret to version control 2026-05-13 10:03:14 -04:00
Robert
ce3a327ff7 The key changed apparently? it works with that one anyway 2026-05-13 10:01:58 -04:00
Robert
af38655170 Adding the runner key
Some checks failed
Build NixOS config / build (pull_request) Failing after 55s
2026-05-10 22:23:29 -04:00
dcf5ac6b5e fix: use nixos-builder label for NixOS CI 2026-05-10 22:15:53 -04:00
d78de94f94 feat: add NixOS build CI for infra PRs 2026-05-10 22:15:02 -04:00
28ab52209c Merge pull request 'Add restricted AI worker access with deployment capabilities' (#1) from ai-worker-restricted-access into master
Reviewed-on: #1
2026-05-11 00:48:29 +00:00
Robert
e6f7f0c263 Merge branch 'ai-worker-restricted-access' of ssh://code.lazyworkhorse.net:2222/gortium/infra into ai-worker-restricted-access 2026-05-10 18:06:46 -04:00
Robert
5c136e0765 Merge remote-tracking branch 'origin/master' into ai-worker-restricted-access 2026-05-10 17:06:58 -04:00
Robert
f722af7803 New ollama model creator module version 2026-05-10 16:56:09 -04:00
Robert
c07debf088 Added wireguard keys 2026-05-10 16:51:32 -04:00
6806898f04 feat: update compose submodule for ollama-gfx906 (v0.23.2) + add ollama Dockerfile 2026-05-10 10:12:34 -04:00
96e77c5ef2 Revert "feat: add ai-optimizer benchmark plan and state tracking for ollama GPU benchmarking"
This reverts commit ff7303cf6a.
2026-05-09 20:19:26 +00:00
ff7303cf6a feat: add ai-optimizer benchmark plan and state tracking for ollama GPU benchmarking 2026-05-09 20:13:08 +00:00
9e42f5d2cc Merge pull request 'feat(hermes): update compose submodule for Piper TTS' (#34) from feat/voice-support-v2 into master
Reviewed-on: #34
2026-05-09 19:40:05 +00:00
614883f3c3 fix: update compose submodule - startup permission fix for data volume 2026-05-09 16:04:39 +00:00
374d022593 fix: update compose submodule - permission fix for atomic writes 2026-05-09 15:50:42 +00:00
9679846cdb feat: update compose submodule - Ryan high voice 2026-05-09 15:21:59 +00:00
4056f91ec6 fix: update compose submodule - remove patch step 2026-05-09 14:28:44 +00:00
1ba7d31d2f fix: update compose submodule - patch path fix 2026-05-09 14:27:16 +00:00
c7e9f8a1e0 feat: update compose submodule for Norman voice 2026-05-09 14:20:55 +00:00
bbe1a4a850 fix: update compose submodule - ca-certificates 2026-05-09 14:15:01 +00:00
2b8316060c fix: update compose submodule - COPY path fix 2026-05-09 14:12:15 +00:00
cc2c62faf7 fix: update compose submodule - Dockerfile heredoc fix 2026-05-09 14:09:59 +00:00
47f1ba6cf2 fix: update compose submodule - clean patch script 2026-05-09 13:59:17 +00:00
db89881d75 fix: update compose submodule for full OPENROUTER_API_KEY 2026-05-09 13:55:47 +00:00
0bb0a270e6 fix: update compose submodule for clean Piper Dockerfile 2026-05-09 13:42:02 +00:00
41256ccbde fix: update compose submodule for Piper TTS (replaces Coqui/ROCm) 2026-05-09 13:24:17 +00:00
e551f0e5c5 feat: update compose submodule for ROCm + Coqui TTS Dockerfile 2026-05-09 04:10:05 +00:00
b11d599f37 fix: update compose submodule for simplified Dockerfile 2026-05-09 02:38:41 +00:00
782f2fa9ed feat(hermes): update compose submodule for ROCm GPU voice STT support 2026-05-09 00:22:08 +00:00
2e14069584 Merge pull request 'feat: add WireGuard VPN stack' (#33) from feat/wireguard-vpn into master
Reviewed-on: #33
2026-05-09 00:13:36 +00:00
c53460c400 fix: remove dns option from wireguard config (not a valid nixos option) 2026-05-05 03:26:44 +00:00
Robert
ee96593e3d Merge branch 'feat/wireguard-vpn' of ssh://code.lazyworkhorse.net:2222/gortium/infra into feat/wireguard-vpn 2026-05-04 23:22:35 -04:00
Robert
030125ab01 Added wireguard pass 2026-05-04 23:21:36 -04:00
Robert
5935747902 Security fixes 2026-05-04 23:20:57 -04:00
Robert
9ae0f6ad62 Submodule update 2026-05-04 23:20:03 -04:00
5c481d664a fix: split tunnel on host VPN - only route 10.8.0.0/24 2026-05-05 02:41:29 +00:00
94a7c7195a fix: remove exposed keys from comments 2026-05-05 02:12:55 +00:00
cf279c4fb0 feat: add host-level WireGuard client via networking.wireguard
- Add wg0 interface config with agenix-managed secrets
- Revert compose submodule to remove NET_ADMIN from Hermes
- WireGuard runs at host level, all containers inherit the tunnel
2026-05-05 02:11:41 +00:00
b9289a149d chore: update compose submodule for Hermes NET_ADMIN + WireGuard Dockerfile 2026-05-05 01:48:24 +00:00
e0068260cb chore: move Hermes Dockerfile to compose repo, add WireGuard tools
- Move Dockerfile.full from infra/docker/hermes to compose/ai/Dockerfile
- Add wireguard-tools and openresolv to Hermes image
- Remove stray docker/hermes directory from infra
2026-05-05 01:43:42 +00:00
a42b2ff65d chore: update compose submodule to wireguard-vpn (fix ref) 2026-05-05 01:21:34 +00:00
92bcf1cc04 chore: update compose submodule to wireguard-vpn 2026-05-05 01:21:19 +00:00
7d0b72a513 chore: update compose submodule to linuxserver/wireguard 2026-05-05 01:18:13 +00:00
48245518a1 fix: load iptables kernel modules for WireGuard NAT
wg-easy needs iptable_nat and iptable_filter to set up
masquerading for VPN traffic. These modules must be loaded
at boot for the container to access iptables.
2026-05-05 01:17:14 +00:00
1673a56439 feat: add WireGuard VPN stack
- Add vpn stack to services.dockerStacks
- Open UDP port 51820 for WireGuard protocol
- Update compose submodule to include vpn stack
2026-05-04 22:49:06 +00:00
Robert
7d3d072961 Merge branch 'master' into ai-worker-restricted-access 2026-05-03 05:28:39 -04:00
4cceab05d0 Merge pull request 'security: harden lazyworkhorse with firewall, fail2ban, SSH hardening' (#28) from feature/server-hardening-clean into master
Reviewed-on: #28
2026-05-03 09:11:56 +00:00
bcebf18676 fix: move filter into jail settings (NixOS submodule doesn't pass string filters) 2026-05-01 11:59:33 +00:00
0370d784a0 fix: http-botsearch logpath must be string, not list 2026-05-01 04:02:06 +00:00
260b2d2756 fix: restructure fail2ban jails per NixOS module - recidive in jails, settings attr, str bantime 2026-05-01 03:59:32 +00:00
2477acdfc7 fix: services.fail2ban top-level options - no findtime, maxretry lowercase 2026-05-01 03:57:21 +00:00
81c25d3f20 fix: use security.auditd instead of services.auditd 2026-05-01 03:55:09 +00:00
9b1f467db9 fix: remove invalid networking.firewall.defaultAllow option 2026-05-01 03:52:57 +00:00
65fa778b2b fix: add custom traefik fail2ban filters for http-auth and http-botsearch jails 2026-05-01 03:40:59 +00:00
5d3bbe99f3 chore: update compose submodule for traefik access logs 2026-05-01 03:33:34 +00:00
Robert
bcf5cadaa0 olllama template fix to remove currenttime 2026-04-30 21:54:47 -04:00
3e04ccc1e8 security: remove deployment commands from ai-worker sudo rules
ai-worker only needs security audit commands, not deployment access.

Removed:
- nh os switch
- nixos-rebuild switch

Kept:
- Firewall checks (iptables)
- Fail2ban status
- Log inspection (journalctl)
- SSH config (sshd -T)
- Docker service checks
- Network diagnostics
2026-04-30 17:46:39 +00:00
21bd4bb283 security: add restricted sudo for ai-worker with security audit commands
- Deployment: nh os switch, nixos-rebuild switch (flake path locked)
- Firewall checks: iptables -L, iptables -S
- Fail2ban: status, banned IPs
- Logs: journalctl for kernel and fail2ban
- SSH config: sshd -T for verification
- Docker: ps, inspect (service health)
- Network: ss -tlnp, /proc/net/tcp

All commands are whitelisted with NOPASSWD.
No shell access, no ALL command - principle of least privilege.
2026-04-30 17:46:39 +00:00
7994aad8d8 security: harden lazyworkhorse with firewall, fail2ban, SSH hardening
- Firewall (default deny):
  - Allow only essential ports: SSH(2424), Gitea(2222), HTTP(80), HTTPS(443)
  - Rate limit SSH (max 4 new connections/60s)
  - Rate limit HTTP/HTTPS (25/minute)
  - Drop invalid packets, log dropped packets

- Fail2ban (auto-ban attackers):
  - SSH jail: 3 strikes = 1 hour ban
  - HTTP auth failures: 5 strikes = 1 hour ban
  - HTTP scanning: 2 strikes = 2 hour ban
  - Recidive jail: repeat offenders = 1 week ban

- SSH hardening:
  - No root login
  - Max 3 auth tries, 5 sessions
  - 30s login grace time
  - No X11/TCP/agent forwarding
  - Verbose logging

- Kernel network hardening:
  - SYN flood protection (syncookies)
  - IP spoofing protection (rp_filter)
  - Disable source routing, redirects
  - Log martian packets
  - Connection tuning for high load

- Audit logging enabled

Ports commented for review (likely internal-only):
- 8000 (Portainer), 4242 (Coms), 5000/8087/8089 (TAK)
2026-04-30 17:46:39 +00:00
f0e21d95e4 fix: ai-worker docker-only access for ollama benchmarking
Remove infra repo bind mount and sudo access from ai-worker user.
Now ai-worker can only:
- SSH into host from Hermes container
- Run docker commands via docker group membership
- Execute ollama benchmarks via docker exec

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

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

- Updated flake.nix:
  - Include new security module

SECURITY: AI must ask for user confirmation before running nh os switch
2026-04-28 15:34:38 +00:00
18 changed files with 599 additions and 736 deletions

View File

@@ -0,0 +1,33 @@
name: Build NixOS config
on:
pull_request:
branches: [ master ]
paths:
- '**.nix'
- 'flake.lock'
- 'secrets/**'
- 'hosts/**'
- 'modules/**'
push:
branches: [ master ]
paths:
- '**.nix'
- 'flake.lock'
- 'secrets/**'
- 'hosts/**'
- 'modules/**'
jobs:
build:
runs-on: nixos-builder
steps:
- name: Checkout
run: |
git clone -b "${{ github.head_ref || github.ref_name }}" \
https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/infra.git .
git log --oneline -3
- name: Build NixOS config (lazyworkhorse)
run: |
nix --version
nh os build .#lazyworkhorse 2>&1

View File

@@ -5,6 +5,7 @@ This document outlines the development conventions for this NixOS-based infrastr
## Build & Deployment
- **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`.
## Linting & Formatting

View File

@@ -1,203 +0,0 @@
# AI Model Optimization Cron Job - EXECUTION PROMPT
**When this cron runs, follow these instructions exactly:**
---
## Your Role
You are an AI model optimization agent. Your task is to find the best ollama/llama.cpp configuration for maximum context size and hardware utilization.
**Hardware:**
- 2× AMD MI50 GPUs (32GB VRAM each, 64GB total)
- 128GB system RAM
- ROCm: HSA_OVERRIDE_GFX_VERSION=9.0.6, HIP_VISIBLE_DEVICES=0,1
---
## File Locations
```
STATE: /opt/data/infra/assets/ai-optimizer/state.json
RESULTS: /opt/data/infra/assets/ai-optimizer/results.csv
INFRA_REPO: /opt/data/infra
```
---
## Model Queues
### GPU Track (Coding - prioritize speed + context on GPU)
1. `devstral-small-2:24b`
2. `qwen2.5-coder:32b`
3. `codellama:34b-instruct`
### RAM Track (Knowledge - prioritize max context)
1. `qwen2.5:72b`
2. `nemotron-3-nano:30b`
3. `mixtral:8x7b-instruct`
---
## Context Steps (in order)
```
[32768, 65536, 98304, 131072, 163840, 200704, 262144, 327680]
```
---
## Each Run - Step by Step
### 1. Read State
```bash
cd /opt/data/infra
cat assets/ai-optimizer/state.json
```
### 2. Determine Next Test
- Read `track` (gpu or ram)
- Read `current_model` from queue at `model_index`
- Read `current_config` for parameters to test
- Select next context step from `context_steps` based on `phase`
### 3. Pull Model (if needed)
```bash
docker exec ollama ollama list | grep -q "<model>" || docker exec ollama ollama pull <model>
```
### 4. Create Test Modelfile
```bash
docker exec ollama bash -c "cat <<EOF > /root/.ollama/test_${model}.modelfile
FROM ${model}
PARAMETER num_ctx ${current_config.num_ctx}
PARAMETER num_gpu ${current_config.num_gpu}
PARAMETER flash_attn ${current_config.flash_attn}
PARAMETER num_predict 4096
PARAMETER num_keep 1024
PARAMETER repeat_penalty 1.1
EOF"
docker exec ollama ollama create test-model -f /root/.ollama/test_${model}.modelfile
```
### 5. Run Benchmark
```bash
# Warm up
docker exec ollama ollama run test-model "Hello" > /dev/null
# Coding prompt
START=$(date +%s%N)
docker exec ollama ollama run test-model "Write a Python async context manager that retries a function with exponential backoff, max 5 retries, and logs each attempt using structlog. Include type hints."
END=$(date +%s%N)
# Calculate tokens/sec from output
```
### 6. Measure VRAM (if possible)
```bash
# Try host first
rocm-smi --showmeminfo vram 2>/dev/null || \
# Try via docker
docker exec --privileged ollama rocm-smi --showmeminfo vram 2>/dev/null || \
# Fallback
echo "VRAM measurement unavailable"
```
### 7. Record Results
- Parse tokens/sec from ollama output
- Record VRAM/RAM usage
- Determine if this is best config so far for this model
- Update `best_configs` if tokens/sec improved or context increased
### 8. Update State
```python
# Logic:
if test_successful:
if context_step < max_reached:
phase = "context_scaling"
current_config.num_ctx = next_context_step
else:
# Move to next model
model_index += 1
phase = "context_scaling"
current_config.num_ctx = context_steps[0]
else:
# OOM or error - record last good as best
best_configs[track][current_model] = last_good_config
model_index += 1
phase = "context_scaling"
```
### 9. Commit to Repo
```bash
cd /opt/data/infra
git add assets/ai-optimizer/
git commit -m "ai-optimizer: tested ${model} at ${num_ctx} ctx - ${status}"
git push origin master
```
### 10. Matrix Notification (if available)
```python
import os
if os.getenv("MATRIX_HOME_SERVER") and os.getenv("MATRIX_ACCESS_TOKEN"):
# Send notification to Matrix room
# Room ID from env or config
pass
# Else: silent
```
---
## Stop Conditions
1. All models in both queues have `best_configs` recorded
2. Manual intervention needed (error in state.json `error` field)
3. No progress for 3 consecutive runs (stuck)
---
## Error Handling
If any step fails:
1. Log error to state.json: `"error": {"message": "...", "timestamp": "..."}`
2. Do NOT increment model_index (retry next run)
3. Commit state with error field
4. Exit gracefully
---
## Important Notes
- **No num_parallel**: Do not use this parameter
- **Two tracks**: Complete GPU track first, then RAM track
- **Backend**: Start with ollama, llama.cpp testing is optional (requires uncommenting in compose.yml)
- **Host access**: Some commands need host - use docker exec or SSH if available
- **Ask before deploy**: If config changes needed in NixOS modules, show diff and wait for user confirmation before `nh os switch`
---
## Example State Transitions
**Start:**
```json
{"track": "gpu", "model_index": 0, "current_model": "devstral-small-2:24b", "current_config": {"num_ctx": 32768, ...}}
```
**After successful test at 32k:**
```json
{"track": "gpu", "model_index": 0, "current_model": "devstral-small-2:24b", "current_config": {"num_ctx": 65536, ...}}
```
**After OOM at 131k:**
```json
{
"track": "gpu",
"model_index": 1,
"current_model": "qwen2.5-coder:32b",
"best_configs": {
"gpu": {
"devstral-small-2:24b": {"num_ctx": 98304, "num_gpu": 99, "tokens_per_sec": 11.2}
}
}
}
```

View File

@@ -1,283 +0,0 @@
# AI Model Optimization Cron Job
**Goal:** Find optimal configurations for maximum context size with full hardware utilization.
**Hardware:**
- 2× AMD MI50 GPUs (32GB VRAM each, 64GB total)
- 128GB system RAM
- ROCm: HSA_OVERRIDE_GFX_VERSION=9.0.6, HIP_VISIBLE_DEVICES=0,1
---
## Model Queue
### GPU-Optimized (Coding - prioritize speed + context on GPU)
1. `devstral-small-2:24b` - Best coding model
2. `qwen2.5-coder:32b` - Strong coder, fits on GPU+offload
3. `codellama:34b-instruct` - Legacy but solid
### RAM-Optimized (Knowledge - prioritize max context, accept slower)
1. `qwen2.5:72b` - Best knowledge, needs heavy offload
2. `nemotron-3-nano:30b` - Good general knowledge
3. `mixtral:8x7b-instruct` - MoE, efficient for knowledge
---
## Optimization Strategy
**Two separate tracks:**
### Track A: GPU-Focused (Coding)
```
Baseline: num_ctx=32768, num_gpu=99, flash_attn=true
Steps:
1. Increase context: 32k → 65k → 98k → 131k → 163k
2. At each step, verify VRAM usage < 60GB (leave headroom)
3. If OOM: reduce num_gpu until stable, record best
4. Measure tokens/sec - if < 5 tok/s, consider context too high
```
### Track B: RAM-Focused (Knowledge)
```
Baseline: num_ctx=65536, num_gpu=50, flash_attn=true
Steps:
1. Increase context: 65k → 131k → 200k → 262k → 327k
2. Allow heavy RAM offload (system RAM up to 100GB)
3. If OOM: reduce context or num_gpu
4. Speed less critical - focus on max stable context
```
---
## Backend-Specific Configs
### Ollama (Modelfile parameters)
```
PARAMETER num_ctx <value>
PARAMETER num_gpu <layers>
PARAMETER flash_attn true/false
PARAMETER num_predict 4096
PARAMETER num_keep 1024
PARAMETER repeat_penalty 1.1
```
### Llama.cpp (CLI flags)
```
--ctx-size <value>
--n-gpu-layers <layers>
--flash-attn on/off
--n-predict 4096
--batch-size 4096
--ubatch-size 512
--cache-type-k f16
--cache-type-v f16
--split-mode layer
--no-mmap
```
---
## Host Test Instructions
**The cron runs inside the hermes container. Some tests require host access:**
### 1. VRAM Monitoring (HOST)
```bash
# Run on host to check VRAM usage during/after benchmark
sudo rocm-smi --showmeminfo vram
# Or via docker exec if rocm-smi available in container
docker exec --privileged ollama rocm-smi --showmeminfo vram
```
### 2. Running Ollama Benchmarks (CONTAINER)
```bash
# Pull model
docker exec ollama ollama pull <model>
# Create custom modelfile
docker exec ollama bash -c 'cat <<EOF > /root/.ollama/test.modelfile
FROM <model>
PARAMETER num_ctx 65536
PARAMETER num_gpu 99
PARAMETER flash_attn true
EOF'
# Create model from modelfile
docker exec ollama ollama create test-model -f /root/.ollama/test.modelfile
# Run benchmark (warm model first)
docker exec ollama ollama run test-model "Write a Python async context manager with exponential backoff"
# Cleanup
docker exec ollama ollama rm test-model
```
### 3. Running Llama.cpp Benchmarks (CONTAINER - needs llama.cpp container)
```bash
# Uncomment llama_cpp_devstral in compose.yml first
# Then rebuild: sudo nh os switch --flake .#lazyworkhorse
# Test via HTTP API
curl http://localhost:8300/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "devstral-2-small-llama_cpp",
"prompt": "Write a Python function",
"max_tokens": 100
}'
```
### 4. Deploying Changes (HOST via ai-worker)
```bash
# After optimization, commit results
cd /home/ai-worker/infra
git add assets/ai-optimizer/
git commit -m "ai-optimizer: new best config for <model>"
git push
# If config changes needed in ollama_init_custom_models.nix:
# 1. Edit the file
# 2. nixpkgs-fmt .
# 3. Show diff to user
# 4. Wait for confirmation
# 5. sudo nh os switch --flake .#lazyworkhorse
```
### 5. Accessing Host from Hermes Container
```bash
# SSH to host as ai-worker (key should be mounted)
ssh -i /path/to/key ai-worker@host.docker.internal
# Or via docker socket if mounted
# (not recommended for security)
```
---
## Benchmark Prompts
### Coding (Track A)
```
"Write a Python async context manager that retries a function with exponential backoff, max 5 retries, and logs each attempt using structlog. Include type hints and error handling."
```
### Knowledge (Track B)
```
"Explain the complete memory hierarchy in modern GPUs, from registers through L1/L2 caches to VRAM, and how data moves between them during matrix multiplication. Include bandwidth considerations for each level."
```
### Measurement
- Tokens per second (generation speed)
- Time to first token (latency)
- VRAM usage (via rocm-smi)
- System RAM usage (via free -h)
- Context success (did it complete without OOM?)
---
## State File Structure
`/opt/data/infra/assets/ai-optimizer/state.json`
```json
{
"track": "gpu",
"current_model": "devstral-small-2:24b",
"model_index": 0,
"phase": "context_scaling",
"backend": "ollama",
"current_config": {
"num_ctx": 65536,
"num_gpu": 99,
"flash_attn": true
},
"best_configs": {
"gpu": {
"devstral-small-2:24b": {
"backend": "ollama",
"num_ctx": 131072,
"num_gpu": 99,
"flash_attn": true,
"tokens_per_sec": 12.5,
"vram_used_gb": 58.2,
"tested_at": "2026-04-28T17:00:00Z"
}
},
"ram": {}
},
"completed_models": [],
"gpu_queue": ["devstral-small-2:24b", "qwen2.5-coder:32b", "codellama:34b-instruct"],
"ram_queue": ["qwen2.5:72b", "nemotron-3-nano:30b", "mixtral:8x7b-instruct"]
}
```
---
## Results CSV
`/opt/data/infra/assets/ai-optimizer/results.csv`
```csv
timestamp,track,model,backend,phase,num_ctx,num_gpu,flash_attn,tokens_per_sec,vram_gb,ram_gb,status,is_best
2026-04-28T17:00:00Z,gpu,devstral-small-2:24b,ollama,context_scaling,65536,99,true,15.2,52.1,18.4,success,false
```
---
## Cron Job Flow
```
1. Read state.json
2. If both queues empty → STOP (all models tested)
3. Select next model from current track queue
4. Pull model if needed (docker exec ollama ollama pull)
5. Create Modelfile / llama.cpp config with current test params
6. Run benchmark (both prompts)
7. Measure: tokens/sec, VRAM (rocm-smi), RAM (free -h)
8. If successful:
- Increase context (next step)
- Update current_config in state
9. If OOM/error:
- Record last good config as best_configs[track][model]
- Move to next model in queue
10. Update state.json
11. Append to results.csv
12. Git commit + push to /opt/data/infra
13. Send Matrix notification if available, else silent
```
---
## Matrix Notification (Optional)
```python
# If matrix credentials available in environment
if os.getenv("MATRIX_HOME_SERVER") and os.getenv("MATRIX_ACCESS_TOKEN"):
# Send completion notification
# Room: !ai-optimizer:lazyworkhorse.net (or similar)
pass
# Else: silent, just commit
```
---
## Files to Create
```
/opt/data/infra/assets/ai-optimizer/
├── state.json # Current progress
├── results.csv # All test results
├── best_configs.json # Final best configs (human-readable)
└── CRON_JOB_DRAFT.md # This file
```
---
## Notes
- **No num_parallel**: Removed to avoid limiting other settings
- **Two tracks**: GPU (coding/speed) vs RAM (knowledge/context)
- **Both backends**: Test ollama first, then llama.cpp if available
- **Host tests**: rocm-smi must run on host or privileged container
- **Deploy**: ai-worker has sudo for nh/nixos-rebuild, must ask user first

View File

@@ -1 +0,0 @@
timestamp,track,model,backend,phase,num_ctx,num_gpu,flash_attn,tokens_per_sec,vram_gb,ram_gb,status,is_best
1 timestamp track model backend phase num_ctx num_gpu flash_attn tokens_per_sec vram_gb ram_gb status is_best

View File

@@ -1,21 +0,0 @@
{
"track": "gpu",
"current_model": "devstral-small-2:24b",
"model_index": 0,
"phase": "context_scaling",
"backend": "ollama",
"current_config": {
"num_ctx": 32768,
"num_gpu": 99,
"flash_attn": true
},
"best_configs": {
"gpu": {},
"ram": {}
},
"completed_models": [],
"gpu_queue": ["devstral-small-2:24b", "qwen2.5-coder:32b", "codellama:34b-instruct"],
"ram_queue": ["qwen2.5:72b", "nemotron-3-nano:30b", "mixtral:8x7b-instruct"],
"context_steps": [32768, 65536, 98304, 131072, 163840, 200704, 262144, 327680],
"last_updated": "2026-04-28T17:00:00Z"
}

View File

@@ -1,64 +0,0 @@
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source
FROM tianon/gosu:1.19-trixie@sha256:3b176695959c71e123eb390d427efc665eeb561b1540e82679c15e992006b8b9 AS gosu_source
FROM debian:13.4
# Disable Python stdout buffering to ensure logs are printed immediately
ENV PYTHONUNBUFFERED=1
# Store Playwright browsers outside the volume mount so the build-time
# install survives the /opt/data volume overlay at runtime.
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
# Install system dependencies in one layer, clear APT cache
# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.)
# that would otherwise accumulate when hermes runs as PID 1. See #15012.
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \
curl poppler-utils imagemagick && \
rm -rf /var/lib/apt/lists/*
# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime
RUN useradd -u 10000 -m -d /opt/data hermes
COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/
COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
WORKDIR /opt/hermes
# ---------- Layer-cached dependency install ----------
# Copy only package manifests first so npm install + Playwright are cached
# unless the lockfiles themselves change.
COPY package.json package-lock.json ./
COPY web/package.json web/package-lock.json web/
RUN npm install --prefer-offline --no-audit && \
npx playwright install --with-deps chromium --only-shell && \
(cd web && npm install --prefer-offline --no-audit) && \
npm cache clean --force
# ---------- Source code ----------
# .dockerignore excludes node_modules, so the installs above survive.
COPY --chown=hermes:hermes . .
# Build web dashboard (Vite outputs to hermes_cli/web_dist/)
RUN cd web && npm run build
# ---------- Permissions ----------
# Make install dir world-readable so any HERMES_UID can read it at runtime.
# The venv needs to be traversable too.
USER root
RUN chmod -R a+rX /opt/hermes
# Start as root so the entrypoint can usermod/groupmod + gosu.
# If HERMES_UID is unset, the entrypoint drops to the default hermes user (10000).
# ---------- Python virtualenv ----------
RUN uv venv && \
uv pip install --no-cache-dir -e ".[all]"
# ---------- Runtime ----------
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
ENV HERMES_HOME=/opt/data
ENV PATH="/opt/data/.local/bin:${PATH}"
VOLUME [ "/opt/data" ]
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ]

View File

@@ -1,102 +0,0 @@
#!/bin/bash
# Docker/Podman entrypoint: bootstrap config files into the mounted volume, then run hermes.
set -e
HERMES_HOME="${HERMES_HOME:-/opt/data}"
INSTALL_DIR="/opt/hermes"
# --- Privilege dropping via gosu ---
# When started as root (the default for Docker, or fakeroot in rootless Podman),
# optionally remap the hermes user/group to match host-side ownership, fix volume
# permissions, then re-exec as hermes.
if [ "$(id -u)" = "0" ]; then
if [ -n "$HERMES_UID" ] && [ "$HERMES_UID" != "$(id -u hermes)" ]; then
echo "Changing hermes UID to $HERMES_UID"
usermod -u "$HERMES_UID" hermes
fi
if [ -n "$HERMES_GID" ] && [ "$HERMES_GID" != "$(id -g hermes)" ]; then
echo "Changing hermes GID to $HERMES_GID"
# -o allows non-unique GID (e.g. macOS GID 20 "staff" may already exist
# as "dialout" in the Debian-based container image)
groupmod -o -g "$HERMES_GID" hermes 2>/dev/null || true
fi
# Fix ownership of the data volume. When HERMES_UID remaps the hermes user,
# files created by previous runs (under the old UID) become inaccessible.
# Always chown -R when UID was remapped; otherwise only if top-level is wrong.
actual_hermes_uid=$(id -u hermes)
needs_chown=false
if [ -n "$HERMES_UID" ] && [ "$HERMES_UID" != "10000" ]; then
needs_chown=true
elif [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$actual_hermes_uid" ]; then
needs_chown=true
fi
if [ "$needs_chown" = true ]; then
echo "Fixing ownership of $HERMES_HOME to hermes ($actual_hermes_uid)"
# In rootless Podman the container's "root" is mapped to an unprivileged
# host UID — chown will fail. That's fine: the volume is already owned
# by the mapped user on the host side.
chown -R hermes:hermes "$HERMES_HOME" 2>/dev/null || \
echo "Warning: chown failed (rootless container?) — continuing anyway"
fi
echo "Dropping root privileges"
exec gosu hermes "$0" "$@"
fi
# --- Running as hermes from here ---
source "${INSTALL_DIR}/.venv/bin/activate"
# Create essential directory structure. Cache and platform directories
# (cache/images, cache/audio, platforms/whatsapp, etc.) are created on
# demand by the application — don't pre-create them here so new installs
# get the consolidated layout from get_hermes_dir().
# The "home/" subdirectory is a per-profile HOME for subprocesses (git,
# ssh, gh, npm …). Without it those tools write to /root which is
# ephemeral and shared across profiles. See issue #4426.
mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home}
# .env
if [ ! -f "$HERMES_HOME/.env" ]; then
cp "$INSTALL_DIR/.env.example" "$HERMES_HOME/.env"
fi
# config.yaml
if [ ! -f "$HERMES_HOME/config.yaml" ]; then
cp "$INSTALL_DIR/cli-config.yaml.example" "$HERMES_HOME/config.yaml"
fi
# Ensure the main config file remains accessible to the hermes runtime user
# even if it was edited on the host after initial ownership setup.
if [ -f "$HERMES_HOME/config.yaml" ]; then
chown hermes:hermes "$HERMES_HOME/config.yaml"
chmod 640 "$HERMES_HOME/config.yaml"
fi
# SOUL.md
if [ ! -f "$HERMES_HOME/SOUL.md" ]; then
cp "$INSTALL_DIR/docker/SOUL.md" "$HERMES_HOME/SOUL.md"
fi
# Sync bundled skills (manifest-based so user edits are preserved)
if [ -d "$INSTALL_DIR/skills" ]; then
python3 "$INSTALL_DIR/tools/skills_sync.py"
fi
# Final exec: two supported invocation patterns.
#
# docker run <image> -> exec `hermes` with no args (legacy default)
# docker run <image> chat -q "..." -> exec `hermes chat -q "..."` (legacy wrap)
# docker run <image> sleep infinity -> exec `sleep infinity` directly
# docker run <image> bash -> exec `bash` directly
#
# If the first positional arg resolves to an executable on PATH, we assume the
# caller wants to run it directly (needed by the launcher which runs long-lived
# `sleep infinity` sandbox containers — see tools/environments/docker.py).
# Otherwise we treat the args as a hermes subcommand and wrap with `hermes`,
# preserving the documented `docker run <image> <subcommand>` behavior.
if [ $# -gt 0 ] && command -v "$1" >/dev/null 2>&1; then
exec "$@"
fi
exec hermes "$@"

View File

@@ -61,6 +61,7 @@
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/openclaw_node.nix
./modules/nixos/security/ai-worker-restricted.nix
./users/gortium.nix
./users/ai-worker.nix
];

View File

@@ -36,7 +36,7 @@
"transparent_hugepage=always" # because mucho ram
];
# 2. Load the specific drivers found by sensors-detect
boot.kernelModules = [ "nct6775" "lm96163" ];
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" ];
# 3. Force the nct6775 driver to recognize the chip if it's stubborn
boot.extraModprobeConfig = ''
options nct6775 force_id=0xd280
@@ -49,6 +49,26 @@
networking.networkmanager.enable = true; # Easiest to use and most distros use this by default.
networking.hostId = "deadbeef";
# WireGuard VPN client -- always up, connects to wg-easy server
# Create age-encrypted secrets before deploying (run on the host):
# echo -n "<private_key>" | agenix -e secrets/wireguard_private_key.age
# echo -n "<preshared_key>" | agenix -e secrets/wireguard_preshared_key.age
networking.wireguard.interfaces = {
wg0 = {
ips = [ "10.8.0.3/24" ];
privateKeyFile = config.age.secrets.wireguard_private_key.path;
peers = [
{
publicKey = "rY9zII3AOm8rog2rv02PyA3Bq7zdvTOGkZapfCV1DkE=";
presharedKeyFile = config.age.secrets.wireguard_preshared_key.path;
allowedIPs = [ "10.8.0.0/24" ];
endpoint = "vpn.lazyworkhorse.net:51820";
persistentKeepalive = 25;
}
];
};
};
# Set your time zone.
time.timeZone = "America/Montreal";
@@ -158,7 +178,7 @@
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "prohibit-password";
# Additional hardening settings below in SERVER HARDENING section
};
hostKeys = [
{
@@ -171,6 +191,7 @@
services.dockerStacks = {
versioncontrol = {
path = self + "/assets/compose/versioncontrol";
envFile = config.age.secrets.containers_env.path;
ports = [ 2222 ];
};
@@ -221,6 +242,11 @@
path = self + "/assets/compose/homepage";
};
vpn = {
path = self + "/assets/compose/vpn";
envFile = config.age.secrets.containers_env.path;
};
# tak = {
# path = self + "/assets/compose/tak";
# };
@@ -264,6 +290,20 @@
mode = "0440";
path = "/run/secrets/openclaw_gateway_token";
};
wireguard_private_key = {
file = ../../secrets/wireguard_private_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_private_key";
};
wireguard_preshared_key = {
file = ../../secrets/wireguard_preshared_key.age;
owner = "root";
group = "root";
mode = "0400";
path = "/run/secrets/wireguard_preshared_key";
};
};
};
@@ -308,6 +348,203 @@
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# =============================================================================
# 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 = 10;
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
# (/run/current-system/configuration.nix). This is useful in case you
# accidentally delete configuration.nix.

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,36 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBOL29w
eGk1N2xxTHJtaUEvWWZmbkh1bk11Tjk3anNnMDB1cCtPYUMzdTNJCkdhQ08vblNG
UlV1K2xVTGZVTzFWYXAzcjZaMWs0RTFWdStKSmlSTURvK1EKLT4gLC1zKU8zVkgt
Z3JlYXNlIFUiXFcpS302IHByVn5jOy0gRDMKQjV3SHpDWUIybGFyQUg3ZlR0R2hV
eWM3SFlCVW5mdlpBVUF3a0xpNlZCeGNUd1oxTTlkc1RkTXdZS0lFTmN3Ci0tLSA3
VlBqM1VLWllZc0JnOTMvUFRjMU13OTdzMmhsdGJubkk5eGpERVVLYUk4Cnzh5UbU
FlgqpM8jkJ6XlsaIDCw/G3D6uJ/GRJW4gIekuhAUxpZJrc8eOA8ZuHfGrBbH3acV
tVafX5F0Kr2oOblqZ6gduZOUS52KmWH8stiBJM+e5ZZ7zRQVE4PJUKUPCzi+WdcH
zr295T//FOdicrYHdsjfziKEHzBtUCFiATW05+O2zMjYjO6cPzePcCzPWinwiID6
V+f6ngfkkQaj3wBGkzaieQJzRcdSwky21aVhGCCX/bvqx61iW2d5QAKxGbtQ2RcG
X1okr+xunAM94nzDMv46vyN97KxY7cZd4pAaOxoICc2Tfhtw6F+iS6QkQh1odJzO
7ZH+sSQCvndG+8z9shXGiHalASF5tdguM+JlEvAGljcaiAUtsQWxr9CoWiEkC6c6
NCaECSYO8Il+SXBQnSZSGJSNDhuPYCYrsjXGSAONFixuyeslAkq9x2WUaUS4H063
1QvRF7XO2tBPtgCLsSjdiGp0h+ImUaGdu6fDR7zrDsGsaAFCSFeH/rGNNXRQ2vP2
CSfPfDDCqpUSCn0WuA30BtaPLxGmZT6OjFevKzYMNDmdeq9ia/q8K0hmjLUBdN3k
tdYWbwoaf4gYbUWxSleD768b0Jgxss9Vod+sFQ+NYRksdGIeyND+aQIc312XehfA
qHFBS8nlj7eUF5bdvCYQ64z741mH4cNlGxyjPBH1x8FHnEOocJXYt1l2AZSRJmJA
c3z0QGXyuCbsrLBXWK1EKa/Juo4PGGsEVoLRhwJAQy9+i1JN0yrfRvSPyzvD4px6
wRPzlZ80MQdb2lv84WS/zcOEZmZzlLntszTRRdIfAsuaavP2Rquh4rEXABYeTZwp
5dem79s8bdW2nFsGMNz1OQKQwocyjYu1jJMHu6Gp7Ngdl1xyW7xfg0dezE1c0cIh
xt1aLER9YJp4n5to5cOH16l3mjDHnAvABx38xE9loNL3399J/evw7LxpTYQ4v2Xv
x8xnDHcqJ+deFSwyuUnMS5DkUeYuHmUl0Q2WYcfY+ibCmcgCb2ObTtuN1/ZxNYrL
OKrnmfuSvBgyuIOj5e6uWW0+Zs8dHKXu2TgV8WignxOhl5zQgCpCBlqVfO0t+NCu
Gi26hU/fhGWQ/1oQa3VkpGsypZbJpgQvfWxfcGHP/MMhnl01zzlP8/aexSY3pAxf
fz9v0IVh6xxtu3zbiiVzUsXbfG7t+xY98jMphf4AS2mWva3GWVmhhu0lS3J3P+go
YEEP4rOFHeU0Y1/6kLydTXvz4jMH0H92XQIzshd7vzQnEJPUPAzqRmw3LKYGgCI+
wZEnxJ6ckqTkGBFnxTpy9LLllwmnz2Ky87nY3XAmqxlhb2Ap1XFAlfgszmGjc+Il
KkIgoWQHTUm6QM9ta++oUTIDneOvxGd0zZsqoEhiC/7E01BNNZ6E58TeJU3fDlA3
mX6n05XjwPRpgXZfayPoAgBlZc2H4KeiynxwNZ/dWu7qz7L6Ppk6Nvtly8giTbFx
CA+tto7vq+D+CAEJ4bgyq4BCH4GL4APrhPcWp98Mko1WCiRTIKgkZxQCYvlg/LZq
LNhMacP9T1qTvNC+yR1NEMiegE3APzk6CkDpVaO9+5f/sqifNPINCMothenI9ePw
zjQLI3Mo1m73bkomytUZ7i1VstP5sEZ5LF72Sq7BpR3oQ3Gp0CAN9w==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBMcVY2
eWRnb05sMEZVZUpicGVYN2ZSUm1mUmVsdUsrSHR4c2I2SXVRWDF3CnlhUkxoeEx5
M1NNcGR3bmx6ZW5RaHU3L2l4WEowdWYzQk0xa3E4ZUtwNkkKLT4gIi1ncmVhc2Ug
SllwSzlIIEdAKltUKSBjSWpmCnljSXlZTXZBL2xuMEtma1NUNjdCM0dMNHJuVjFS
enV5LzF1NlYzbFNURW1rTlI2aUxCRFFxK2ZJdktsTTU1ZSsKNUZiZmVQWQotLS0g
bTVrZEdWMHFWK0Q5RzZUc09PYmlEQjNweXRxU2FETWNWTXRUVEFKUUFpdwqtUbmM
kbnj4Q+9QlnJHuWBVu+BcWPl5HuiPUjrrxnWAuN6rFYd79H7qk9MagDB/2BAEk82
iuQeqS0r8wAmp9bTzhbiMQEbtN96huSGA2aO9I/RoPU1jv1Upi8bNy+KX0jSsfV9
+cGFm0JsBZ4o5acq4isanC+3YkNq+IHxyDqUdwWMIHiEkR26pwndyMAaJTCBAgTN
kmdWMK4PxR36ElY+8J0tiOv6VmGS73rVS4R/2Rdc5ICx6QO6oQQqETKYxqcmr8eD
8dgkZVpmBF/iuw7BYZ2U/p2PZSzgEVTVqfT3VO8WQ9ii2a4dw+2QEORgISbhxzoy
WGB0q4X33QWEYSp0FNyAG4Kc9yTphUq+hMHfNOtz3XmTZLltcVfA6XQbKwB/nDbq
G1gAfDwdEZ5OpN2IT0S6Zc2rKKeovqdFYWgqmDWiaBfqncl9qLH37KkpKqmUSQSe
zyriQt8nZCzVrh4EKohjeLogVBsn0tPTYqiFnV+ZFK/kOWIYWduWDJcM3zwRVjx7
Mr5l81gFv4WRHbnQB9eynGJLZYs1lI4/X9tKRUgUj0mN20Bt80NXGNPaJ3TAkrCO
rX0tgjsX/Sc59JTHaMOT419+ob9UtDoS7mGlxswKfyhvjzluu+EdHd0GRJ5PUsSq
8YlKYjRonBqhC0Ju9CRuZS0pk2bh72bG9z1Gb4LcJ0pLJ6lMfmF1j4zzfzsABe5G
0bJF3ikzNxo6Wzsf3rk45/FvMhKjkI5O3Btnfp+Vkw+iRqDMh7wiD6cczNSp+Skd
Et43NIobiLa6O95y2YEjqSkT5T0ug1nbLkygmxwffVn6RTWgScZSfBPvOKTINAVo
J/MU2c0DaBm4glLfm4IWaJNcEmZn8+FWG6m42WEWTMEfSeAo5XXaEb0FsK0Yqd3+
RlE/b6a/DdoNEAQOlEPSASsoQhTVvsiEwH4Pq5G0STHtSBBIT/xJow4pa/GOiQnn
yAFva9F7KFYWA7bjbRbv+B21bvss0T+BK9HRF+bklSzjxBNfDEWXW0GhwIiahwXW
Fxi3BUMZcfgoLg2NkhMb3irwJUoGqQFqn68e2jTJVyUATyVgGV5mzfjDBB8thcDt
ehuIL/Y1bUVGWZteU/JAF7z+Fb1yBO2FJqpCKIcfd+JgkWVtQKaqYGjcuzbI7ce4
fcrRDNCse2pBO1unE7HS160rK+dMWavlrsHHZKezsvNv7TwMj1SKjCKSVr8up7TC
NLW0I6uXGEcUBj+RNqF6frdpw/Ve1WTAkIbznMV6kZ8cTfAGhOzJ0wxHMBSQka8X
uMatPuGywu+dvjrJXTwD0gJD/Jrd88K685ahu86nSt/DYnxIYfQhwo/oZMR7E6kN
4NgGtYjRU0asmio6sF8D1uA2YgmxNPRw2GwTqpS2XyYaEh3B6yjqa4pJ1vfo6t/g
aPLhMuT4qt/eKMvSiR+sTaOMNkcLWmCpY762aYk4XUZFTYAAY5XsNSAU+Hs7o/Eu
9NSMrIrpqLAcgzr/nZZTLqswbsYtdVl5WUd9uqdQe+AhTbrkcvHibUrVgW/XU+oq
QdHXUyfn/IByp2uE7WZpfRVhwjXg/LQJh/ogksbzrh4/anOivku+n1ouciAAgnQn
04i6taBu+393ysMC/sp7wZkp//yAZj2SNPOTzbakG8xWoVGzbqxLCIOIE7I97KBv
G49jiEOS42vZZXSGLxQND+N0aOqosZfQ1WKpI9XjirB8qVOt/sr6uqEIx311V+BJ
wrdoMa9hWmDFzf3+ThqfUuHOtxxJXReL7vC4J7K8iU6nCVIGJN7axifk
-----END AGE ENCRYPTED FILE-----

View File

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

View File

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

View File

@@ -9,6 +9,85 @@
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/
services.aiWorkerAccess = true;
# 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" ];
}
];
}
];
}