# 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 \<server\> Apply all updates on named server
/report Generate daily report now
/banned List Fail2Ban banned IPs
/unban \<ip\> Unban an IP
/restart \<container\> Restart a Docker container on Hetzner
/ask \<question\> 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:

  1. 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.