# ADR 0012 — Hermes/Oikos interaction architecture **Status:** Accepted **Date:** 2026-07-08 ## Context Hermes (the AI agent) is the primary operator interface for the hubris homelab. It communicates with Oikos via the MCP protocol. The MCP tools map to the OODA loop phases (Observe, Orient, Decide, Act). A thin client model distributes agent context via API deltas instead of git clones. ## Decision Hermes interacts with Oikos through three surface layers: MCP tools (agent-facing), REST API endpoints (operator-facing and agent-facing), and the SSH actuator (internal). All read paths go through the Postgres DB as the single source of truth. All writes go through the API with audit logging and policy classification. ## Hermes → Oikos interaction flow ```mermaid sequenceDiagram participant H as Hermes (AI Agent) participant MCP as MCP Server (:8090/mcp) participant API as REST API (:8090/api/v1) participant DB as Postgres (TimescaleDB) participant Act as Actuator participant PVE as Proxmox Hosts participant Matrix as Matrix Notifier Note over H,Matrix: ── OODA: Observe ── H->>MCP: get_entity("service:caddy") MCP->>DB: SELECT * FROM entities WHERE slug=$1 DB-->>MCP: {slug, type, state, health, attrs} MCP-->>H: entity record H->>MCP: get_state_snapshot() MCP->>DB: SELECT e.slug, st.health, st.disk_usage_pct FROM entities e LEFT JOIN entity_status st DB-->>MCP: [{slug, health, disk_pct, drift_count}, ...] MCP-->>H: fleet health snapshot H->>MCP: search_knowledge("jellyfin hardware acceleration") MCP->>DB: SELECT ... WHERE search @@ to_tsquery('jellyfin & hardware & acceleration') DB-->>MCP: [documents, runbooks] MCP-->>H: ranked FTS results H->>MCP: get_blast_radius("service:caddy") MCP->>DB: SELECT blast_radius($1, 3) -- recursive CTE DB-->>MCP: [{entity, depth}, ...] MCP-->>H: what breaks if caddy goes down Note over H,Matrix: ── OODA: Orient ── H->>MCP: explain("lxc:jellyfin") MCP->>DB: SELECT e.*, st.health, st.last_check FROM entities e LEFT JOIN entity_status st MCP->>DB: SELECT r.type, se.slug, te.slug FROM relationships r WHERE ... DB-->>MCP: compact context card MCP-->>H: {type, state, health, relations, version, updated_at} H->>MCP: preflight("lxc:jellyfin", "restart") MCP->>DB: SELECT risk_class, approval FROM classification_for($1, $2) DB-->>MCP: {risk_class: "reversible_low", approval: "auto-act"} MCP-->>H: safe to auto-act H->>MCP: preflight("lxc:jellyfin", "deploy") MCP->>DB: ... DB-->>MCP: {risk_class: "config_mutation", approval: "operator-approval"} MCP-->>H: needs operator approval Note over H,Matrix: ── OODA: Decide ── alt reversible_low (auto-act) H->>MCP: request_execution("lxc:caddy", "restart") MCP->>API: POST /executions {action:"restart", target:"lxc:caddy"} API->>DB: INSERT executions (auto_approved) API->>Act: queue execution Act->>PVE: ssh systemctl restart caddy Act->>DB: UPDATE execution status→completed MCP-->>H: execution {status: completed} else config_mutation (escalate) H->>MCP: request_execution("lxc:jellyfin", "deploy") MCP->>API: POST /executions API->>DB: INSERT executions (proposed, needs approval) API->>Matrix: send approval request via notifier Matrix->>Operator: "Approve deploy lxc:jellyfin? ✅/❌" Operator->>Matrix: ✅ Matrix->>API: POST /approvals/{id}/approve API->>DB: UPDATE execution status→approved API->>Act: queue execution Act->>PVE: run deploy procedure MCP-->>H: execution {status: completed} end Note over H,Matrix: ── OODA: Act (mutation gated) ── H->>MCP: tail_log("caddy", lines=200) MCP->>PVE: ssh journalctl -u caddy -n 200 PVE-->>MCP: log lines MCP-->>H: caddy logs H->>MCP: get_service_status("caddy") MCP->>PVE: ssh systemctl show caddy PVE-->>MCP: {ActiveState, SubState, ...} MCP-->>H: service status H->>MCP: list_lxcs() MCP->>DB: SELECT * FROM entity_status WHERE type='lxc' DB-->>MCP: [lxc:caddy, lxc:jellyfin, ...] MCP-->>H: all LXCs with state H->>MCP: get_lxc_state("lxc:caddy") MCP->>PVE: ssh pct status {vmid} --verbose PVE-->>MCP: RAM, CPU, disk, uptime MCP-->>H: LXC resource state ``` ## Thin client bootstrap flow ```mermaid sequenceDiagram participant New as New Client (bare machine) participant Gitea as Gitea (raw URL) participant API as Oikos API participant DB as Postgres New->>Gitea: curl bootstrap.sh Gitea-->>New: bootstrap.sh New->>Gitea: fetch CLIENTS.md, AGENTS.md, OIKOS.md, tools/ Gitea-->>New: agent orientation files New->>API: POST /clients/enroll {slug, hostname, mesh_ip} API->>DB: validate state, mesh IP API->>API: generate age keypair API->>DB: store pubkey, transition→provisioning API-->>New: {age_private_key, age_public_key, infisical_identity} New->>New: write /etc/age/key.txt, /etc/infisical/identity New->>New: install context-poller (launchd/systemd, every 5min) loop Every 5 minutes New->>API: GET /clients/ws:{hostname}/context?since={timestamp} API->>DB: SELECT files changed since {timestamp} API-->>New: {agent_files_changed, sops_config_changed, tools_changed} New->>Gitea: fetch only changed files end ``` ## Internal Oikos component interactions ```mermaid sequenceDiagram participant Sched as Scheduler participant API as API Server participant Notif as Notifier participant Act as Actuator participant DB as Postgres Note over Sched,DB: The OODA loop (internal) Sched->>DB: probe endpoints (HTTP, TCP, disk, cert-expiry) DB-->>Sched: results Sched->>DB: INSERT signals (dedup, flap suppression) Sched->>DB: UPDATE entity_status (health, disk, drift_count) Act->>DB: poll executions with status=auto_approved DB-->>Act: pending executions Act->>Act: circuit breaker check Act->>SSH: execute procedure Act->>DB: UPDATE execution status+result Notif->>DB: poll pending approvals DB-->>Notif: [approval requests] Notif->>Matrix: send approval messages API->>DB: INSERT audit_log (every mutation) API->>DB: NOTIFY oikos_events (SSE streaming) ``` ## Tool ownership matrix | Tool | Interface | Package | DB query | SSH | |------|-----------|---------|----------|-----| | `get_entity` | MCP | `internal/mcp/server.go` | DIRECT | — | | `list_entities` | MCP | `internal/mcp/server.go` | DIRECT | — | | `get_relations` | MCP | `internal/mcp/server.go` | DIRECT | — | | `get_blast_radius` | MCP + REST | both | CTE function | — | | `search_knowledge` | MCP | `internal/mcp/server.go` | FTS query | — | | `get_health_summary` | MCP | `internal/mcp/server.go` | DIRECT | — | | `whoami` | MCP | `internal/mcp/server.go` | DIRECT | — | | `explain` | MCP | `internal/mcp/server.go` | DIRECT + JOIN | — | | `preflight` | MCP | `internal/mcp/server.go` | CASE expression | — | | `get_change_history` | MCP | `internal/mcp/server.go` | audit_log query | — | | `get_state_snapshot` | MCP | `internal/mcp/server.go` | DIRECT + JOIN | — | | `list_my_secrets` | MCP | `internal/mcp/server.go` | attributes query | — | | `tail_log` | MCP | `internal/mcp/server.go` | — | journalctl | | `get_service_status` | MCP | `internal/mcp/server.go` | — | systemctl show | | `list_lxcs` | MCP | `internal/mcp/server.go` | entity_status query | — | | `get_lxc_state` | MCP | `internal/mcp/server.go` | relationship query | pct status | | `ping_service` | MCP | `internal/mcp/server.go` | — | probe | | `request_execution` | MCP | `internal/mcp/server.go` | executions INSERT | SSH via actuator | | `get_agent_activity` | MCP | `internal/mcp/server.go` | agent_activity query | — | | `get_signal_history` | MCP | `internal/mcp/server.go` | signals query | — | | `get_patterns` | MCP | `internal/mcp/server.go` | patterns query | — | | `get_skills` | MCP | `internal/mcp/server.go` | skills query | — | | `get_audit_trail` | MCP | `internal/mcp/server.go` | audit_log query | — | | `get_trend` | MCP | `internal/mcp/server.go` | metrics query | — | | `get_event_timeline` | MCP | `internal/mcp/server.go` | events query | — | | `query_metrics` | MCP | `internal/mcp/server.go` | metric_samples query | — | | `enroll` | REST | `internal/httpapi/impl.go` | entities+audit+events | — | | `context` | REST | `internal/httpapi/impl.go` | context_files query | — | | `secrets` | REST | `internal/httpapi/impl.go` | secrets.Manager.List | — | | `provision` | REST | `internal/httpapi/impl.go` | entities+steps+relations | SSH via actuator | ## Consequences - **Hermes is the primary operator interface.** All operator actions flow through Hermes → MCP → Oikos. The old `bin/homelab` CLI is dead. - **MCP tools are read-only by design.** Mutations go through `request_execution`, which is policy-gated and requires operator approval for `config_mutation` and `destructive` actions. - **The actuator holds the SSH key.** Hermes has no direct SSH access. The security boundary is Hermes → MCP → API → execution queue → actuator → SSH. - **Thin clients poll for context deltas.** No git clones on workstations. The 5-minute poll replaces `git pull` with HTTP queries to `GET /clients/{slug}/context`. - **The DB is the single source of truth.** All state transitions, audit entries, and event emissions go through Postgres. The scheduler, actuator, notifier, and API all read/write the same tables. --- **2026-07-08 — renamed to Nomos.** Nomos (from *oikonomos*, the steward of the oikos) under the [Nomos resident agent plan](../../plans/2026-07-08-nomos-resident-agent.md), ### Hermes MCP client setup To connect a Hermes Agent instance to oikos as a native MCP client, add to `~/.hermes/config.yaml`: ```yaml mcp_servers: oikos: url: "https://mcp.hubris.network/mcp" headers: Authorization: "Bearer " timeout: 180 ``` Run `/reload-mcp` in-session or restart Hermes. Tools appear as `mcp__oikos__*`. **Caveat:** Hermes stores the bearer token in plaintext in `config.yaml` — it does not support `${VAR}` interpolation in MCP server headers. Ensure `security.redact_secrets: true` (default) so the token value is stripped from tool output and logs. File an upstream feature request at https://github.com/NousResearch/hermes-agent/issues for env-var interpolation support. N0 milestone. The gateway binary (`cmd/nomos`), Docker service, DB slug (`agent:nomos`), and all referencing docs were updated. All architectural principles in this ADR remain unchanged.