plans: rename resident agent Hermes -> Nomos, add implementable rename phase N0
Nomos (from oikonomos, steward of the oikos) avoids the name collision with Nous Research's Hermes Agent. N0 enumerates the full rename scope: cmd/, hermes/ dir, env vars, config fields, compose service, Caddy vhost, identity-preserving DB slug migration + seed update, persona docs. History, the Matrix bot user, and legacy bin/hermes stay untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,12 +116,12 @@ Persistent nav + live top status strip (health dots, open-signal badge,
|
|||||||
pending-approval badge):
|
pending-approval badge):
|
||||||
|
|
||||||
0. **Agent chat (home view)** — *amendment 2026-07-08, see the
|
0. **Agent chat (home view)** — *amendment 2026-07-08, see the
|
||||||
[Hermes resident agent plan](2026-07-08-hermes-resident-agent.md)*: the
|
[Nomos resident agent plan](2026-07-08-nomos-resident-agent.md)*: the
|
||||||
default view at `/ui/#/` is a conversation with the resident Hermes agent
|
default view at `/ui/#/` is a conversation with the resident Nomos agent
|
||||||
(streamed via `/agent/chat`), with a live right rail showing pending
|
(formerly Hermes; streamed via `/agent/chat`), with a live right rail
|
||||||
approvals (decidable in place), recent events, and health. A persistent
|
showing pending approvals (decidable in place), recent events, and health.
|
||||||
chat drawer is reachable from every other page. Lands with resident-agent
|
A persistent chat drawer is reachable from every other page. Lands with
|
||||||
milestone H3; until then, Overview is the home view.
|
resident-agent milestone N3; until then, Overview is the home view.
|
||||||
1. **Overview** — summary cards from `/dashboard/summary`, event-rate
|
1. **Overview** — summary cards from `/dashboard/summary`, event-rate
|
||||||
sparkline, live event ticker, top degraded entities.
|
sparkline, live event ticker, top degraded entities.
|
||||||
2. **Graph explorer** — d3-force over `/graph`; node color = health; filters
|
2. **Graph explorer** — d3-force over `/graph`; node color = health; filters
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
# 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 decisions
|
|
||||||
|
|
||||||
### Hand-rolled tool loop, not a hosted connector or off-the-shelf agent
|
|
||||||
|
|
||||||
Three ways to get an LLM driving the tools; decision is (3):
|
|
||||||
|
|
||||||
1. **Hosted MCP connector** (e.g. Anthropic's `mcp_servers` param): zero
|
|
||||||
loop code, but the provider'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. **Off-the-shelf agent framework** (evaluated: Nous Research's
|
|
||||||
[Hermes Agent](https://hermes-agent.nousresearch.com/) — MIT, self-hostable,
|
|
||||||
multi-channel personal assistant with memory/subagents/browsing/code-exec).
|
|
||||||
Rejected for this role: it is a desktop/personal-assistant framework with
|
|
||||||
chat-platform frontends, not an embeddable service — there is no clean
|
|
||||||
`/chat` API for the control-room UI to stream from; the oikos-native
|
|
||||||
integration (sessions in Postgres, `agent_activity` + correlation-id joins,
|
|
||||||
approval rail) would not exist; and its browsing/code-exec surface is far
|
|
||||||
larger than a loop confined to the 28 MCP tools. Note it (or any MCP
|
|
||||||
client) remains usable as an *external* agent against the MCP endpoint —
|
|
||||||
that works today and is orthogonal to the resident agent.
|
|
||||||
3. **Hand-rolled agent loop** (~200 lines): Hermes calls the LLM API with
|
|
||||||
tool definitions translated from `tools/list`, executes returned tool
|
|
||||||
calls through its *existing* `mcpClient` inside the mesh, feeds results
|
|
||||||
back, repeats until the model answers in text. Only conversation text
|
|
||||||
leaves the mesh; the tool transport stays private; the agent is confined
|
|
||||||
to the MCP surface by construction.
|
|
||||||
|
|
||||||
### LLM provider: OpenRouter, default model DeepSeek V4 Flash
|
|
||||||
|
|
||||||
- **OpenRouter** (OpenAI-compatible Chat Completions API) rather than a
|
|
||||||
single-vendor SDK: use the official `openai-go` client with
|
|
||||||
`base_url=https://openrouter.ai/api/v1` and `OPENROUTER_API_KEY`. Model
|
|
||||||
choice becomes one env var; any OpenRouter model can be trialed.
|
|
||||||
- **Default model: `deepseek/deepseek-v4-flash`** — supports tool calling,
|
|
||||||
1M context, $0.09/M input / $0.18/M output. Use OpenRouter's **Exacto**
|
|
||||||
routing (highest measured tool-calling accuracy) since tool calls are this
|
|
||||||
agent's entire job; low temperature; strict system prompt.
|
|
||||||
- MCP tool `inputSchema` is already JSON Schema — exactly what
|
|
||||||
`tools[].function.parameters` expects — so translation is a field rename.
|
|
||||||
- **Privacy:** conversation text and tool results (hostnames, log excerpts)
|
|
||||||
transit OpenRouter and the upstream provider. Pin OpenRouter provider
|
|
||||||
preferences to ZDR / no-training providers in the request payload.
|
|
||||||
- A flash-class MoE will be weaker than frontier models on long multi-step
|
|
||||||
chains; mitigations are the iteration cap, Exacto routing, and bumping
|
|
||||||
`HERMES_MODEL` per-deployment when a task warrants it.
|
|
||||||
|
|
||||||
## 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 = chat.completions (OpenRouter, model, system, history, tools)
|
|
||||||
if resp has tool_calls:
|
|
||||||
for each: result = mcpClient.callTool(name, args) // existing code path
|
|
||||||
log to agent_activity (correlation_id = session turn id)
|
|
||||||
append assistant msg + tool-role result msgs to history
|
|
||||||
else: final text → stream to caller, persist turn
|
|
||||||
```
|
|
||||||
|
|
||||||
- Model: `deepseek/deepseek-v4-flash` default, `HERMES_MODEL` override.
|
|
||||||
`max_tokens` and iteration cap configurable; hard per-turn budget so a
|
|
||||||
pathological loop can't burn the API bill.
|
|
||||||
- `OPENROUTER_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
|
|
||||||
`OPENROUTER_API_KEY`, `HERMES_MODEL`, `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` + openai-go (OpenRouter base URL); `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 # openai-go (OpenRouter-compatible client)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
264
plans/2026-07-08-nomos-resident-agent.md
Normal file
264
plans/2026-07-08-nomos-resident-agent.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
# 2026-07-08 — Nomos resident agent (renames Hermes)
|
||||||
|
|
||||||
|
**Status:** Planned
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Rename the gateway formerly known as **Hermes** to **Nomos** (from
|
||||||
|
*oikonomos*, the steward of the oikos — and avoiding confusion with Nous
|
||||||
|
Research's unrelated "Hermes Agent" product), and turn it 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 the LLM.
|
||||||
|
- `.agents/HERMES.md` / `hermes/SOUL.md` — the persona, becomes the system
|
||||||
|
prompt (renamed in N0).
|
||||||
|
- `agent_activity` hypertable + `correlation_id` conventions — where tool
|
||||||
|
calls get logged so the control room can show the agent working.
|
||||||
|
|
||||||
|
## Milestone N0 — the rename (Hermes → Nomos)
|
||||||
|
|
||||||
|
Do this first, as its own commit, before any agent code lands. Scope is the
|
||||||
|
**live service identity**; history (`archive/`, `plans/done/`, past
|
||||||
|
investigations) is never rewritten.
|
||||||
|
|
||||||
|
**Code & build**
|
||||||
|
- `cmd/hermes/` → `cmd/nomos/` (binary `nomos`; keep `serve` subcommand).
|
||||||
|
- Env vars in main.go: `HERMES_MCP_URL`, `HERMES_LISTEN`,
|
||||||
|
`HERMES_AGENT_SLUG` → `NOMOS_*`. Default slug `agent:nomos`;
|
||||||
|
`clientInfo.name` → `nomos`.
|
||||||
|
- `internal/config/config.go`: `HermesAgentSlug` field +
|
||||||
|
`OIKOS_HERMES_AGENT_SLUG` → `NomosAgentSlug` / `OIKOS_NOMOS_AGENT_SLUG`;
|
||||||
|
`hermesAgentID` in `internal/httpapi/server.go:140-149` renamed to match.
|
||||||
|
- `hermes/` directory → `nomos/` (`SOUL.md`, `config.yaml`,
|
||||||
|
`skills/homelab-ops/`); `tools/setup-hermes-soul.sh` →
|
||||||
|
`tools/setup-nomos-soul.sh` with paths updated.
|
||||||
|
|
||||||
|
**Deploy**
|
||||||
|
- `compose/hermes/Dockerfile` → `compose/nomos/Dockerfile`; docker-compose
|
||||||
|
service `hermes:` → `nomos:` (docker-compose.yml:111, env at :63 and :121).
|
||||||
|
- Caddy: `hermes.hubris.network` vhost → `nomos.hubris.network`
|
||||||
|
(compose/caddy/Caddyfile.oikos:26); keep the old hostname as a `redir`
|
||||||
|
block for one transition window; add the DNS record. (These live in the
|
||||||
|
external caddy-conf repo / DNS — operator step.)
|
||||||
|
|
||||||
|
**Data (identity-preserving — do NOT create a new entity)**
|
||||||
|
- Live DB: `UPDATE entities SET slug='agent:nomos',
|
||||||
|
attributes = jsonb_set(attributes,'{name}','"nomos"') WHERE
|
||||||
|
slug='agent:hermes';` — relationships/audit reference the UUID, so history
|
||||||
|
survives the slug change. Ship as a migration so every environment gets it.
|
||||||
|
- `seeds/inventory.yaml:328` (`agent:hermes` entity) and `:541` (owns
|
||||||
|
relationship) → `agent:nomos`. Seed upserts by slug, so the migration must
|
||||||
|
run before seeding or the seed would mint a duplicate entity.
|
||||||
|
|
||||||
|
**Docs & persona**
|
||||||
|
- `.agents/HERMES.md` → `.agents/NOMOS.md`; update every referencing doc
|
||||||
|
(`AGENTS.md`, `CLIENTS.md`, `README.md`, `CONTRIBUTING.md`,
|
||||||
|
`.agents/operations/hermes-agent.md` → `nomos-agent.md`,
|
||||||
|
`.agents/operations/commands.md`, skills docs).
|
||||||
|
- ADR-0012 (`docs/adr/0012-hermes-oikos-interactions.md`): append a
|
||||||
|
"renamed to Nomos" note; don't rewrite the ADR.
|
||||||
|
|
||||||
|
**Explicitly out of scope**
|
||||||
|
- Matrix user `@hermes:hubris.network` (docker-compose.yml:103) — that's the
|
||||||
|
notifier's homeserver account; renaming it is an operational Matrix task,
|
||||||
|
optional and later.
|
||||||
|
- Legacy `bin/hermes` wrapper and `hermesd` on LXC 129 (referenced in
|
||||||
|
`.sops.yaml:89,110`) — separate legacy systems, untouched. Note: that
|
||||||
|
`.sops.yaml` entry shows an **OpenRouter API key path already exists** in
|
||||||
|
the secrets tree — reuse it for N1.
|
||||||
|
- Historical knowledge/investigation pages and executed plans.
|
||||||
|
|
||||||
|
**Verify N0:** `go build ./...`; `docker compose --profile full config`
|
||||||
|
resolves; `curl nomos.hubris.network/healthz` (and the old vhost redirects);
|
||||||
|
`SELECT slug FROM entities WHERE slug LIKE 'agent:%'` shows `agent:nomos`
|
||||||
|
with its original UUID; `grep -ri hermes` returns only history/ADR/legacy
|
||||||
|
hits.
|
||||||
|
|
||||||
|
## Architecture decisions
|
||||||
|
|
||||||
|
### Hand-rolled tool loop, not a hosted connector or off-the-shelf agent
|
||||||
|
|
||||||
|
Three ways to get an LLM driving the tools; decision is (3):
|
||||||
|
|
||||||
|
1. **Hosted MCP connector** (e.g. Anthropic's `mcp_servers` param): zero
|
||||||
|
loop code, but the provider'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. **Off-the-shelf agent framework** (evaluated: Nous Research's
|
||||||
|
[Hermes Agent](https://hermes-agent.nousresearch.com/) — MIT, self-hostable,
|
||||||
|
multi-channel personal assistant with memory/subagents/browsing/code-exec;
|
||||||
|
also the naming-collision motivation for N0). Rejected for this role: it
|
||||||
|
is a desktop/personal-assistant framework with chat-platform frontends,
|
||||||
|
not an embeddable service — there is no clean `/chat` API for the
|
||||||
|
control-room UI to stream from; the oikos-native integration (sessions in
|
||||||
|
Postgres, `agent_activity` + correlation-id joins, approval rail) would
|
||||||
|
not exist; and its browsing/code-exec surface is far larger than a loop
|
||||||
|
confined to the 28 MCP tools. It (or any MCP client) remains usable as an
|
||||||
|
*external* agent against the MCP endpoint — orthogonal to the resident.
|
||||||
|
3. **Hand-rolled agent loop** (~200 lines): Nomos calls the LLM API with
|
||||||
|
tool definitions translated from `tools/list`, executes returned tool
|
||||||
|
calls through its *existing* `mcpClient` inside the mesh, feeds results
|
||||||
|
back, repeats until the model answers in text. Only conversation text
|
||||||
|
leaves the mesh; the tool transport stays private; the agent is confined
|
||||||
|
to the MCP surface by construction.
|
||||||
|
|
||||||
|
### LLM provider: OpenRouter, default model DeepSeek V4 Flash
|
||||||
|
|
||||||
|
- **OpenRouter** (OpenAI-compatible Chat Completions API) rather than a
|
||||||
|
single-vendor SDK: use the official `openai-go` client with
|
||||||
|
`base_url=https://openrouter.ai/api/v1` and `OPENROUTER_API_KEY` (secret
|
||||||
|
path already exists in `.sops.yaml`). Model choice becomes one env var.
|
||||||
|
- **Default model: `deepseek/deepseek-v4-flash`** — supports tool calling,
|
||||||
|
1M context, $0.09/M input / $0.18/M output. Use OpenRouter's **Exacto**
|
||||||
|
routing (highest measured tool-calling accuracy) since tool calls are this
|
||||||
|
agent's entire job; low temperature; strict system prompt.
|
||||||
|
- MCP tool `inputSchema` is already JSON Schema — exactly what
|
||||||
|
`tools[].function.parameters` expects — so translation is a field rename.
|
||||||
|
- **Privacy:** conversation text and tool results (hostnames, log excerpts)
|
||||||
|
transit OpenRouter and the upstream provider. Pin OpenRouter provider
|
||||||
|
preferences to ZDR / no-training providers in the request payload.
|
||||||
|
- A flash-class MoE will be weaker than frontier models on long multi-step
|
||||||
|
chains; mitigations are the iteration cap, Exacto routing, and bumping
|
||||||
|
`NOMOS_MODEL` per-deployment when a task warrants it.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Agent loop (`cmd/nomos/agent.go`, new)
|
||||||
|
|
||||||
|
```
|
||||||
|
system prompt = SOUL.md content (mounted; provisioned by tools/setup-nomos-soul.sh)
|
||||||
|
tools = listToolsFull() // extend listTools() to return name, description, inputSchema
|
||||||
|
loop (max 15 iterations):
|
||||||
|
resp = chat.completions (OpenRouter, model, system, history, tools)
|
||||||
|
if resp has tool_calls:
|
||||||
|
for each: result = mcpClient.callTool(name, args) // existing code path
|
||||||
|
log to agent_activity (correlation_id = session turn id)
|
||||||
|
append assistant msg + tool-role result msgs to history
|
||||||
|
else: final text → stream to caller, persist turn
|
||||||
|
```
|
||||||
|
|
||||||
|
- Model: `deepseek/deepseek-v4-flash` default, `NOMOS_MODEL` override.
|
||||||
|
`max_tokens` and iteration cap configurable; hard per-turn budget so a
|
||||||
|
pathological loop can't burn the API bill.
|
||||||
|
- `OPENROUTER_API_KEY` from env / Infisical / SOPS (existing path).
|
||||||
|
- 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:nomos`.
|
||||||
|
**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/nomos/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 — Nomos 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: the `nomos` service (renamed in N0, `full` profile) gains
|
||||||
|
`OPENROUTER_API_KEY`, `NOMOS_MODEL`, `DATABASE_URL` env.
|
||||||
|
- Caddy: route `oikos.hubris.network/agent/*` → `nomos: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.
|
||||||
|
- Nomos enforces trusted-proxy headers (`X-Authentik-Username`) when
|
||||||
|
`NOMOS_TRUSTED_PROXY=true`, and finally implements the `mesh_only: true`
|
||||||
|
check that the config 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: Nomos 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
|
||||||
|
|
||||||
|
- **N0 — rename:** see above; standalone commit, deployable on its own.
|
||||||
|
- **N1 — loop:** `agent.go` + openai-go (OpenRouter base URL);
|
||||||
|
`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.
|
||||||
|
- **N2 — state + streaming:** sessions migration, DB persistence, SSE
|
||||||
|
streaming on `/chat`, `agent_activity` logging, budgets.
|
||||||
|
- **N3 — 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).
|
||||||
|
- **N4 (optional):** Matrix bridge, proactive sessions.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/nomos/{main.go, agent.go, store.go} # renamed + loop + sessions
|
||||||
|
internal/config/config.go, internal/httpapi/server.go # slug config rename
|
||||||
|
nomos/{SOUL.md, config.yaml, skills/} # renamed dir; drop query_routing
|
||||||
|
tools/setup-nomos-soul.sh
|
||||||
|
migrations/0xx_rename_agent_hermes_to_nomos.up.sql
|
||||||
|
migrations/0xx_agent_sessions.up.sql
|
||||||
|
seeds/inventory.yaml
|
||||||
|
compose/nomos/Dockerfile, docker-compose.yml, Caddyfile.oikos (/agent route, vhost)
|
||||||
|
.agents/NOMOS.md + referencing docs; docs/adr/0012 note
|
||||||
|
web/src/pages/Chat.svelte + lib/stores/chat.ts # control-room home view
|
||||||
|
go.mod # openai-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- N0: see milestone N0 verify list.
|
||||||
|
- N1: `curl -N /agent/chat` with a question requiring 2+ tools; confirm the
|
||||||
|
tool chain in the response and rows in `agent_activity`.
|
||||||
|
- N2: restart nomos mid-session, resume by session_id, history intact.
|
||||||
|
- N3: from the UI home, ask Nomos 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.
|
||||||
@@ -12,7 +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 | [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 | [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 | [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 |
|
| 2026-07-08 | [Nomos resident agent (renames Hermes)](2026-07-08-nomos-resident-agent.md) | Planned |
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user