Jarvis (Hermes Agent)¶
Jarvis is the homelab's autonomous AI assistant — the Nous Research Hermes Agent running on its own dedicated VM so it can be isolated (or killed) instantly.
| Host | VM 121 hermes-jarvis on pve2 — 192.168.1.252 (onboot=1 since 2026-07-28). Moved pve1 → pve2 on 2026-09-08 and resized onto the heavy node: 8GB RAM / 8 cores / 48GB on local-zfs-pve2 (was 3GB / 32GB). |
| Install | Bare-metal, systemd services hermes-gateway + hermes-dashboard (non-root jarvis user) |
| Web UI | jarvis.mdhmedia.uk — built-in Hermes dashboard (:9119), behind tinyauth SSO (Google login) |
| Messaging | Telegram bot (outbound polling, allowlisted users only) |
| Models | Per-profile since 2026-09-10 — default on deepseek/deepseek-v4-flash, claude on claude-opus-4-8, codex on gpt-5.6-sol. Inference was verified on default only (its gateway is the one running); the two workers are configured but untested in this pass |
| Profiles | Three since 2026-09-10 — default (orchestrator, gateway running), claude (alias hclaude, stopped), codex (alias hcodex, stopped). default routes work to the two workers; the retired neteng profile remains on disk, dormant, and must not be revived as-is |
| Crons | Renamed neteng-* → jarvis-* on 2026-09-10. Two are actually scheduled — jarvis-malware-sweep (07:30) and jarvis-backup-watchdog (20:05), both last-run ok. reclaim-scan is no longer scheduled, despite SOUL.md naming six crons |
| Repo | hermes-vm/agent/ — setup script, config templates, full README; archive/neteng-profile/ — the retired network-engineer profile |
Why a dedicated VM¶
The agent runs shell commands as part of its job, so the VM boundary is the sandbox — not a container. Containerizing an infra-ops agent forces a docker-socket mount or SSH-back-to-host, which is host-root with extra steps.
ssh root@192.168.1.199 "qm stop 121" # pull the plug — severs everything
ssh root@192.168.1.199 "qm start 121" # self-resumes via systemd
Backups
All agent state (config, memories, skills, session history) lives in
/home/jarvis/.hermes. VM 121 is backed up by the daily 19:15 PBS job
(primary — datastore main on pve3, which is now a wake-on-LAN box woken
at 19:00 each evening), with a local pve1 vzdump (Sun 02:00, keep-last 1)
as a floor and an incremental encrypted offsite from the PBS datastore in
the same evening window. See
Proxmox → Backups.
VS Code ACP bridge¶
Hermes exposes the Agent Client Protocol (ACP)
natively through hermes acp. Its transport is stdio JSON-RPC only: there is
no socket, listening service or open port. The VS Code client starts that
process through SSH, so Hermes runs on VM 121 and JSON-RPC rides the existing
SSH channel. The earlier design for an MCP service on the VM was rejected in
favour of this path because ACP adds no network attack surface to the VM.
The client configuration lives on the operator's laptop, not in this repo. It
uses Jun Han's formulahendry.acp-client extension ("ACP Client for VS Code")
and this SSH alias:
Host jarvis-vm
HostName 192.168.1.252
User jarvis
IdentityFile ~/.ssh/claude-code-lan
IdentitiesOnly yes
RequestTTY no
ServerAliveInterval 30
ServerAliveCountMax 6
The laptop's VS Code settings.json has a separate acp.agents entry with
these exact values:
"acp.agents": {
"Hermes (Jarvis VM)": {
"command": "ssh",
"args": ["-T", "jarvis-vm", "hermes", "acp"],
"env": {}
}
}
RequestTTY no and -T are load-bearing. A PTY can inject terminal control
bytes into stdout and corrupt the JSON-RPC framing. The extension's shipped
Hermes Agent entry was deliberately left in place beside the VM entry; it
invokes a bare local hermes and cannot start anything because Hermes is not
installed on the laptop. Do not repurpose that default entry as the VM bridge.
Behaviour recorded on 2026-08-28¶
hermes acp --checkreturnedHermes ACP check OKon VM 121.- A JSON-RPC
initializeoverssh -T jarvis-vm hermes acpreturned cleanly, with pure JSON-RPC on stdout and Hermes logs on stderr. - The handshake reported agent name
hermes-agent, version0.20.0, protocol version 1,loadSession: true, session capabilitiesfork,listandresume, and auth methodsopenai-codex("openai-codex runtime credentials") plus the terminal-basedhermes-setupmethod. - Hermes' documentation says ACP sessions are in-memory only and server restart
clears session history, while the live agent advertised
loadSession,fork,listandresume. This is an observed discrepancy, not a resolved statement about persistence. - End-to-end tool execution and filesystem access worked. The agent wrote
/home/jarvis/acp-connection-test.txt, proving that its filesystem context is the VM rather than the laptop workspace. Durable work must still cross the established GitHub boundary; no completed work may exist only on the VM. - A session took roughly 10 seconds to start while plugin discovery found 55 plugins and enabled 48.
Phase 13 is complete: Stage D passed 2026-09-01. An audit of 17 laptop
configuration surfaces found no critical-path dependency on VM 121, and a live
test with the VM stopped confirmed the bridge fails closed (ssh -T jarvis-vm
hermes acp times out cleanly) while git and gh to github.com keep working;
Jarvis recovered ~20s after restart. The bridge is non-essential to laptop Git
work by design.
VM maintenance recorded with the bridge¶
The same session ran hermes doctor --fix, which migrated _config_version
from 33 to 34. hermes acp --setup-browser installed agent-browser and Chrome
for Testing 152.0.7977.64 under /home/jarvis/.agent-browser/browsers/; the
binary launched, so the installer's suggestion to run
agent-browser install --with-deps did not apply. The remaining hermes doctor
findings are npm advisories in the web and ui-tui build workspaces (3-4 high
in each). These are build-tooling advisories, not a runtime risk.
Only openai-codex was authenticated, using gpt-5.6-sol; Nous Portal,
MiniMax, xAI and all other providers were logged out. The live profile was
default with its gateway running. The retired neteng profile still existed
on disk in a stopped state; it has not been live since 2026-08-09.
Memory cap repair¶
Hermes injects ~/.hermes/memories/MEMORY.md in full into the system prompt at
session start. memory_char_limit is a hard cap: a write that would exceed it
returns an error rather than silently dropping entries. The 2026-08-09
neteng-to-default consolidation appended the old profile's memory to the
base file, growing it from 1,328 to 24,964 characters against a 2,200-character
cap (11.3 times over). It then went unwritten for 19 days and 80 sessions,
consistent with memory writes returning errors.
The 2026-08-28 fix set and read back the larger cap through the harness with
hermes config set memory.memory_char_limit 40000, then rebuilt MEMORY.md to
30,088 characters. Seven entries were removed after each was found to be
false or obsolete: the claude-opus-4-8/Anthropic OAuth fallback chain with
Nous fallbacks; the claim that pve2 had no node exporter or Alloy; a nonexistent
02:00 user-systemd timer said to restart both gateways; three point-in-time PBS
backup-freshness JSON snapshots; and a duplicated Ubuntu VM entry.
Three facts were corrected in place: pve1 SSD life is 14% as of 2026-08-28;
the pve1 HighMemoryUsage condition was resolved on 2026-08-11 by capping ZFS
ARC at 2 GiB; and the expected-state table now records VM 123 haos as parked
(stopped, onboot=0). The embedded "MDH Media Homelab - Network Engineer
Knowledge Pack" seed was preserved intact.
The trade-off was explicit: all 30,088 characters are injected on every
session, roughly 11,000 tokens against the intended budget of about 800. The
operator chose that cost, and it is reversible by moving the knowledge pack
back into HOMELAB.md. The standalone ~/.hermes/HOMELAB.md is not referenced
by config.yaml or .env, so it is not auto-injected. SOUL.md tells Hermes to
read it without giving its location; that is why the agent looked in
/home/jarvis and incorrectly reported the file missing.
Security posture¶
- Command approval
manual(never YOLO mode); Telegram allowlist, deny-by-default gateway auth; SSRF protection on. - Hardened systemd unit:
NoNewPrivileges,ProtectSystem=strict,PrivateTmp. - The CT 101 Arcane manager can create and destroy containers on VM 121. A
separate Arcane headless agent has been live there since 2026-08-10 as the
Jarvis VMenvironment, beside the manager'sLocal Dockerenvironment0; its environment id is a UUID, not1. The agent mounts/var/run/docker.sock, giving the manager root-equivalent container control on VM 121. This is an accepted, deliberate trade-off for managing a genuinely separate docker host, not a reversal of the 2026-07-21 retirement of the redundant same-host agent. Exposure is limited to the LAN-only192.168.1.252:3553bind, with no Caddy route. The bind limits the interface, not the source, and Docker's NAT bypasses the VM's ufw, so a boot-persistent DOCKER-USER guard (arcane-agent-fwtimer on VM 121, installed fromstacks/management/arcane-agent/deploy/) restricts:3553to the CT 101 manager (192.168.1.241) only — verified live 2026-08-23 (#302). The agent is capped atmem_limit: 512m. Its image is distroless, so inspect it withdocker inspectanddocker logs, notdocker exec ... sh. The agent is a separate container; Hermes itself does not receive the docker socket. - The
jarvisuser retains passwordless sudo (NOPASSWD:ALL) and docker-group membership by design: the VM boundary plusqm stop 121is the accepted containment, so intra-VM privilege reduction was deliberately not pursued. The security audit's two "CRITICAL" findings (docker-group defeating systemd hardening; NOPASSWD sudoers) are accepted trade-offs, not open items. - Provider credentials are held in Hermes' local auth store, never in git.
Model authentication¶
The 2026-08-28 session recorded openai-codex/gpt-5.6-sol as the only
authenticated provider/model. Nous Portal, MiniMax, xAI and every other
provider were logged out, so no provider fallback was available. The full
provider configuration runbook is
hermes-vm/agent/MULTI_PROVIDER_SETUP.md.
Base default profile — homelab network engineer¶
Since 2026-08-09, Jarvis has run one profile: base default. Its gateway is
running. The retired neteng profile remains on disk but is stopped; its crons,
environment, SOUL.md, HOMELAB.md and workspace were ported to default,
with their model pins intact. The homelab network/SRE capabilities below
therefore remain live even though that profile is retired.
- Taught the homelab. The network-engineer knowledge pack - topology, host
inventory, Loki/PromQL cookbooks, deployment model and quirks - is embedded
in
MEMORY.mdand therefore injected with that file. The standalone~/.hermes/HOMELAB.mdis not auto-injected; see the memory-cap finding above. - Monitors. The
grafana-stackMCP gives it live Loki + Prometheus queries; theALERTSmetric is its source of truth for what is firing. - Fixes within a tight scope. Its one write capability is restarting existing containers through a scoped, revocable Arcane API token. Everything structural (Caddy, Proxmox, DNS, data) is proposed via Telegram and routed through the GitOps repo.
- Acts on the smart home — scoped (2026-08-01). Jarvis has API access to the
rebuilt Home Assistant (the HAOS VM, built but parked
since 2026-08-27 with
onboot=0; the old.240instance owns the route until the VM is started and setonboot=1at cutover): it can read any entity but act only on lights, switches and scenes (HA_URL/HA_TOKENlive in the base profile env). It is one leg of the future voice story — HA Assist with an AI conversation agent and/or the Jarvis Hermes HA skill. - Keeps the pane of glass honest. It annotates Grafana for every incident and action; the "Jarvis Ops" dashboard renders those as an incident timeline.
- Alerts two ways. A Grafana-native Telegram contact point fires straight from
Grafana (independent of the agent — the dumb-pipe safety net). The triage layer
that once sat on top —
neteng-health-sweep,neteng-daily-digestandneteng-weekly-review— is present but disabled (verified in the live cron list, 2026-08-12), and carries no model pin, so re-enabling one without pinning it would fail closed. Grafana's own alerting is what actually pages today. The jobs kept theirneteng-*names when ported, so the retired profile name still appears in logs; it does not identify a live profile. - Protects the data. Three data-protection cron jobs — all pinned to
openai-codex/gpt-5.6-sol(Hermes fail-closes an unpinned cron whose inference config has drifted — "#44585") — run under theSOUL.md"Reclaim and malware protocol", the only sanctioned auto-deletes:neteng-malware-sweep(daily 07:30) auto-removes fake video torrents (video category + an executable file extension + no real video file; capped at 5 per run). It prefers the Radarr/Sonarr queue delete withblocklist=true— the bad release is blocklisted and re-searched — and falls back to a direct qBittorrent delete only for orphans. Its first run removed three fakes (an Odyssey.exeand two House of the Dragon.scr).neteng-reclaim-scan(Mon 09:00) cross-references *arr imported history against qBittorrent and writesworkspace/reclaim-pending.tsv; a Telegram reply (reclaim delete/list/cancel) acts on it — nothing is deleted without that reply.neteng-backup-watchdog(daily 20:05 — moved from 06:00, when the wake-on-LAN PBS box is asleep) alerts if a guest's newest PBS snapshot is over 30 h old or missing, or the datastore passes 90%. It also tracks the age of Holly'shost/hollyappdata backup (uploaded daily at 20:35, so ~23.5 h old at the 20:05 check is healthy). An unreachable PBS is expected (it sleeps between backup windows) and stays silent.
The retired profile folder, source and deploy script are archived in
archive/neteng-profile/. See
hermes-vm/agent/README.md for the full write-up.