# Hermes — Infrastructure Agent **Search engine:** SearXNG at search.kaburu.cc (Docker on kaburusvr, LiteSpeed reverse proxy). Use for web searches. Two separate systems share the name "Hermes" on this infrastructure. Do not confuse them. --- ## 1. Hermes Docker Agent (custom monitoring stack) Automated monitoring, alerting, and management agent for the Kaburu infrastructure. Runs on kaburuaibox as a Docker Compose stack. Communicates via Telegram. ### Architecture ``` hermes_agent — Python scheduler + AI checks + Telegram bot hermes_redis — State store ``` Stack location: `/opt/hermes/` on kaburuaibox (192.168.0.253) > Note: The old `hermes_dashboard` FastAPI container was torn down 2026-06-01 (Hermes Workspace). > The NousResearch dashboard (port 9119) now serves as the web UI — see Section 2. ### Model routing (router.py v3 — last updated 2026-07-02) All tasks route to llama-server (qwopus-mccoder) as primary. DeepSeek API as fallback if llama-server down. Claude/Anthropic removed entirely — `ANTHROPIC_API_KEY` is blank. `ollama.service` stopped and disabled 2026-07-14 to prevent phantom scheduled burns. Start manually when needed for interactive use. See log.md entry 2026-07-14 (Gwen) for full incident. **CRITICAL: No reasoning models for automation.** `qwen36-reap:28b` and `glm47-reap:23b` perform "prompt processing" (internal thought tokens) at ~800 tok/s with zero output, consuming 75k+ tokens and 10+ GB VRAM per request. Gateway timeout 1800s + 3 retries = 90 min GPU burn per failed task. All recurring crons must use `no_agent: true` + bash scripts. Never route auxiliary tasks, cron, or fallback defaults to reasoning models. OpenWebUI stopped and disabled 2026-07-14 (user declares emergency-only). | Tier | Model | When | |------|-------|------| | Primary | qwopus-mccoder via llama-server (192.168.0.253:11440/v1) | Always | | Fallback | deepseek-chat via DeepSeek API | llama-server unreachable | Task routing: - `local_capable` (qwopus-mccoder): security, anomaly, architecture, strategic, log analysis, code gen - `local_fast` (qwopus-mccoder): health checks, backups, reports, WordPress, maintenance, routine Headroom Proxy at localhost:8787 provides context compression for all llama-server requests. `_ClaudeStub` class in router.py redirects any `router.claude.messages.create()` calls (used in Telegram chat path in hermes.py ~line 564) to qwopus-mccoder with an Anthropic-compatible response object. ### Scheduled jobs | Job | Schedule | Alert threshold | |-----|----------|-----------------| | Security check (Fail2Ban + SSH) | Every 60 min | 🚨 alert-only — no Telegram on clean | | SNMP trap monitor | Every 5 min | Any new trap | | Site uptime (16 sites) | Every 5 min | Any site returning 5xx or unreachable | | Server health — all hosts | Every 2 hours | 🚨 alert-only — no Telegram on clean | | SSL certificate expiry | 06:00 daily | <30 days warn, <7 days alert | | Update check — all servers | 06:30 daily | Security updates = immediate alert | | Backup log check | 07:00 daily (`BACKUP_CHECK_HOUR`) | Always notifies — success summary or failure alert | | WordPress check | 07:30 daily | Updates available or errors | | Daily summary report | 08:00 daily | Always sent | ### Monitored hosts | Host | Method | What's checked | |------|--------|----------------| | kaburusvr (Hetzner) | SSH via Tailscale 100.112.54.2 | Disk, memory, load, Docker, Fail2Ban, SSL, WordPress, backups | | kaburuaibox (Z840) | subprocess (local, inside container) | Disk, memory, load, Docker, ZFS, GPU, snapshots | | ubuntu-svr (UM790) | SSH via Tailscale 100.101.7.86 | Docker (Frigate, Omada, Portainer, Open-WebUI), Coral TPU, disk, memory | | truenas-scale | SSH via Tailscale 100.74.178.14 | ZFS pools, scrub, datasets, disk, memory | | ~~KaburuLINK (Unraid)~~ | Decommissioned 2026-05-29 | `check_unraid_health()` is a no-op | ### Update checker (updates.py — fixed 2026-06-03) Uses `apt-get upgrade --dry-run | grep '^Inst'` to determine actually-installable updates. Previous version used `apt list --upgradable` which included phased packages (Ubuntu gradual rollout) that cannot actually be applied — this caused false "feature updates available" reports. Phased packages only appear in the dry-run output if the specific machine is within the phase window — correct behaviour. Categories: - 🔴 security — package source URL contains "security" - 🔵 feature — everything else actually installable ### Backup check (hermes.py — fixed 2026-06-03) `check_backups()` now always sends a Telegram notification: - ✅ Success: `notify()` with LLM summary of the backup log - ⚠️ No log: `notify()` warning if `/var/log/hetzner-backup.log` not found on Hetzner - 🚨 Failed: `alert()` with LLM detail if ERROR in log and no completion marker Previous version was silent on success — only alerted on failure. ### SNMP monitoring Hermes receives SNMP traps from all nodes via snmptrapd on kaburuaibox (UDP 162). Trap log at `/var/log/snmptrapd-events.log`. `check_snmp()` polls every 5 minutes. | Trap type | Emoji | Alert level | |-----------|-------|-------------| | linkDown | 🔴 | Immediate alert | | linkUp | 🟢 | Info notification | | coldStart / warmStart | 🔄 | Info (unexpected reboots escalated) | | authenticationFailure | 🚨 | Immediate urgent alert | See [[snmp]] for full SNMP infrastructure documentation. ### Alert levels | Level | When | Telegram format | |-------|------|-----------------| | 🚨 URGENT | Security breach, site down, pool degraded, backup failed | Bold alert, immediate | | ⚠️ WARNING | Resources >80%, updates available, container restart | Standard notification | | ✅ INFO | Routine status, all clear | Silent store only (redis) | ### Telegram commands | Command | Action | |---------|--------| | /status | All panel statuses with timestamps | | /health | Latest health check across all hosts | | /security | Latest security report | | /wordpress | WP sites status + pending updates | | /wpupdate | Apply all WordPress updates now | | /backups | Latest backup status | | /ssl | SSL certificate status | | /uptime | Current site uptime status | | /updates | Latest update check report | | /updatesec | Apply security updates on all in-scope servers | | /updateall \ | Apply all updates on named server | | /report | Generate daily report now | | /banned | List Fail2Ban banned IPs | | /unban \ | Unban an IP | | /restart \ | Restart a Docker container on Hetzner | | /ask \ | Ask Hermes anything about the infrastructure | ### File structure ``` /opt/hermes/ ├── docker-compose.yml ├── .env ← API keys + credentials ├── config/ │ ├── hetzner_id_ed25519 ← SSH key for Hetzner │ ├── truenas_id_ed25519 ← SSH key for TrueNAS + ubuntu-svr │ ├── hermes_shell_id_ed25519 ← SSH key for OPNsense shell connector │ └── domains.txt ← domains to monitor SSL for └── agent/ ├── hermes.py ← main scheduler + checks ├── router.py ← model routing (v3, llama-server primary) ├── hetzner.py ← SSH connector to kaburusvr.uk ├── truenas.py ← SSH connector to TrueNAS ├── ubuntu_svr.py ← SSH connector to ubuntu-svr ├── unraid.py ← stub (KaburuLINK decommissioned) ├── opnsense.py ← HTTP API connector (hermes user, read-only) ├── shell.py ← generic SSH shell connector ├── snmp_monitor.py ← SNMP trap log watcher ├── updates.py ← cross-server update checker ├── telegram_bot.py ← send alerts/notifications ├── telegram_commands.py ← receive /commands ├── wordpress.py ← WP-CLI operations ├── memory.py ← Redis-backed memory/state └── requirements.txt ``` ### Environment variables (`/opt/hermes/.env`) | Variable | Purpose | |----------|---------| | `ANTHROPIC_API_KEY` | Blank — Claude removed from routing | | `DEEPSEEK_API_KEY` | DeepSeek API fallback | | `OPENAI_API_KEY` | Retained but unused in routing | | `HETZNER_HOST/USER/SSH_KEY_PATH/PORT` | SSH to kaburusvr.uk via Tailscale | | `TELEGRAM_TOKEN / TELEGRAM_CHAT_ID` | Telegram bot credentials | | `BACKUP_CHECK_HOUR` | Hour for daily backup check (default 3) | | `SECURITY_CHECK_INTERVAL` | Minutes between security checks (default 60) | | `CHECK_INTERVAL_MINUTES` | Minutes between health checks (default 15) | | `TRUENAS_HOST/USER/SSH_KEY_PATH` | TrueNAS SSH | | `UBUNTU_SVR_HOST/USER/SSH_KEY_PATH` | ubuntu-svr SSH | | `OPNSENSE_HOST/KEY/SECRET` | OPNsense API credentials | ### Management ```bash cd /opt/hermes # Status docker ps | grep hermes # Logs docker logs hermes_agent --tail 50 -f docker logs hermes_redis --tail 20 # Restart docker restart hermes_agent # Full rebuild (after code changes) docker-compose down && docker-compose build --no-cache && docker-compose up -d # Check redis state docker exec hermes_redis redis-cli keys "hermes:*" # Check SNMP trap log tail -f /var/log/snmptrapd-events.log ``` --- ## 2. Hermes Agent (NousResearch) — Production Open-source autonomous agent from Nous Research. Installed on kaburuaibox. Distinct from the Docker monitoring stack above. **Install location:** `/usr/local/lib/hermes-agent/` (system-wide) **User data:** `/home/kaburu/.hermes/` **Binary:** `/usr/local/bin/hermes` **Current version:** v0.15.1 (current HEAD as of 2026-06-03, 180 commits pulled) ### Services Three systemd units, all boot-persistent: | Service | User | Port | Purpose | |---------|------|------|---------| | `hermes-gateway.service` | kaburu | 8642 | Gateway + OpenAI-compatible API server | | `hermes-dashboard.service` | kaburu | 9119 | Web dashboard + TUI chat | | `hermes-mcpo.service` | root | 11435 | MCP-to-HTTP bridge for Open WebUI | ### Gateway (hermes-gateway.service) Runs `hermes gateway run --replace` as kaburu. Previous unit (`hermes-agent.service`) ran `hermes gateway start` which is a management command, not a runner. It crash-looped 5711+ times. Fixed 2026-06-03. Unit at `/etc/systemd/system/hermes-gateway.service`: - `TimeoutStopSec=210` — matches `restart_drain_timeout=180s` + margin - `StartLimitIntervalSec=120 / Burst=5` — in `[Unit]` section (not `[Service]`) - `Wants=llama-server.service` ### Dashboard (hermes-dashboard.service) Runs `hermes dashboard --host 0.0.0.0 --port 9119 --no-open --insecure --tui --skip-build`. `--insecure` flag disables same-origin auth guard — required for LAN/Tailscale access. Do NOT expose port 9119 to the internet. Access: `http://192.168.0.253:9119` from any LAN or Tailscale device. Install as PWA on phone/tablet for app-like experience. ### API server (port 8642) Enabled via `display.platforms.api_server.enabled: true` in config.yaml. Bound to `0.0.0.0:8642`. OpenAI-compatible API. Used by: - Nous Desktop app (remote mode) - Any OpenAI-compatible frontend Connection details for desktop app: - URL: `http://192.168.0.253:8642` - API key: stored in `/home/kaburu/.hermes/.env` as `API_SERVER_KEY` For Tailscale access: substitute kaburuaibox Tailscale IP (100.120.18.44). ### Model configuration (config.yaml) **Current (2026-08-03):** Primary is `deepseek-v4-flash` via DeepSeek API (`provider: deepseek`, base_url `https://api.deepseek.com/v1`, key `DEEPSEEK_API_KEY` in `.env`). Configured at Steve's request. Key verified against `GET /v1/models` — DeepSeek's API serves ONLY `deepseek-v4-flash` / `deepseek-v4-pro`; the dated `-0731` ID would 404 there. NOTE: `deepseek-v4-flash-0731` IS DeepSeek's production V4-Flash release (Jul 31, supersedes April preview; huge agentic gains — AAII 50 vs 40) — it's served under the STABLE ID `deepseek-v4-flash` per DeepSeek API docs ("The `deepseek-v4-flash` model has been updated to DeepSeek-V4-Flash-0731"). So the stable ID already runs the 0731 weights. Dated IDs exist only on HF/OpenRouter/Nous catalog. Verified end-to-end with one-shot `hermes chat -q` (PONG) through the config default. Cron model snapshots cleared (no pins — jobs inherit global model). ```yaml model: default: deepseek-v4-flash provider: deepseek base_url: https://api.deepseek.com/v1 ``` **Housekeeping (2026-08-03):** `updates.pre_update_backup: quick` (was legacy `true` = full zip of HERMES_HOME pre-update → 20GB zip pile-up, hung updates). ~/.hermes cleaned 32G → 4.2G: removed 3 old pre-update zips, orphaned 8.8GB GGUF, malformed state.db backup. Keep the newest pre-update zip + state-snapshots until update confirmed stable (~1.2GB floor after: state.db 0.75GB is legit session history). ⚠️ state.db index corruption — FIXED (dup rowid purged, unique index added). ✅ state.db ghost NULL cell — **PURGED 2026-08-06** (was "residual ghost NULL artifact benign"): ghost was NOT an index phantom — a real corrupt row (rowid 274877908374, NULL id + NULL started_at + 4050B blob source, dup-rowid class from Jul 30 torn write) invisible to index-backed queries (`WHERE started_at IS NULL` uses idx_sessions_started which lacked the entry) but visible to `WHERE id IS NULL` / `.dump`. Deleted the single cell via `DELETE WHERE rowid=274877908374` — integrity_check now **ok** (first clean since Jul 30), sessions 2962→2961, FTS in sync. No downtime. Pre-delete rollback: /home/kaburu/state.db.pre-ghost-delete-20260806.db. ✅ Nightly z840 backup permission errors — FIXED (midclt chown + `-rlptz` flags + symlink purge). **History:** 2026-07-26 `z-ai/glm-5.1` via Nous Portal replaced unstable `qwen/qwen3.7-plus` (404'd twice, Nous catalog rotation). 2026-08-03 `deepseek-v4-flash` direct via DeepSeek API per Steve (backup: `~/.hermes/config.yaml.bak.deepseek.20260803171528`). **Fallback chain** (in `~/.hermes/config.yaml`, must be added manually with proper YAML list syntax — `hermes config set` stringifies it): ```yaml fallback_providers: - provider: kat model: KAT-Coder-V2.5-Dev-APEX-I-Mini base_url: http://127.0.0.1:11440/v1 - provider: custom model: qwopus-mccoder:latest ``` **Why these specific fallbacks:** - `KAT-Coder-V2.5-Dev-APEX-I-Mini` — **first local fallback** (2026-08-04). Qwen3.5 MoE coder, 13.5GB via llama-server on port 11440. **ENABLED persistently 2026-08-08** (`systemd enable --now llama-server-kat.service`) — survived reboot check, auto-loads at boot. Native 262K ctx, evaluated 46/50 on the ops eval (same as TC-McQwen) at 6.4× the speed. Replaces TC-McQwen as the preferred local ops/code model. - `qwopus-mccoder:latest` — 27B, code/Divi capable, second-tier fallback - GLM Reap (`glm47-reap:23b`) **REMOVED 2026-08-08** — Steve: "thick as shit, no context". Deleted from Ollama + GGUF on disk, removed from fallback chain, zero references in config.yaml. First fallback when the Nous/deepseek primary errors is now KAT directly. Local Ollama custom providers still configured (`local-ollama`, `ollama-vision`) for when Ollama is manually started. Local models available: `qwen36-reap:28b`, `qwopus-mccoder:latest`, `mistral-nemo:12b`, `minicpm-v:8b`. (glm47-reap:23b removed 2026-08-08.) **Removed 2026-07-26:** `qwen/qwen3.7-plus` from Nous Portal — closed-source, Nous rotates the ID without notice, mid-session 404s. Replaced with `z-ai/glm-5.1` (Z.ai, 200K context, stable). **Cleaned 2026-07-08:** Removed OpenRouter (API key + config block), and earlier Nous-primary setup. Dead `local-llama` custom provider removed. `ollama-vision` → `minicpm-v:8b`. `compression.enabled: true`. `model_catalog.url: ""` (disabled phoning home). ### .env (`/home/kaburu/.hermes/.env`) | Variable | Value | |----------|-------| | `OLLAMA_BASE_URL` | http://localhost:11434 | | `DEEPSEEK_API_KEY` | set | | `XAI_API_KEY` | set (primary provider as of 2026-07-14) | | `API_SERVER_KEY` | set — bearer token for port 8642 | | `API_SERVER_HOST` | 0.0.0.0 | | `API_SERVER_PORT` | 8642 | | `GATEWAY_ALLOW_ALL_USERS` | true — open to all on LAN/Tailscale | | `OPENROUTER_API_KEY` | REMOVED 2026-07-08 — OpenRouter provider deleted | ### MCP bridge (hermes-mcpo.service) Wraps `hermes mcp serve` via mcpo, exposed on port 11435. Consumed by Open WebUI as a tool server. ### Desktop app access (cross-device) | Device | Method | URL | |--------|--------|-----| | Any browser (LAN/Tailscale) | Direct | http://192.168.0.253:9119 | | Phone/tablet | PWA install | http://192.168.0.253:9119 | | Nous Desktop app | Remote mode | http://192.168.0.253:8642 + API key | | Tailscale (off-LAN) | Substitute Tailscale IP | http://100.120.18.44:9119 or :8642 | --- ## History | Date | Event | |------|-------| | 2026-05-11 | Initial Docker agent build on ubuntu-svr | | 2026-05-16 | Fail2Ban alert tuning (bantime 600→86400) | | 2026-05-17 | Docker agent migrated to Z840 (kaburuaibox) | | 2026-05-18 | Added TrueNAS, ubuntu-svr, Unraid, OPNsense monitoring | | 2026-05-18 | SSH reconnect retry logic added to all connectors | | 2026-05-19 | Update checker added (updates.py) | | 2026-05-25 | SNMP trap receiver added. All nodes sending traps | | 2026-05-25 | OPNsense SSH → HTTP API (hermes user, read-only) | | 2026-05-29 | OPNsense connector rewritten | | 2026-05-30 | Health check intervals reduced; alert-only mode | | 2026-05-30 | KaburuLINK (Unraid) decommissioned | | 2026-06-01 | NousResearch Hermes gateway crash-loop disabled (hermes-agent.service) | | 2026-06-01 | Open WebUI connected to llama-server via OPENAI_API_BASE_URL | | 2026-06-01 | Docker agent router.py rewritten (v3) — Claude removed, all tasks → qwen36-reap | | 2026-06-01 | GLM VRAM OOM documented — cannot coexist with qwen36 in 24GB | | 2026-06-03 | GLM removed from models.ini — qwen36-reap only model in llama-server | | 2026-06-03 | NousResearch Hermes updated to current HEAD (180 commits) | | 2026-06-03 | hermes-gateway.service rewritten — correct exec, TimeoutStopSec=210 | | 2026-06-03 | hermes-dashboard.service created — port 9119, LAN/Tailscale access | | 2026-06-03 | API server enabled on port 8642 — desktop app + cross-device access confirmed | | 2026-06-03 | provider: openai → provider: custom in config.yaml (fixes "Unknown provider" error) | | 2026-07-08 | Model cleanup: removed OpenRouter (API key + config), removed Nous Research refs (MoA, portal, catalog) | | 2026-07-08 | Removed dead custom_provider local-llama (llama-server disabled). Fixed ollama-vision -> minicpm-v | | 2026-07-08 | Ollama model purge: removed glm4:9b, moondream, mistral-nemo:128k. Kept glm47-reap, qwen36-reap | | 2026-07-08 | qwopus-mccoder imported from GGUF into Ollama (Qwopus3.6-27B-v2-MTP-Q4_K_M, 16GB) | | 2026-08-08 | KAT-Coder enabled persistently (llama-server-kat.service was disabled → didn't survive reboot). GLM Reap removed from Ollama + fallback chain ("thick as shit, no context"). kaburu.uk DB registered in CyberPanel, PHP limits fixed | | 2026-06-03 | updates.py — switched to apt-get dry-run to filter phased packages | | 2026-06-03 | check_backups() — now notifies on success, not just on failure | ## Notes - Security check is conservative — only alerts on genuine issues, not SSH brute-force noise - `PermitRootLogin=prohibit-root-only` on all servers — password brute-force always fails - Tailscale 100.64.0.0/10 always whitelisted in Fail2Ban - Docker agent logs persist on ZFS at `/kaburudata/hermes/logs/` - Port 9119 (dashboard) must not be exposed to internet — no auth guard when `--insecure` - Port 8642 (API) requires bearer token — safer to expose if needed, but Tailscale preferred ### MCP servers (config.yaml — command/stdio transport) | Name | Script | |------|--------| | hetzner | /usr/local/bin/kaburu-shell-mcp-hetzner.py | | kaburuaibox | /usr/local/bin/kaburu-shell-mcp.py | | opnsense | /usr/local/bin/kaburu-shell-mcp-opnsense.py | | truenas | /usr/local/bin/kaburu-shell-mcp-truenas.py | ubuntu-svr, cyberpanel, cloudflare accessible via hetzner tool (SSH proxy). approvals.cron_mode = auto — tools work unattended in cron jobs. ## Cron model assignments (2026-07-08) | Model | Jobs | Schedule | |-------|------|----------| | **qwopus-mccoder** | ssl-check, update-check, wordpress-check | 06:00, 06:30, 07:30 daily | | | docker-watch, lynis-review | Mon 09:00, 10:00 weekly | | **glm47-reap:23b** | backup-check | 07:00 daily | | | observer, reflector, pruner (memory pipeline) | every 4h, 03:00 daily, Sun 04:00 | | **DeepSeek** | health-check, security-audit | hourly, every 6h | | | daily-report, pagespeed-report | 08:00, 08:30 daily | | **mistral-nemo:12b** | security-check (no_agent script) | every 30min | | **minicpm-v:8b** | Always loaded (vision) | KEEP_ALIVE=-1 | | **none (no_agent script)** | state-db-integrity (`0267b2efda86`) — PRAGMA integrity_check on ~/.hermes/state.db, silent when clean, 🚨 Telegram on failure. Detection ≤24h for torn-write corruption. Deployed 2026-08-06 (born from the ghost NULL cell incident) | 08:45 daily | ### VRAM strategy minicpm-v:8b stays loaded 24/7 (4.9GB, partially on CPU when coexisting with large models). qwopus (18GB) and glm47 (12GB) swap on-demand — ~2 min load from NVMe. Only ONE large model loaded at a time — the scheduler handles unloading automatically.