Files
oikos/plans/2026-07-08-hermes-resident-agent.md
dtoro 750f0e088d
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
plans: add Hermes resident agent plan, make agent chat the control-room home view
Hermes becomes an LLM-backed agent loop (hand-rolled tool loop over the
existing mcpClient, not the public MCP connector), with Postgres-backed
sessions, SSE chat streaming, and Authentik-gated /agent routing. The
control-room plan is amended to make the chat the main entry point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:30:42 +02:00

8.1 KiB

2026-07-08 — Hermes resident agent

Status: Planned

Goal

Turn Hermes from a keyword router into a resident LLM-backed agent: a long-running service you converse with, which reasons over the full 28-tool MCP surface, holds session context, and is surfaced as the main entry point of the control-room web UI (companion plan). Oikos already has the agentic substrate — policy classes, approval gating, agent_activity, an OODA loop; this gives it the conversational front half.

What exists vs what's added

Today cmd/hermes/main.go contains no LLM anywhere: routeQuery is strings.Contains over ~6 phrases, extractEntity knows 5 hardcoded services, and everything else silently falls back to get_health_summary. But the hard plumbing already exists and is reused as-is:

  • mcpClient (main.go:190) — working Streamable-HTTP MCP client (initialize → session → tools/call with SSE frame parsing).
  • listTools() (main.go:318) — currently dead code; extended (below) it becomes the bridge that feeds MCP tool schemas to Claude.
  • .agents/HERMES.md / hermes/SOUL.md — the persona, becomes the system prompt.
  • agent_activity hypertable + correlation_id conventions — where tool calls get logged so the control room can show the agent working.

Architecture decision: hand-rolled tool loop, not the MCP connector

Two ways to let Claude drive the tools:

  1. Anthropic MCP connector (mcp_servers param on the Messages API): zero loop code, but Anthropic's servers must reach the MCP endpoint over the public internet. mcp.hubris.network is currently exposed without auth — a hole the gaps plan says to close, not to build on. Keeping the MCP surface mesh-private is the right posture for a homelab control plane.
  2. Hand-rolled agent loop (~200 lines): Hermes calls the Messages API with tool definitions translated from tools/list, executes Claude's tool_use blocks through its existing mcpClient inside the mesh, feeds back tool_result blocks, repeats until Claude answers in text.

Decision: (2). MCP tool inputSchema is already JSON Schema — the exact format the Anthropic tools parameter expects — so translation is a field rename. Only the conversation text ever leaves the mesh; the tool transport stays private. Use the official anthropic-sdk-go.

Design

Agent loop (cmd/hermes/agent.go, new)

system prompt  = SOUL.md content (mounted; already provisioned by tools/setup-hermes-soul.sh)
tools          = listToolsFull()   // extend listTools() to return name, description, inputSchema
loop (max 15 iterations):
  resp = messages.New(model, system, history, tools)
  if resp has tool_use blocks:
      for each: result = mcpClient.callTool(name, args)   // existing code path
                log to agent_activity (correlation_id = session turn id)
      append assistant msg + tool_result user msg to history
  else: final text → stream to caller, persist turn
  • Model: claude-sonnet-5 default, HERMES_MODEL override. max_tokens and iteration cap configurable; hard per-turn budget so a pathological loop can't burn the API bill.
  • ANTHROPIC_API_KEY from env / Infisical (same secret path as other creds).
  • Mutations need no new guardrails: the agent's only write path is request_execution, which flows through the existing risk-class / approval machinery. The agent's actor identity is agent:hermes. Depends on gaps-plan bug A1 (approvals FK) — without that fix the agent's config mutations dead-end silently, which is worse when a conversational agent confidently reports "queued for approval".

