⚠️ Outdated (2026-10-09). The single "Agent Hub" LXC 122 described below was decommissioned on 2026-08-14 and split in two: LXC 126
ollama(192.168.86.218 — shared Ollama API on :11434, used by n8n's monitoring agent and Odysseus) and LXC 127ntfy(192.168.86.202 — ntfy on :8080, public namentfy.codycodez.com, plus theaction-executorAPI on :8081). SearXNG, the dashboard and the lead-gen workflows were dropped. The sections below still describe the original build and are kept for history. The local assistant has its own page, Jarvis (it uses ntfy topicjarvison LXC 127 but is independent of this stack).
Local AI agent platform: a self-hosted LLM (Ollama), a private search backend (SearXNG), a push-notification server (ntfy), and a small hand-written FastAPI service ("action-executor") that gives n8n workflows a tightly-scoped, audited way to search, reason, notify, and take pre-approved actions on the homelab. n8n (LXC 105) is the orchestrator — it calls into this LXC's services on a schedule; nothing in here runs autonomously on its own.
Backs three autonomous agents (see Agents below) plus the Agent Hub Dashboard. If it's down: the 3 scheduled n8n agent workflows fail (no local LLM, no search, no action execution), but nothing else in the homelab depends on it — Frigate, MQTT, etc. all keep running independently.
| Field | Value |
|---|---|
| Hostname | agent-hub |
| VMID / Type | LXC 122, unprivileged, nesting=1,keyctl=1 |
| Proxmox Node | SuperPC |
| IP Address | 192.168.86.218 |
| CPU / RAM | 4 vCPU / 8 GB (+2 GB swap) |
| Storage | 32G rootfs, local-lvm |
| Login | root via console/SSH |
| Port | Protocol | Purpose | Exposed to | Auth |
|---|---|---|---|---|
| 8080 | HTTP | ntfy (push notifications) | LAN | none |
| 8081 | HTTP | action-executor API | LAN | Bearer token |
| 8082 | HTTP | SearXNG (search) | LAN | none |
| 8090 | HTTP | Agent Hub Dashboard | LAN | none |
| 11434 | HTTP | Ollama API | LAN | none |
None of this is exposed through Nginx Proxy Manager / a public domain — LAN-only by design, no WAN attack surface.
| Component | Type | Path |
|---|---|---|
| SearXNG | Docker Compose | /opt/agent-hub/searxng/docker-compose.yml, config searxng/config/settings.yml |
| ntfy | Docker Compose | /opt/agent-hub/ntfy/docker-compose.yml |
| action-executor | native systemd (Python venv) | /opt/agent-hub/action-executor/app.py, service action-executor |
| Dashboard | native systemd (Python venv) | /opt/agent-hub/dashboard/app.py + static/, service agent-hub-dashboard |
| Ollama | native systemd (apt package) | models under /usr/share/ollama/.ollama |
Ollama models: qwen2.5:7b-instruct-q4_K_M (base, CPU inference — no GPU passthrough to this LXC) and agent-hub-llm (a Modelfile wrapping the base model with num_thread 4; does not yet have its own SYSTEM prompt — each agent's persona/instructions live entirely in that workflow's own prompt text in n8n, not in the model itself).
SearXNG: the Brave engine is explicitly disabled in settings.yml — it was getting scrape-blocked (429, 3-minute suspension) on every single search, which was surfacing as flaky/slow lead-gen runs.
Three n8n workflows (in n8n, LXC 105) drive this LXC. All follow the same 3-tier decision pattern, logged via POST /log to action-executor's audit_log table: 1_noise (logged only), 2_notify (pushed to ntfy), 3_act (executes a pre-approved whitelisted action, then confirms via ntfy).
Runs every 20 minutes. Pulls read-only status from 3 of the 4 online Proxmox cluster nodes (SuperPC, M910q, NAS — mini-pc intentionally excluded) via a restricted, read-only SSH key hitting a forced-command wrapper script on each host (/monitor/collect-logs → per-host journal warnings from the last 20 minutes + pct list; SuperPC's also includes full Ceph status, since Ceph is cluster-wide). A "Dedup Logs" code node collapses runs of 3+ identical consecutive log lines (e.g. a noisy pvestatd warning firing every ~10s) into one summarized line before the LLM sees it, so repetition doesn't drown out real signal. The LLM classifies the combined output into a tier; 3_act is restricted to exactly two pre-approved remediations:
| Action | Effect | Mechanism |
|---|---|---|
restart-frigate |
restarts the Frigate NVR container | SSH (restricted key) → forced-command wrapper on SuperPC → pct exec 115 -- docker restart frigate |
restart-mqtt |
restarts Mosquitto | SSH (restricted key) → forced-command wrapper on SuperPC → pct exec 116 -- systemctl restart mosquitto |
The LLM can never invent a different action — the forced-command wrapper on the target host is a hardcoded case statement; anything not in the allowlist is rejected, no shell passthrough. Active as of 2026-07-14.
Runs daily. Rotates through a small set of local-business category/area search queries via SearXNG, flattens the top results per query, and — one at a time — asks the LLM to judge whether each result looks like a real small business with a weak/missing web presence (directory-listing-only, Facebook-page-only, etc.). Qualifying leads get saved via POST /leads. Currently inactive (manual-trigger only) — activate once you're happy with lead quality.
Stub — no mailbox wired in yet. Intended to triage inbound messages and draft replies to POST /drafts for manual approval (nothing auto-sends). Not built out beyond the placeholder workflow.
http://192.168.86.218:8090 — a small self-contained status page (FastAPI + static HTML/CSS/JS, agent-hub-dashboard service) showing each of the 3 agents above as a little creature whose animation/color reflects real state (idle/working/needs attention/asleep), a run-history sparkline (last 14 n8n executions), and a click-through detail panel showing that agent's actual recent output (leads saved, drafts pending, or audit log entries). Polls /api/status every 20s. Pulls n8n's execution history via the same restricted read-only SSH key used for monitoring (a n8n-status command on the M910q forced-command wrapper), and reads action-executor's audit.db directly (same LXC, no network hop, no credential needed).
/opt/agent-hub/action-executor/keys/api_token): random 48-char bearer token, checked on every endpoint except none — all endpoints require it. n8n holds this as a Header Auth credential named agent-hub-bearer (Name: Authorization, Value: Bearer <token>, domain-restricted to 192.168.86.218).restart_key (action-executor's keys dir) — only reaches the tier-3 action wrapper on SuperPC (agent-hub-actions.sh), which only accepts restart-frigate / restart-mqtt.monitor_key (action-executor's keys dir, also reused by the dashboard backend) — read-only, reaches collect-logs on SuperPC/M910q/NAS and n8n-status on M910q. All of it read-only; never mutates anything./root/.ssh/authorized_keys (it's a symlink to /etc/pve/priv/authorized_keys, Proxmox's cluster-wide config store), so the forced-command wrapper script referenced by that shared key entry must exist under the identical path on both hosts (/usr/local/bin/agent-hub-n8n-status.sh) even though its content differs per host. SuperPC has its own independent, non-symlinked authorized_keys.Not currently backed up. LXC 122 is missing from the cluster's Sunday 01:00 vzdump job (backup-0343c483-22ec — its vmid list runs through 121 but doesn't include 122). Nothing here is currently irreplaceable (workflows live in n8n's own backup coverage, Ollama models are re-downloadable, action-executor's audit.db is the only thing that would actually be lost — leads/drafts/history). Worth adding 122 to the job.
# SearXNG / ntfy
cd /opt/agent-hub/searxng && docker compose pull && docker compose up -d
cd /opt/agent-hub/ntfy && docker compose pull && docker compose up -d
# action-executor / dashboard (native, no image to pull — edit app.py, then:)
systemctl restart action-executor
systemctl restart agent-hub-dashboard
# Ollama
apt update && apt upgrade ollama
agent-hub-llm Ollama model has no real customization yet — it's the base qwen2.5:7b-instruct plus a thread-count tweak, no SYSTEM prompt.httpHeaderAuth credential is still pointing at a placeholder ID that was never wired to the real agent-hub-bearer credential. Re-select the real credential on that node (this bit all three original agent workflows on first build — check every node with authentication: genericCredentialType if a new one starts failing the same way).app.py; e.g. LeadEntry.business_name/signal are str | None because the LLM's own prompt allows returning null for either — if a model field doesn't match what the LLM is told it's allowed to say, requests with null in that field get rejected.authorized_keys file; confirm the forced-command script exists at the exact path that shared entry references, on both hosts.pct enter 122 # from SuperPC
systemctl status action-executor agent-hub-dashboard ollama
docker ps -a # searxng, ntfy
sqlite3 /opt/agent-hub/action-executor/audit.db "select * from audit_log order by ts desc limit 10;"
curl -s http://192.168.86.218:8090/api/status | python3 -m json.tool # dashboard state
ollama list # installed models
| Date | Change |
|---|---|
| 2026-07-13 | Initial build: LXC 122 on SuperPC, Ollama + SearXNG + ntfy + action-executor (3-tier agent pattern, whitelisted SSH actions via forced-command wrappers), 3 n8n workflows created (Homelab Monitoring, Lead-Gen Outbound, Lead-Gen Inbound Triage stub). |
| 2026-07-14 | Debugged and fixed lead-gen workflow: disabled SearXNG's rate-limited Brave engine; added timeout+retry to the LLM judging step; fixed the placeholder-credential bug (all 3 workflows had a fake httpHeaderAuth credential ID that was never wired to a real credential); loosened action-executor's LeadEntry schema to match what the LLM prompt actually allows. Built the Agent Hub Dashboard (creature-per-agent status page + run history + click-through detail panel). Activated the Homelab Monitoring Agent; extended its log collection from SuperPC-only to SuperPC + M910q + NAS via a new read-only SSH forced-command pattern; added a log-dedup step ahead of the LLM step. |