docs: reconcile plans/ status against actual code state
Audited all 10 active plan docs against the codebase (not just commit titles). 5 were fully shipped and stale-tagged "Planned"/"In Progress" — moved to done/ with verification notes. The other 4 got corrected Planned→In Progress status plus concrete remaining-gap notes so the next pass doesn't re-derive what's already done. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
266
plans/done/2026-07-08-nomos-resident-agent.md
Normal file
266
plans/done/2026-07-08-nomos-resident-agent.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 2026-07-08 — Nomos resident agent (renames Hermes)
|
||||
|
||||
**Status:** Done — 2026-07-11. N0-N3 (rename, agent loop, sessions/streaming,
|
||||
UI entry point) all verified in current code. N4 (Matrix bridge, proactive
|
||||
sessions) was explicitly out of scope and remains unstarted.
|
||||
|
||||
## 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.
|
||||
260
plans/done/2026-07-08-plan-implementation-audit.md
Normal file
260
plans/done/2026-07-08-plan-implementation-audit.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# 2026-07-08 — Plan vs implementation cross-reference
|
||||
|
||||
**Status:** Done — 2026-07-11. Every action this audit recommended has a
|
||||
corresponding follow-up commit (consolidation `7660e56`, client lifecycle
|
||||
`efa66c7`/`fcd9f23`/`28ab9b8`, comprehensive audit `43aaf2a`,
|
||||
DB-as-source-of-truth `a3ebd12`, MCP tool surface `7c6cffb`, apps/105 webhook
|
||||
cleanup `cefeba7`). Its own Prometheus finding (0% done) still matches the
|
||||
current state — see [2026-07-05-oikos-prometheus-lxc.md](2026-07-05-oikos-prometheus-lxc.md),
|
||||
still Planned.
|
||||
|
||||
## Goal
|
||||
|
||||
Snapshot each active plan against the actual codebase on disk. No action taken
|
||||
— this is the map from which the next round of work is drawn.
|
||||
|
||||
---
|
||||
|
||||
## 1. Consolidate Oikos on mac-mini (2026-07-06)
|
||||
|
||||
**Plan status:** Done (2026-07-08) — Code complete. Scripts, runbooks, safeguards in place.
|
||||
|
||||
**Cutover item status:**
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| Infisical bootstrap | **CODE COMPLETE.** `bootstrap-infisical.sh` (138 lines), Infisical Go backend, Docker service with Redis, `.env` configured, ADR-0010. `OIKOS_SECRET_BACKEND=infisical` set in `.env`. Needs operator to run bootstrap script on mac-mini. |
|
||||
| Watchdog | **DONE.** `scripts/watchdog.sh` rewritten: dual-path health check (LAN `192.168.8.175:8090` + mesh `100.122.0.10:8090`). Alerts only when BOTH paths fail. Partial failure (one path down) logged but not paged. External to Docker stack (runs on apps/105). |
|
||||
| Rollback drill | **DONE.** `scripts/rollback.sh` works (rehearsed 2026-07-07, recovered to SHA 7ac2521 with 20 tools). `docs/operations/rollback.md` runbook created. |
|
||||
| Rollback verify + re-deploy | **DONE.** Verify is in rollback script (30-attempt health check loop). Re-deploy via separate `deploy.sh` invocation. Runbook documents the full cycle. |
|
||||
| Deploy pre-dump | **DONE.** `deploy.sh` now runs `pg_dump` before every deploy → `/opt/oikos/backups/pre-deploy-<sha>.sql`. Rollback script recovers from this dump. |
|
||||
| apps/105 cleanup | **ALMOST DONE.** Gitea webhooks (ids 10, 11, 14) deleted 2026-07-08. LXC archival remains: `ssh hubris pct stop 105 && pct snapshot 105 archive-$(date +%Y%m%d)`. |
|
||||
|
||||
**Score: 98%** (code complete; 2 operational actions require operator on Proxmox/Gitea)
|
||||
|
||||
---
|
||||
|
||||
## 2. Oikos Prometheus LXC (2026-07-05)
|
||||
|
||||
**Plan status:** Planned
|
||||
|
||||
**Reality check:**
|
||||
|
||||
| Claim | Reality |
|
||||
|-------|---------|
|
||||
| No LXC exists | True |
|
||||
| "Extend oikos/scheduler.py" probes | **Stale.** `oikos/scheduler.py` was deleted. Plan references dead Python. |
|
||||
| "bin/homelab" CLI for provisioning | **Stale.** `bin/homelab` directory deleted. Go binary handles operations. |
|
||||
| Undocumented LXC 131 | **Unchanged.** Never investigated. |
|
||||
|
||||
**Score: 0%**
|
||||
|
||||
**Blockers:**
|
||||
- Plan needs rewrite to reference Go scheduler (`internal/scheduler/`) and `check_defs` table
|
||||
- LXC 131 mystery unresolved — may collide with Prometheus VMID
|
||||
|
||||
---
|
||||
|
||||
## 3. Client Lifecycle in Go (2026-07-07)
|
||||
|
||||
**Plan status:** Done (2026-07-08)
|
||||
|
||||
**Initial audit was incorrect — the API was already fully implemented.** Discovery:
|
||||
|
||||
| Phase | Status |
|
||||
|-------|--------|
|
||||
| Phase 1: enrollment API (`POST /api/v1/clients/enroll`) | **DONE.** impl.go:1091. Generates age keypair, stores pubkey in attrs, sets state→provisioning. |
|
||||
| Phase 1: `GET /api/v1/clients/{slug}/secrets` | **DONE.** impl.go:1245. Lists secrets scoped to client prefix from secretsManager. |
|
||||
| Phase 1: `GET /api/v1/clients/{slug}/context` | **DONE.** impl.go:1195. Returns context_version + changed file/tool/sops deltas. |
|
||||
| Phase 2: `POST /api/v1/entities/provision` | **DONE.** impl.go:1271. Creates entity in planned, validates slug uniqueness, inserts provisioning_steps, creates hosts relationship, emits audit+events. |
|
||||
| Phase 2: `GET /api/v1/entities/{slug}/provision/status` | **DONE.** impl.go:1380. Polls provisioning_steps table for step-by-step progress. |
|
||||
| Phase 2: lifecycle transitions (activate/deprecate/destroy/fail) | **DONE.** impl.go:933. PATCH /entities/{id} validates transitions against lifecycle_defs, rejects illegal transitions with 409. |
|
||||
| Phase 3: MCP tools (`whoami`, `explain`, `preflight`, etc.) | **DONE.** All 6 in mcp/server.go. |
|
||||
| Tests | **DONE.** `client_lifecycle_test.go`: 324 lines, full e2e: planned→enroll→provisioning→active→migrating→deprecated→failed. Provision rejection, relationship edges, blast radius verified. |
|
||||
|
||||
**Score: 100%** (12/12 verified; see below)
|
||||
|
||||
**12-point verification (2026-07-08):**
|
||||
|
||||
| # | Item | File:Line |
|
||||
|---|------|-----------|
|
||||
| 1 | POST /clients/enroll | `impl.go:1099` — age keypair, state→provisioning, audit |
|
||||
| 2 | GET /clients/{slug}/context | `impl.go:1203` — context_version + file/tool/sops deltas |
|
||||
| 3 | GET /clients/{slug}/secrets | `impl.go:1253` — scoped secret key listing |
|
||||
| 4 | POST /entities/provision | `impl.go:1279` — slug validation, provisioning_steps, hosts edge |
|
||||
| 5 | GET /entities/{slug}/provision/status | `impl.go:1388` — step-by-step polling |
|
||||
| 6 | Lifecycle transitions | `impl.go:933` — PATCH /entities/{id} with lifecycle_defs validation |
|
||||
| 7 | MCP tools (6) | `mcp/server.go:602-723` — whoami, explain, preflight, history, snapshot, secrets |
|
||||
| 8 | Precondition checks | `impl.go:1459-1551` + `ontology/validate.go:127-246` — dual impl |
|
||||
| 9 | bootstrap.sh thin-client | No git clone; curl-fetched files; API enroll; context poller install |
|
||||
| 10 | tools/context-poller.sh | 73 lines — polls /context, fetches deltas, re-runs setup scripts |
|
||||
| 11 | migration 012 | provisioning_steps, context_version, context_files, enrolled_at |
|
||||
| 12 | openapi.yaml endpoints | All 5 endpoints + 7 schemas defined; codegen in sync |
|
||||
|
||||
**Minor deviations from plan spec:**
|
||||
- `internal/db/queries/clients.sql` not created — queries are inline in impl.go (same DB operations)
|
||||
- `internal/secrets/infisical.go` lacks `CreateMachineIdentity` — enrollment uses synthetic IDs until Infisical is bootstrapped (consolidation plan #1)
|
||||
|
||||
**Transition precondition enforcement** (Phase 5):
|
||||
- Hard checks: `no-inbound-edges`, `backups-verified`, `secrets-revoked`, `ingress-dns-removed`, `age-key-enrolled-if-needed`, `mesh-joined-if-needed`, `health-check-answering`, `doc-page-complete`
|
||||
- Soft checks (operator intent): `inventory-entry`, `cancelled-note`, `preflight-passed`, `error-summary`, `replacement-live-or-role-retired`, `post-verify-passed`, `recovery-verified`, `written-off`, `ingress-live-if-public`, `doc-page-stub`
|
||||
- Parsed from `lifecycle_defs.transitions` JSONB `{requires: [...]}` at mutation time
|
||||
|
||||
---
|
||||
|
||||
## 4. Comprehensive Audit & Next Steps (2026-07-07)
|
||||
|
||||
**Plan status:** Done (2026-07-08)
|
||||
|
||||
**Reality check:**
|
||||
|
||||
| Audit item | Status |
|
||||
|-----------|--------|
|
||||
| Remove 9 superseded `oikos/*.py` files | **DONE.** All deleted. Only `gen-topology.py` + `gen_topology_lib.py` remain. |
|
||||
| `bin/homelab` audit/removal | **DONE.** `bin/` directory doesn't exist. |
|
||||
| `oikos/cards/` (45 files) audit/removal | **DONE.** Directory deleted. |
|
||||
| `.hermes/plans/` (7 files) → `archive/hermes-plans/` | **DONE.** All 7 files archived. |
|
||||
| TRMNL plan marked done in index | **DONE.** Already in Done table. |
|
||||
| Create wiki pages for seanime (133), romm (134) | **DONE.** Both documented in `seeds/knowledge.yaml`. Wiki is DB-native now. |
|
||||
| Update strong.md + hubris.md guest lists | **ARCHIVED.** Host pages are in `archive/knowledge/hosts/`. DB is source of truth. |
|
||||
| Regenerate topology.md | **ARCHIVED.** Topology lives in DB relationships + `seeds/inventory.yaml`. |
|
||||
| Prometheus plan — update Python → Go references | **DONE.** References updated to Go scheduler, check_defs, MCP request_execution. |
|
||||
| ADR-0011 (Go rewrite completion) | **COVERED.** ADR-0011 exists (client lifecycle). Consolidation plan (1,540 lines) is the authoritative record. |
|
||||
| Traefik reference audit | **VALID.** VPS still runs traefik for public termination. References in seeds are accurate. |
|
||||
| Infisical bootstrap | **PENDING.** Cross-plan item, belongs to consolidation plan (#1). |
|
||||
| Watchdog tested | **PENDING.** Cross-plan item, belongs to consolidation plan (#1). |
|
||||
| Rollback drill | **PENDING.** Cross-plan item, belongs to consolidation plan (#1). |
|
||||
| apps/105 cleanup | **PENDING.** Cross-plan item, belongs to consolidation plan (#1). |
|
||||
|
||||
**Score: 100%** (audit-specific items complete; remaining items owned by consolidation plan)
|
||||
|
||||
**4 operator decisions:** already resolved — `oikos/cards/` deleted, `bin/homelab` gone.
|
||||
Infisical + apps/105 decisions belong to consolidation plan.
|
||||
|
||||
---
|
||||
|
||||
## 5. DB as Source of Truth (2026-07-07)
|
||||
|
||||
**Plan status:** Done (2026-07-08)
|
||||
|
||||
**Reality check:**
|
||||
|
||||
| Phase | Status |
|
||||
|-------|--------|
|
||||
| Phase 1: `seeds/knowledge.yaml` seed format | **DONE.** 24 documents + 6 investigations + 3 runbooks. |
|
||||
| Phase 1: `content_hash` column (migration 010) | **DONE.** |
|
||||
| Phase 1: `search` tsvector column + GIN index (migration 011) | **DONE.** |
|
||||
| Phase 1: Knowledge ingestion logic (`internal/knowledge/seed.go`) | **DONE.** |
|
||||
| Phase 2: convert wiki → seeds, archive originals | **DONE.** `archive/knowledge/` contains all originals. `knowledge/` directory removed. |
|
||||
| Phase 3: `search_knowledge` with PostgreSQL FTS | **DONE.** Both MCP and HTTP use `ts_rank` + `ts_headline` + `plainto_tsquery`. |
|
||||
| Phase 3: `get_entity_knowledge` MCP tool | **DONE.** Walks relationships to return docs/investigations/runbooks linked to entity. |
|
||||
| Phase 3: `GET /api/v1/knowledge/search` (HTTP) | **DONE.** Full FTS with ranked results and snippets. |
|
||||
| Phase 3: `GET /api/v1/knowledge/{entitySlug}` (HTTP) | **DONE.** Aggregates documents, investigations, runbooks via relationship edges. |
|
||||
| Phase 4: agent conventions for knowledge cycle | **DONE.** AGENTS.md documents `search_knowledge` + `get_entity_knowledge`. Export round-trip via `oikos export`. |
|
||||
|
||||
**Score: 100%**
|
||||
|
||||
---
|
||||
|
||||
## 6. MCP Tool Completion / bin/homelab Migration (2026-07-07)
|
||||
|
||||
**Plan status:** Done (2026-07-08)
|
||||
|
||||
**Reality check:**
|
||||
|
||||
| Phase | Status |
|
||||
|-------|--------|
|
||||
| `tail_log` — journalctl via SSH | **DONE.** `internal/mcp/server.go:466-488` |
|
||||
| `get_service_status` — systemctl is-active/enabled | **DONE.** `internal/mcp/server.go:489-509` |
|
||||
| `ping_service` — HTTP reachability from entity_status | **DONE.** `internal/mcp/server.go:438-465` |
|
||||
| `list_lxcs` — all LXCs with ID/host/IP/state | **DONE.** `internal/mcp/server.go:425-437` |
|
||||
| `get_lxc_state` — pct status from Proxmox | **DONE.** `internal/mcp/server.go:511-562` |
|
||||
| `request_execution` routing: restart | **DONE.** Immediate execute via SSH. |
|
||||
| `request_execution` routing: systemctl (reload/restart) | **DONE.** Immediate; enable/disable gated as config_mutation. |
|
||||
| `request_execution` routing: pct_exec | **DONE.** Resolves Proxmox host via relationships. |
|
||||
| `request_execution` routing: apt_upgrade (audit/upgrade) | **DONE.** Audit immediate; upgrade gated as config_mutation. |
|
||||
| `get_execution_status` | **DONE.** `internal/mcp/server.go:339-365` |
|
||||
| Matrix approval escalation | **DONE.** Notifier sends Matrix messages with approval tokens. Stores `matrix_event_id`. Polls for ✅/❌ reactions via `/relations/{id}/m.annotation`. Calls DecideApproval internally on reaction detection. Token verification in DecideApproval endpoint. |
|
||||
| Delete `bin/homelab` | **DONE.** Directory gone. |
|
||||
| Delete `bin/oikos` | **DONE.** Directory gone. |
|
||||
| Update AGENTS.md | **DONE.** Full 21-tool surface documented. Stale `homelab` CLI references removed. |
|
||||
|
||||
**End-to-end approval flow:**
|
||||
```
|
||||
Hermes → request_execution (config_mutation) → creates approval record
|
||||
Notifier → generates HMAC token → sends Matrix message → stores event_id
|
||||
Operator → reacts ✅ on Matrix message
|
||||
Notifier → polls /relations/{eventId}/m.annotation → detects ✅
|
||||
Notifier → POST /api/v1/approvals/{id}/decision {decision:"approve"}
|
||||
DecideApproval → verifies token (if provided) → executes gated SSH command
|
||||
```
|
||||
|
||||
**Score: 100%**
|
||||
|
||||
---
|
||||
|
||||
## Summary matrix
|
||||
|
||||
| Plan | Score | Key blocker |
|
||||
|------|-------|-------------|
|
||||
| Consolidation | 98% | Code complete. 2 operator actions: apps/105 webhooks + LXC archive |
|
||||
| Prometheus LXC | 10% | Not provisioned; plan references updated to Go |
|
||||
| Client lifecycle | 100% | DONE — 12/12 verified |
|
||||
| Audit & next steps | 100% | DONE — all cleanup resolved |
|
||||
| DB as source of truth | 100% | DONE — wiki archived, FTS live |
|
||||
| MCP tool surface | 100% | DONE — Matrix approval loop + token verification wired |
|
||||
|
||||
---
|
||||
|
||||
## Drift catalog (index vs reality)
|
||||
|
||||
| Issue | Detail |
|
||||
|-------|--------|
|
||||
| TRMNL plan still in Active | `2026-06-24-trmnl-plugins-lxc.md` is in `done/` but `index.md` Active table hasn't been updated |
|
||||
| Grimmory plan internal status | File in `done/` but internal status header says `in-progress` |
|
||||
| `.hermes/plans/` directory | Missing from disk. 7 executed plans lost. Recoverable from git history. |
|
||||
| Prometheus plan stale refs | References `oikos/scheduler.py` (deleted) and `bin/homelab` (deleted) |
|
||||
| Consolidation cutover checklist | 5 items open per `scripts/cutover-checklist.md` |
|
||||
| Audit plan decisions | 4 operator decisions listed as outstanding (section 7) |
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-07-08 — plan 1 completed (code)
|
||||
Consolidation at 98%. Dual-path watchdog.sh, pre-deploy pg_dump in deploy.sh,
|
||||
rollback runbook created. Infisical bootstrap scripts + Go backend complete.
|
||||
Two operational items remain (apps/105 webhooks + LXC archive — operator on
|
||||
Proxmox/Gitea). All 5 cutover checklist items now resolved or documented.
|
||||
|
||||
### 2026-07-08 — plan 3 fully completed
|
||||
Client lifecycle at 100%. Transition precondition enforcement added: no-inbound-edges,
|
||||
backups-verified, secrets-revoked, ingress-dns-removed, age-key-enrolled, mesh-joined,
|
||||
health-check-answering, and doc-page-complete are checked before transitions. Soft
|
||||
preconditions (inventory-entry, cancelled-note, etc.) confirmed by operator intent.
|
||||
Thin-client distribution: bootstrap.sh already rewritten; standalone context-poller.sh
|
||||
created in tools/.
|
||||
|
||||
### 2026-07-08 — plan 4 completed
|
||||
Audit plan at 100%. All cleanup resolved: hermes plans archived to
|
||||
archive/hermes-plans/, TRMNL in Done, seanime/romm in seeds (no wiki pages
|
||||
needed), Prometheus plan references updated to Go. Remaining items (cutover,
|
||||
Infisical, watchdog, rollback, apps/105) belong to consolidation plan.
|
||||
|
||||
### 2026-07-08 — plan 5 completed
|
||||
DB as source of truth at 100%. Wiki files already archived to `archive/knowledge/`.
|
||||
`seeds/knowledge.yaml` has 24 docs + 6 investigations + 3 runbooks. HTTP knowledge
|
||||
endpoints already used full PostgreSQL FTS. MCP `search_knowledge` upgraded from
|
||||
ILIKE to `ts_rank`/`ts_headline`. MCP `get_entity_knowledge` tool added, walks
|
||||
relationship edges to return all docs/investigations/runbooks for an entity.
|
||||
|
||||
### 2026-07-08 — plan 6 completed
|
||||
MCP tool surface at 100%. Matrix approval webhook loop implemented: notifier
|
||||
sends Matrix messages, polls for ✅/❌ reactions via `/relations/{id}/m.annotation`,
|
||||
calls DecideApproval API internally. Token verification added to DecideApproval.
|
||||
AGENTS.md updated with full 21-tool surface and policy-gated mutation path.
|
||||
Migration 013 added `matrix_event_id` + `alert_sent_at` to approvals table.
|
||||
|
||||
### 2026-07-08 — initial audit
|
||||
Cross-referenced all 6 active plans against codebase on disk. Consolidation
|
||||
infrastructure is solid; client lifecycle and Prometheus are the gap.
|
||||
164
plans/done/2026-07-09-chat-sessions-improvements.md
Normal file
164
plans/done/2026-07-09-chat-sessions-improvements.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 2026-07-09 — Chat sessions: reliability, cost, and session-management fixes
|
||||
|
||||
**Status:** Done — 2026-07-11. All 5 findings fixed on `main`
|
||||
(`49c37fe fix: chat session reliability, cost, and hygiene`): empty/refusal
|
||||
retry guard in `agent.go`, bulk-tool guidance in `SOUL.md`, tool-result
|
||||
truncation in `store.go`, `get_state_snapshot` filtering, and session
|
||||
delete + generated titles.
|
||||
|
||||
## Goal
|
||||
|
||||
Fix concrete problems found by inspecting the *actual* production Nomos chat
|
||||
data (`agent_sessions`/`agent_messages` on the `oikos` Postgres, 24 sessions /
|
||||
52 messages as of 2026-07-09), not a code-review of the UI in the abstract.
|
||||
Findings below are backed by real rows, not hypotheticals.
|
||||
|
||||
## How this was investigated
|
||||
|
||||
Queried `oikos-postgres-1` directly (`docker exec oikos-postgres-1 psql -U oikos
|
||||
-d oikos`) since this runs on mac-mini, the same host as the production
|
||||
containers. Pulled session list, per-message content sizes, tool-call
|
||||
breakdowns, and cross-checked against `cmd/nomos/agent.go` to explain what was
|
||||
observed.
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. ~17% of turns come back completely empty, silently
|
||||
|
||||
4 of 24 sessions ("hi" ×3, "what services are healthy?") have an assistant
|
||||
message with `text=""` and zero tool calls — the model returned a blank
|
||||
completion. `cmd/nomos/agent.go:175-183` treats `len(msg.ToolCalls) == 0` as a
|
||||
normal final answer and emits `text: ""` + `done`. No `error` event fires, so
|
||||
[chat.ts](../web/src/lib/stores/chat.ts) never sets `$error`, and
|
||||
[Chat.svelte](../web/src/pages/Chat.svelte) renders a permanently-blank
|
||||
assistant bubble (the "…" typing dots only show while `$streaming` is true;
|
||||
once `done` fires they vanish, leaving nothing). The user has no idea the turn
|
||||
failed and no obvious way to retry — they have to notice the silence and
|
||||
retype.
|
||||
|
||||
**Fix:** in `agent.chat()`, if `msg.Content == "" && len(msg.ToolCalls) == 0`,
|
||||
treat it as a retryable failure: log it, retry once against the provider
|
||||
before giving up, and if still empty, emit a real `error` event instead of an
|
||||
empty `text`/`done` pair. On the frontend, surface a "Nomos didn't respond —
|
||||
retry?" affordance on empty assistant messages rather than a silent blank
|
||||
bubble.
|
||||
|
||||
### 2. A tool-heavy turn came back as a canned non-English refusal
|
||||
|
||||
The session "what are the termals of hubris?" ran 22 tool calls and then
|
||||
returned, verbatim: `关于这个问题,我没有相关信息,您可以尝试问我其它问题,我会尽力为您解答~`
|
||||
("I don't have relevant information on this, try asking me something else").
|
||||
This is after successfully gathering data via tools — the model discarded its
|
||||
own tool results and emitted a boilerplate deflection in the wrong language.
|
||||
`NOMOS_MODEL` is currently a DeepSeek flash-tier model on OpenRouter, which is
|
||||
consistent with this kind of degraded-tier fallback text leaking through.
|
||||
|
||||
**Fix:** add a response-quality guard in `agent.chat()` — if the final text
|
||||
doesn't match the conversation's language/looks like a canned refusal (simple
|
||||
heuristic: non-ASCII-majority reply to an ASCII-majority conversation, or
|
||||
matches a small denylist of known refusal boilerplate), treat it like the
|
||||
empty-response case (retry, then surface an error rather than showing it to
|
||||
the operator as a real answer). Separately, reconsider whether the flash-tier
|
||||
model is worth the latency/cost tradeoff given it's producing failures like
|
||||
this in a small sample — worth an eval pass against a couple of alternative
|
||||
OpenRouter models on the same 24 real prompts before deciding.
|
||||
|
||||
### 3. Simple fleet questions fan out into dozens of individual tool calls
|
||||
|
||||
"Are any of the proxmox hosts saturated?" (2 hosts) triggered **70 tool
|
||||
calls** in one turn, 21 of them individual `get_lxc_state` calls — one per LXC
|
||||
container — instead of using the already-available `list_lxcs()` bulk tool.
|
||||
Similar pattern in "What should be updated with high priority?" (68 calls) and
|
||||
"What needs updating?" (54 calls). Each `get_lxc_state` is a live `pct status`
|
||||
SSH round-trip to the Proxmox host, so this is 21 sequential SSH round trips
|
||||
to answer a question `list_lxcs()` already answers in one call. This is the
|
||||
direct cause of both slow responses and the huge persisted payloads in
|
||||
finding 4.
|
||||
|
||||
**Fix:** two angles, not mutually exclusive:
|
||||
- **Prompt-level**: tighten the Nomos system prompt (`nomos/SOUL.md`) to
|
||||
explicitly prefer bulk tools (`list_lxcs`, `get_state_snapshot`,
|
||||
`query_metrics`) over per-entity tools when the question is fleet-wide, and
|
||||
only fall back to `get_lxc_state`/`tail_log` for a specific named entity.
|
||||
- **Tool-level**: `get_lxc_state` already exists per-slug; consider whether
|
||||
`list_lxcs()`'s summary is actually sufficient for "saturated" (CPU/mem %
|
||||
per container) — if it's missing that field, that's *why* the model loops
|
||||
per-container, and the real fix is enriching `list_lxcs()` rather than
|
||||
prompting around the gap.
|
||||
|
||||
### 4. Tool results are persisted raw and unbounded, inflating messages to 100KB+
|
||||
|
||||
Message content sizes in `agent_messages.content` (JSONB) range up to
|
||||
**106KB** for a single assistant turn. Even a plain "hi" greeting produced a
|
||||
44KB message, because `get_state_snapshot()`'s full result — every entity in
|
||||
the DB, including ~15 `document:containers/*` rows that are all
|
||||
`state: <nil>, health: unknown` and contribute nothing — gets embedded
|
||||
verbatim in the `tool_calls[].result` field and stored as-is
|
||||
(`cmd/nomos/store.go:68-76` just JSON-inserts whatever the tool returned).
|
||||
This bloats the DB, and every time a session is opened via
|
||||
[loadSessionMessages](../web/src/lib/stores/chat.ts#L56) or the
|
||||
[SessionRail](../web/src/lib/components/SessionRail.svelte)/
|
||||
[Sessions](../web/src/pages/Sessions.svelte) page loads history, the browser
|
||||
downloads and parses all of it just to render a collapsed tool-call summary.
|
||||
|
||||
**Fix:**
|
||||
- Filter `get_state_snapshot()`'s result server-side (in the MCP tool, not
|
||||
the agent) to drop entities with no meaningful state/health signal, or add
|
||||
a `type` filter param the agent can pass.
|
||||
- In `store.saveMessage`, cap persisted tool-result size (e.g. truncate to a
|
||||
few KB with a `"...truncated, N bytes"` marker) — the full result already
|
||||
served its purpose informing that turn's answer; historical replay
|
||||
(`agent.chat()`'s history-replay loop at `agent.go:122-137`) doesn't need
|
||||
the full blob, just enough for the model to know what it already checked.
|
||||
|
||||
### 5. No session hygiene: duplicate/typo'd titles, no delete/archive
|
||||
|
||||
Session titles are the raw, unprocessed first user message
|
||||
(`store.createSession`), with no dedup, normalization, or cleanup. Real
|
||||
production titles include **6 sessions titled "hi"**, **2 titled "say ok"**,
|
||||
and typos preserved verbatim ("what are the **termals** of hubris?", "whats
|
||||
the **termans** of strong", "Are any of the **proxomox** hosts saturated?").
|
||||
Neither [Sessions.svelte](../web/src/pages/Sessions.svelte) nor
|
||||
[SessionRail.svelte](../web/src/lib/components/SessionRail.svelte) nor the
|
||||
`store`/API layer (`cmd/nomos/store.go`, `web/src/lib/api.ts`) has any delete
|
||||
or archive path — grepped the whole stack, confirmed absent. Throwaway test
|
||||
sessions accumulate forever with no way to clean them from the UI.
|
||||
|
||||
**Fix:**
|
||||
- Add `DELETE /sessions/{id}` to the nomos gateway + a matching store method
|
||||
and wire a delete affordance into `SessionRail`/`Sessions` (hover trash
|
||||
icon, confirm on click).
|
||||
- Generate titles from the assistant's actual answer once the turn completes
|
||||
(or a cheap follow-up summarization call) instead of the raw first message,
|
||||
so distinct "hi" sessions become distinguishable by what was actually
|
||||
discussed.
|
||||
|
||||
---
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. **Empty-response + refusal-leak guard** (`cmd/nomos/agent.go`) — highest
|
||||
user-visible impact, smallest change, no schema/API changes.
|
||||
2. **Bulk-tool prompting fix** (`nomos/SOUL.md`) — cheap, directly cuts
|
||||
latency and tool-call volume; re-run the same 24 real prompts against the
|
||||
updated prompt to confirm `get_lxc_state` fan-out drops.
|
||||
3. **Tool-result truncation on persist** (`cmd/nomos/store.go`) — bounds
|
||||
future DB growth; pair with a one-off cleanup pass on the 52 existing rows
|
||||
if the table needs to be shrunk immediately.
|
||||
4. **`get_state_snapshot` filtering** — coordinate with whichever MCP tool
|
||||
file defines it; verify with `jsonb_pretty` on a fresh "hi" session that
|
||||
payload drops well below the current ~44KB.
|
||||
5. **Session delete + title generation** — UI + gateway change, lowest risk,
|
||||
can ship independently of 1-4.
|
||||
|
||||
## Verification
|
||||
|
||||
- Re-run the same 24 real user prompts (recorded in this plan's investigation)
|
||||
against the patched agent; confirm zero empty/refusal-leak responses and
|
||||
`get_lxc_state`-style fan-out drops to O(hosts) not O(containers).
|
||||
- `docker exec oikos-postgres-1 psql -U oikos -d oikos -c "SELECT max(length(content::text)) FROM agent_messages;"`
|
||||
before/after — expect the ceiling to move from ~106KB to low single-digit KB.
|
||||
- Manually delete a test session via the new UI affordance, confirm it's gone
|
||||
from both `SessionRail` and the `agent_sessions` table.
|
||||
161
plans/done/2026-07-09-session-execution-and-ux-fixes.md
Normal file
161
plans/done/2026-07-09-session-execution-and-ux-fixes.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 2026-07-09 — Session execution, UX, and learning improvements
|
||||
|
||||
**Status:** Done — 2026-07-11. Hard blocker and all major items verified:
|
||||
`pct_create` wired into `request_execution`, `ToolCallGroup` collapse +
|
||||
live status, `InlineApproval` blast radius, `http_get` tool, `session-review`
|
||||
skill. Two minor secondary items not implemented: `list_lxcs` CPU/mem
|
||||
enrichment, and a dedicated `get_tools_summary` tool (SOUL.md has general
|
||||
bulk-tool guidance instead).
|
||||
|
||||
## Goal
|
||||
|
||||
Fix the hard blocker and UX issues found in the latest production Nomos session
|
||||
(`b9c5de7c-e0b5-424c-9149-9fd45b2e7011` — "deploy TypeType as an LXC on strong").
|
||||
The user asked Nomos to deploy an LXC; Nomos gathered data, proposed a plan, but
|
||||
at the final step ("run all yourself") said it *couldn't* — `request_execution`
|
||||
has no `pct_create` action. The user's objective was not achieved.
|
||||
|
||||
## Session analysis
|
||||
|
||||
10 messages (5 user / 5 assistant), 150 tool calls, zero LXC created.
|
||||
|
||||
| Msg | Role | Tool calls | Top tools | Summary |
|
||||
|-----|------|-----------|-----------|---------|
|
||||
| 0 | user | 0 | — | "deploy TypeType on strong as LXC" |
|
||||
| 1 | assistant | 40 | get_entity(20), search_knowledge(12) | Fleet scan, entity detail per host/LXC |
|
||||
| 2 | user | 0 | — | "it's github.com/Priveetee/TypeType, use tube.hubris.network" |
|
||||
| 3 | assistant | 2 | search_knowledge(2) | MCP has no web fetch tool → couldn't read GitHub |
|
||||
| 4 | user | 0 | — | "I think you can figure out those" |
|
||||
| 5 | assistant | 60 | search_knowledge(32), request_execution(14) | Built the plan, tried provisioning, failed silently |
|
||||
| 6 | user | 0 | — | "connect to strong media disk, no youtube login, proceed" |
|
||||
| 7 | assistant | 44 | search_knowledge(10), get_entity(10), request_execution(8) | Full deployment plan laid out |
|
||||
| 8 | user | 0 | — | "run all yourself" |
|
||||
| 9 | assistant | 4 | request_execution(4) | **"I can't run pct create — only pct_exec, systemctl, restart, apt_upgrade"** — failure |
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. HARD BLOCKER: `pct_create` missing from `request_execution`
|
||||
|
||||
`request_execution` (`internal/mcp/server.go:264-376`) supports `restart`,
|
||||
`systemctl`, `pct_exec`, `apt_upgrade`. The actuator has `ProvisionLXC()`
|
||||
(`internal/actuator/actuator.go:209`) that calls `pct create` via SSH, but it
|
||||
is only wired into the auto-act signal pipeline — Nomos has no way to invoke
|
||||
it through MCP.
|
||||
|
||||
Nomos's final response laid out the exact `pct create` command the operator
|
||||
needs to run manually on the Proxmox host. That's a dead end for the user, who
|
||||
expected the agent to execute from chat.
|
||||
|
||||
**Fix:**
|
||||
- Add `pct_create` action to `request_execution` handler.
|
||||
- Wire it to the existing `ProvisionLXC()` function.
|
||||
- Classification: `config_mutation` — requires operator approval. Once approved
|
||||
(from the Ops page or Matrix), the actuator picks it up and provisions the
|
||||
LXC with the step callback reporting progress.
|
||||
- Alternatively (for the "run from chat" expectation): add a chat-level approval
|
||||
flow — when Nomos proposes `request_execution` with `pct_create`, the frontend
|
||||
renders an inline "Approve" button in the chat bubble. Operator clicks → it runs.
|
||||
This is the UX the user described: "allow the agent to run things directly from
|
||||
the chat I'm in."
|
||||
|
||||
### 2. UI: ToolCallGroup expanded by default during streaming — no status animation
|
||||
|
||||
`ToolCallGroup.svelte` uses `<details open>` when `active=true`. During a
|
||||
streaming turn with 40+ tool calls, the collapsed group fills the viewport
|
||||
with raw JSON. The summary header shows only a static icon and "N tools" text.
|
||||
|
||||
**What happens now:**
|
||||
- Streaming starts → group opens and stays open → all tool results visible as raw JSON.
|
||||
- When streaming ends → auto-collapses. No animation.
|
||||
- Header shows `WrenchIcon` pulsing OR `CheckIcon` OR `XIcon` — but no live
|
||||
running count, no per-tool status in the collapsed summary bar.
|
||||
|
||||
**What should happen:**
|
||||
- Group starts **collapsed** by default. The summary header shows a live animated
|
||||
status: "⠋ Running get_lxc_state (caddy)… [2/40 done]" with the active tool
|
||||
name + a progress fraction, updating in real time.
|
||||
- When a tool completes, the header briefly reflects it ("✓ get_lxc_state (caddy)")
|
||||
before moving to the next.
|
||||
- Clicking the summary expands the group with a smooth animated open/close
|
||||
(replacing native `<details>` with bits-ui `Collapsible` + CSS transition).
|
||||
- On load from history (not streaming), always starts collapsed.
|
||||
|
||||
**Fix:**
|
||||
- Replace `<details open>` with bits-ui `Collapsible` component (already in
|
||||
`web/src/lib/components/ui/collapsible/`).
|
||||
- Add `animate-pulse` to the chevron icon during streaming (user sees motion).
|
||||
- Add a `statusText` derived that shows the in-progress tool name + count.
|
||||
- CSS animation: `Collapsible.Content` supports `forceMount` with transitions.
|
||||
|
||||
### 3. No web-fetch tool → Nomos can't read GitHub READMEs
|
||||
|
||||
Msg 3: Nomos needed to inspect `github.com/Priveetee/TypeType` to understand the
|
||||
stack. It used `search_knowledge` (DB FTS), which returned nothing because the
|
||||
repo isn't in the DB. The agent had no way to fetch external URLs.
|
||||
|
||||
The MCP has 27 tools (21 listed in AGENTS.md + 6 more added since), but none
|
||||
for HTTP/web fetching. Nomos can only query the DB or execute SSH commands on
|
||||
existing hosts.
|
||||
|
||||
This forced the human to provide context that should have been machine-read.
|
||||
|
||||
**Fix options (non-blocking):**
|
||||
- Add an `http_get` MCP tool that returns sanitized body text (strip scripts,
|
||||
truncate to 8KB). Rate-limited per-turn.
|
||||
- Or: add `web_fetch` as a first-class action in the MCP gateway itself, since
|
||||
the gateway container already makes outbound HTTP calls to OpenRouter.
|
||||
|
||||
### 4. Task: Bulk-tool awareness already in SOUL.md but not enough
|
||||
|
||||
The SOUL.md already says "prefer `list_lxcs` over `get_lxc_state` for fleet-wide"
|
||||
(line 27-28). But msg 1 still made 40 calls. The issue:
|
||||
|
||||
- `get_entity` was called 20 times (one per entity found by `list_entities`).
|
||||
`list_entities` already returns all entities; the agent wanted per-entity
|
||||
detail, which is redundant since `explain` or `get_state_snapshot` gives the
|
||||
same info in one call.
|
||||
|
||||
**Fix (already planned in `2026-07-09-chat-sessions-improvements.md` finding 3):**
|
||||
- Enrich `list_lxcs` with CPU/memory utilization so the model doesn't feel it
|
||||
needs `get_lxc_state` per container.
|
||||
- Add `get_tools_summary` to SOUL.md preamble that lists each tool's intended
|
||||
use and warns about N+1 call patterns.
|
||||
|
||||
### 5. Skill gaps
|
||||
|
||||
| Missing | Why | Where to add |
|
||||
|---------|-----|-------------|
|
||||
| `http_get` / `web_fetch` | Agent can't read external URLs | MCP tool in `internal/mcp/server.go` |
|
||||
| `session-review` skill | No way to learn from failed sessions | `.agents/skills/session-review/SKILL.md` |
|
||||
| `pct_create` action | Can't provision new LXCs from chat | `internal/mcp/server.go` + `internal/actuator/` |
|
||||
| `request_execution` approval from chat | Operator must switch to Ops page | Inline chat approval component |
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. **Add `pct_create` to `request_execution`** (`internal/mcp/server.go`) —
|
||||
hard blocker, needed for the session's objective.
|
||||
2. **ToolCallGroup: collapse by default + animated status header**
|
||||
(`web/src/lib/components/ToolCallGroup.svelte`) — immediate UX win, the user
|
||||
explicitly asked for this.
|
||||
3. **Inline chat approval for `request_execution`** — renders an "Approve"/"Deny"
|
||||
button inside the chat when an execution is queued for approval. Lets the
|
||||
operator approve from the same chat.
|
||||
4. **`session-review` local skill** — lives in `.agents/skills/`, loads when
|
||||
examining chat sessions for failure patterns.
|
||||
5. **`http_get` MCP tool** — non-blocking but addresses a real gap seen in this
|
||||
session.
|
||||
|
||||
## Verification
|
||||
|
||||
- Re-run the TypeType deploy prompt against the patched agent. Confirm:
|
||||
- Agent proposes plan as before (keep plan-proposal behavior).
|
||||
- When user says "run all", agent calls `request_execution(target=lxc:typetype,
|
||||
action=pct_create, params=<json>)`.
|
||||
- Approval fires → operator sees inline approval in chat → approves → LXC created.
|
||||
- ToolCallGroup: start a session that triggers 5+ tool calls. Confirm:
|
||||
- Group starts collapsed.
|
||||
- Summary header shows animated status: tool name + count updating in real time.
|
||||
- Clicking expands smoothly.
|
||||
- On session reload (history), stays collapsed.
|
||||
- Load `session-review` skill and ask it to analyze the TypeType failure session;
|
||||
confirm it identifies the missing action as the root cause.
|
||||
163
plans/done/2026-07-10-autonomous-plan-execution.md
Normal file
163
plans/done/2026-07-10-autonomous-plan-execution.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 2026-07-10 — Autonomous plan execution: close the observation gap
|
||||
|
||||
**Status:** Done — 2026-07-11. Full scope shipped, including the Option B
|
||||
stretch goal: atomic `pct_create` decomposition, robust assent-window
|
||||
open/extend, SOUL persist-through-errors + `maxIterations=40`, and
|
||||
event-driven auto-continuation (`cmd/nomos/continue.go`). Commits `233b5e4`,
|
||||
`d2f749d`, `657e1a8`, `d529688`, `84ecb6b`.
|
||||
|
||||
## The real problem (not the one we kept fixing)
|
||||
|
||||
Operator, verbatim: *"the agent seems to stop when it encounters the first error,
|
||||
it does not recover from it… my goal is that the agent can do anything once a
|
||||
plan has been approved."*
|
||||
|
||||
We have patched ~10 individual failure modes (DNS, gateway, sshExec timeout,
|
||||
substring bug, slug collisions, docker CLI, assent window…). Every one was real.
|
||||
None fixed the thing the operator keeps hitting, because they all fixed
|
||||
**individual commands** — and the problem is the **loop**, not the commands.
|
||||
|
||||
## Root cause: the agent never sees the result of the thing it started
|
||||
|
||||
The agent runs in discrete request→response turns. Provisioning executions are
|
||||
**asynchronous**: `request_execution(pct_create)` queues an execution, fires the
|
||||
real SSH work in a **goroutine** (`go executeApprovedViaAPI(...)`,
|
||||
[internal/mcp/server.go](../internal/mcp/server.go)), and returns
|
||||
*"provisioning now"* immediately. The multi-minute result lands in the DB
|
||||
**after the agent's turn has already ended.**
|
||||
|
||||
So the agent literally is not running when the error happens. It cannot react to
|
||||
a failure it never observes. The only way the result re-enters the agent's
|
||||
reasoning is if a human types "continue" to start a new turn — **the human is the
|
||||
event loop.** Read the failing session
|
||||
(`7c25edaa`, 18 messages): the operator typed "continue" / "continue?" / "??" /
|
||||
"proceed" **eight times**, each one just ticking the agent forward one async step.
|
||||
The agent *was* recovering (it correctly diagnosed the docker-CLI issue and
|
||||
proposed fixes) — it simply could not proceed one step without a human tick.
|
||||
|
||||
Two concrete asymmetries prove the diagnosis:
|
||||
|
||||
1. **`run` is synchronous, `pct_create` is async.** Inside an assent window, the
|
||||
general `run` tool executes the command inline and returns stdout/exit-status
|
||||
to the agent ([server.go](../internal/mcp/server.go) ~L514) — the agent *sees*
|
||||
the result and can continue. `pct_create` in the same window auto-approves and
|
||||
then `go`-routines the work — the agent sees nothing. The failure-prone path
|
||||
is the unobservable one.
|
||||
2. **`pct_create` is monolithic and all-or-nothing.** It does create + apt +
|
||||
docker + post_install + verify in one SSH call. Even if it were synchronous,
|
||||
the agent could only see "the whole thing failed at some point," not step 3 of
|
||||
6 — so it can't surgically fix step 3 and resume. Recovery *requires*
|
||||
intermediate observation.
|
||||
|
||||
Secondary (real but downstream): "continue" is **not** an assent word
|
||||
([cmd/nomos/assent.go](../cmd/nomos/assent.go)), so in that session the assent
|
||||
window never even opened — every step stayed gated, compounding the ticking.
|
||||
|
||||
## The reframe: Nomos should work like a coding agent
|
||||
|
||||
A coding agent (Claude Code) runs a command, **sees the output**, runs the next,
|
||||
fixes errors inline, all in one continuous session — it does not stop and ask a
|
||||
human to forward it after each command. That is exactly "do anything once the
|
||||
plan is approved." The homelab agent needs the same loop:
|
||||
|
||||
> approve the plan → agent runs step → **observes result** → runs next step / on
|
||||
> failure diagnoses + adapts + retries → … → verifies goal met → reports.
|
||||
|
||||
The machinery for this **already exists** in the `run` tool (synchronous,
|
||||
observable, auto-executing within an assent window). Provisioning just doesn't
|
||||
use it — it uses a black box. The fix is to make the whole system consistent
|
||||
with the model `run` already embodies.
|
||||
|
||||
## Target architecture
|
||||
|
||||
### 1. One observable primitive; retire the async black box
|
||||
|
||||
- Everything the agent does — including provisioning — is a sequence of
|
||||
**synchronous `run` calls** whose real output (stdout, stderr, exit code)
|
||||
returns inline. No goroutine hand-off for agent-initiated work.
|
||||
- **Decompose `pct_create`.** Keep a thin `pct_create` that only does the fast,
|
||||
atomic container creation (create + start + register), returning synchronously.
|
||||
Move package install / service setup / post_install / verify **out** into
|
||||
agent-driven `run` steps. Now the agent observes each step and can fix a
|
||||
failed one without redoing the container.
|
||||
- Net: the agent orchestrates `create → apt → install → configure → up → verify`,
|
||||
seeing each result, exactly like a human operator at a shell.
|
||||
|
||||
### 2. Approve the plan = an autonomy grant the agent executes to completion
|
||||
|
||||
- The assent/autonomy window already exists. Make it robust:
|
||||
- Opening it must not depend on a magic word list. "continue", "go", "do it",
|
||||
"proceed", clicking Approve, or approving the first queued step should all
|
||||
open/extend it. Safer: when the operator approves ANY step of a plan, treat
|
||||
that as opening the window for the rest of that plan.
|
||||
- Within the window: read-only + config_mutation `run` steps execute inline,
|
||||
no re-prompt. **Destructive still stops** for typed confirmation — but a
|
||||
destructive step *described in the approved plan* can be pre-authorized so
|
||||
the agent isn't blocked mid-flow on something already shown and approved.
|
||||
- The window is the scope boundary: "you may do what the plan needs on this
|
||||
target; you may not wander outside it."
|
||||
|
||||
### 3. The agent persists through errors (prompt + loop)
|
||||
|
||||
- SOUL: "You are the executor of the approved plan. Run it step by step,
|
||||
observing each result. **On failure, do not stop and hand back — diagnose
|
||||
(read logs / inspect state), form a hypothesis, fix it, and retry or take an
|
||||
alternative path.** Continue until the goal is verified working or you are
|
||||
genuinely blocked (you need information only the operator has, or a step
|
||||
exceeds the approved scope). Never end a turn with a half-finished plan just
|
||||
because one command failed."
|
||||
- `maxIterations` sized for a full provision-with-recovery (raise 25 → ~40) and
|
||||
count observation/read-only steps cheaply so recovery attempts aren't starved.
|
||||
|
||||
### 4. Long-running steps: keep the turn alive, or auto-continue
|
||||
|
||||
A synchronous `apt install` is ~1–2 min; a full stack up is longer. Options,
|
||||
in preference order:
|
||||
- **A (simplest, ship first):** synchronous `run` with the existing 10-min cap;
|
||||
the streaming turn stays open (the chat UI already holds the SSE). Emit
|
||||
progress events so the operator sees liveness (already built — elapsed timer).
|
||||
- **B (for very long ops):** event-driven auto-continuation — when an async
|
||||
execution tied to an active plan completes, a worker **re-invokes Nomos**
|
||||
automatically with the result (the system becomes the event loop, not the
|
||||
human). More plumbing; do only if A's long turns prove problematic.
|
||||
|
||||
## Why this is the root fix, not another patch
|
||||
|
||||
Every prior fix made an individual command more likely to succeed. This makes
|
||||
the agent able to **notice and respond when one doesn't** — which is the only
|
||||
thing that generalizes to "do anything," because "anything" always includes
|
||||
"the first thing didn't work." You cannot enumerate every failure mode of an
|
||||
unbounded action space; you can give the agent a loop that observes and adapts.
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. **Make provisioning observable**: decompose `pct_create` into a fast atomic
|
||||
create + agent-orchestrated `run` steps for install/config/verify. (Biggest
|
||||
single win — removes the async black box from the failure-prone path.)
|
||||
2. **Robust window open**: any approval / any forward-assent opens/extends it;
|
||||
pre-authorize plan-described destructive steps.
|
||||
3. **SOUL persist-through-errors** framing + `maxIterations` bump.
|
||||
4. Verify end-to-end (below). Only then consider **B** (auto-continuation).
|
||||
|
||||
## Verification
|
||||
|
||||
- Re-run the exact TypeType deploy. Expected: operator approves the plan **once**;
|
||||
the agent then creates the container, installs docker (recovering from the
|
||||
Debian docker.io-CLI gap on its own by falling back to get.docker.com), brings
|
||||
up the stack, hits a transient error (e.g. Docker Hub 500), **retries on its
|
||||
own**, verifies `:8082` responds, and reports success — **with zero additional
|
||||
"continue" ticks from the operator.**
|
||||
- Failure injection: point a step at a wrong path; confirm the agent reads the
|
||||
error, adapts, and continues rather than ending the turn.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Scope of an autonomy window**: per-plan, per-target, time-boxed (30 min now)?
|
||||
What exactly may the agent do inside it without asking again?
|
||||
- **Pre-authorized destructive steps**: allow a plan to include a named
|
||||
destructive step (e.g. "destroy the half-provisioned CT and redo") that the
|
||||
agent may execute during recovery without a fresh typed confirmation, since
|
||||
the plan approval covered it? Or always re-confirm destructive, accepting the
|
||||
interruption?
|
||||
- **A vs B**: is a single 5–10 min streaming turn acceptable, or do we need
|
||||
event-driven auto-continuation from the start?
|
||||
Reference in New Issue
Block a user