HTTP surface (cmd/hermes/main.go)

  • POST /chat {session_id?, message}SSE stream of typed events: text (deltas), tool_use (name + args), tool_result (truncated), done (session_id, usage). The web UI renders tool calls as inline chips as they happen.
  • GET /sessions, GET /sessions/{id} — history for the UI.
  • /query kept for scripts/structured callers. The toy NLU is removed (per gaps plan §C): a query with no tool returns "natural language belongs to /chat" plus the tool list from the now-live listTools().
  • /healthz unchanged.

Sessions (Postgres, new migration)

agent_sessions (id, title, actor, created_at, last_active_at) and agent_messages (id, session_id, role, content JSONB, created_at) in the shared oikos DB — Hermes gains a DATABASE_URL (it's in the same compose stack). DB-backed rather than in-memory so conversations survive restarts and the control room can list/replay them. Tool invocations additionally go to agent_activity with the session's correlation_id, so the existing agent-activity page and the ops ledger join up with zero new query paths.

Connectivity & auth

  • Compose: hermes already runs under the full profile; add ANTHROPIC_API_KEY, DATABASE_URL env.
  • Caddy: route oikos.hubris.network/agent/*hermes:8092 behind the same Authentik forward_auth as the rest of the vhost, stripping the /agent prefix. The web UI then calls same-origin /agent/chat — no CORS, and EventSource/fetch-streaming work unmodified.
  • Hermes enforces trusted-proxy headers (X-Authentik-Username) when HERMES_TRUSTED_PROXY=true, and finally implements the mesh_only: true check that hermes/config.yaml:9 promises but main.go never enforces (gaps plan B3). Direct :8092 access stays mesh-only for agents/scripts.

Web UI entry point (amends the control-room plan)

The agent chat becomes the home view of the control room (/ui/#/):

  • Center: conversation pane (streamed text, expandable tool-call chips showing args/results, correlation-id links into the ops ledger).
  • Right rail: live context — pending approvals with approve/deny buttons, recent events, health strip. When the agent's request_execution needs approval, the approval card appears in the rail mid-conversation (via the approval.created SSE event) and can be decided without leaving chat. That's the whole product in one screen: ask → watch it act → approve → watch it complete.
  • A persistent chat drawer is available from every other page.
  • Session list in the nav; sessions resumable.

Later (explicitly out of scope for v1)

  • Matrix bridge: Hermes as a Matrix bot in the operator room, reusing the notifier's homeserver credentials — same /chat loop, different frontend.
  • Proactive mode: agent opens a session itself when a signal fires (escalation-with-context instead of a bare alert).

Milestones

  • H1 — loop: agent.go + anthropic-sdk-go; listToolsFull(); /chat (non-streaming JSON first); remove toy NLU, fix /query fallback + help. Verify by curl: multi-tool question ("what's degraded and what depends on it?") produces chained get_health_summaryget_blast_radius calls.
  • H2 — state + streaming: sessions migration, DB persistence, SSE streaming on /chat, agent_activity logging, budgets.
  • H3 — UI entry point: home chat view + context rail + drawer in the control-room SPA (needs control-room M1; rail approvals need M2's approval.created event and gaps-plan A1).
  • H4 (optional): Matrix bridge, proactive sessions.

Files

cmd/hermes/agent.go            # Claude loop (new)
cmd/hermes/main.go             # /chat, /sessions, NLU removal, mesh/proxy auth
cmd/hermes/store.go            # session persistence (new)
migrations/0xx_agent_sessions.up.sql
hermes/config.yaml             # model, budgets; drop query_routing block
compose/ (env), Caddyfile.oikos (/agent route)
web/src/pages/Chat.svelte + lib/stores/chat.ts   # control-room home view
go.mod                         # anthropic-sdk-go

Verification

  • H1: curl -N /agent/chat with a question requiring 2+ tools; confirm the tool chain in the response and rows in agent_activity.
  • H2: restart hermes mid-session, resume by session_id, history intact.
  • H3: from the UI home, ask Hermes to restart a low-risk service; watch the tool chips stream, the execution appear in the rail, and (for a gated action) the approval card arrive and be decidable in place.