From 750f0e088d54a1ffc52eca604cef36958d92e37c Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 8 Jul 2026 13:30:42 +0200 Subject: [PATCH] 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 --- plans/2026-07-08-control-room-webui.md | 7 + plans/2026-07-08-hermes-resident-agent.md | 167 ++++++++++++++++++++++ plans/index.md | 1 + 3 files changed, 175 insertions(+) create mode 100644 plans/2026-07-08-hermes-resident-agent.md diff --git a/plans/2026-07-08-control-room-webui.md b/plans/2026-07-08-control-room-webui.md index 9a42e67..384c546 100644 --- a/plans/2026-07-08-control-room-webui.md +++ b/plans/2026-07-08-control-room-webui.md @@ -115,6 +115,13 @@ New endpoints (spec-first in `api/openapi.yaml`, regen, then implement): Persistent nav + live top status strip (health dots, open-signal badge, pending-approval badge): +0. **Agent chat (home view)** — *amendment 2026-07-08, see the + [Hermes resident agent plan](2026-07-08-hermes-resident-agent.md)*: the + default view at `/ui/#/` is a conversation with the resident Hermes agent + (streamed via `/agent/chat`), with a live right rail showing pending + approvals (decidable in place), recent events, and health. A persistent + chat drawer is reachable from every other page. Lands with resident-agent + milestone H3; until then, Overview is the home view. 1. **Overview** — summary cards from `/dashboard/summary`, event-rate sparkline, live event ticker, top degraded entities. 2. **Graph explorer** — d3-force over `/graph`; node color = health; filters diff --git a/plans/2026-07-08-hermes-resident-agent.md b/plans/2026-07-08-hermes-resident-agent.md new file mode 100644 index 0000000..46ade6c --- /dev/null +++ b/plans/2026-07-08-hermes-resident-agent.md @@ -0,0 +1,167 @@ +# 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](2026-07-08-control-room-webui.md)). +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](2026-07-08-oikos-gaps-and-improvements.md) + 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_summary` → `get_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. diff --git a/plans/index.md b/plans/index.md index ab0ae14..3e1ffbf 100644 --- a/plans/index.md +++ b/plans/index.md @@ -12,6 +12,7 @@ went sideways, open an investigation. | 2026-07-08 | [Plan vs implementation cross-reference](2026-07-08-plan-implementation-audit.md) | Planned | | 2026-07-08 | [Oikos gaps, broken things, and improvements](2026-07-08-oikos-gaps-and-improvements.md) | Planned | | 2026-07-08 | [Control room web UI](2026-07-08-control-room-webui.md) | Planned | +| 2026-07-08 | [Hermes resident agent](2026-07-08-hermes-resident-agent.md) | Planned | ## Done