# 2026-08-03 — Session review: `service:haos` monitoring + agent capability gaps **Status:** Plan (audit complete; ready to implement). **Reviewed session:** `23da10db-46a9-444c-bbde-ca9457bd9087` — *"Work out what monitoring checks service:haos should have and configure them."* **Method:** Direct Postgres read of `agent_sessions`/`agent_messages`/ `agent_activity`/`session_plan_steps` on the prod mac-mini (oikos prod runs here in docker compose project `oikos`; gateway `:8092`), cross-referenced with the code paths in `internal/mcp`, `internal/httpapi`, `internal/policy`, `internal/checkdefaults`, `internal/db/seed.go`. --- ## 1. Session audit (objective vs outcome) | Dimension | Finding | |---|---| | Objective | Determine + configure monitoring checks for `service:haos` (HAOS VM 108, `home.hubris.network`, `192.168.8.101:8123`). | | Outcome | ❌ **Failed/stuck.** `status=executing`, `outcome=null` ~4 min after last activity (UTC); never reached a terminal state. Only the *existing* `check:vm-status:vm:haos:0` stub got populated; the three **new** checks (`http:service`, `http:ingress`, `cert-expiry`) and their `ingress:`/`cert:` entities were never created. | | Tool calls | **116** (vs the >30 N+1 failure signature). ~45 redundant `list_entities`/`get_entity`/`get_relations`, then a ~15-min storm of `run` doing `find`/`grep`/`cat` on prod source. | | Plan | 8 steps proposed; steps 1–4 genuinely done; **step 5 falsely marked "done"** after both its tool calls errored `entity not found`; steps 6–8 never started. | | Operator friction | 3 manual interventions: `status`, `proceed`, *"why dony you use the mcp?"*; plus a **44-minute approval stall** (19:52→20:36) on two trivial reachability curls. | | Severity | **blocker** (capability gap) + **friction** (classifier, plan-state, reaping). | ### Timeline (UTC) - **19:45–19:50** — read-only exploration; `run` correctly blocked ("No plan… call set_goal then propose_plan"). Good guard. - **19:50** — `propose_plan` (8 steps). - **19:52** — two `curl … -o /dev/null -w '%{http_code}'` reachability probes → both classified `config_mutation` → one queued for approval (`019fc92e…`), second blocked ("approval already pending"). - **19:52 → 20:36 (44 min)** — idle, waiting on operator approval. - **20:36** — approval granted ("auto via assent window"); both curls → 200/200. - **20:37** — step 4 ✅: populated `check:vm-status:vm:haos:0` + `checks` edge. - **20:37:58** — step 5 ❌: `update_entity_attributes("check:http:service:haos:0")` → **`entity not found`**; `create_relationship` → **`source entity not found`**. *(There is no create tool.)* - **20:38–20:47** — spiral: `search_knowledge` (empty), then `run find/grep/cat` across `/opt/homelab-context/**/*.go` to reverse-engineer check creation. Reads `checkdefaults.go`, `monitoring.go`, `default_checks.go`, `checks.go`, `coverage.go`. - **20:42** — sets `service:haos` `monitoring: ["http"]` via `update_entity_attributes`, hoping `checkdefaults.Ensure()` auto-generates. **It does not** (see A2). - **20:42–20:50** — tries to reach the REST API directly: `psql` on hubris (cmd 127), `docker exec` on hubris (docker absent), `curl http://192.168.178.25:8090` (wrong subnet; real net is `192.168.8.x`; exit 7), `curl http://oikos-api:8090` (MCP routes to hubris which can't resolve the mac-mini docker alias; 30s timeouts ×2), `ssh root@192.168.178.25` (no route). Final `update_entity_attributes` on `ingress:`/`cert:` → `not found`. - **20:50:38** — last activity: a failed 30s `run`. Session goes silent, never terminates. --- ## 2. Root-cause findings (with code evidence) ### A1 — No entity-creation capability in the MCP toolset *(the blocker)* `internal/mcp/tools.go` registers **37 tools**; the only entity-mutation surface is `update_entity_attributes` (merge into an **existing** entity) and `create_relationship` (needs **existing** source+target). Neither can create a new entity. The capability **does** exist at the HTTP layer — `CreateEntity` (`internal/httpapi/impl.go:865`, `POST /api/v1/entities`) — it is simply not exposed to the agent. Every "set up / onboard / configure entity X" task that needs a new check/ingress/cert/service hits this wall. ### A2 — MCP `update_entity_attributes` bypasses `ensureDefaultChecks` `ensureDefaultChecks` (`internal/httpapi/default_checks.go:20`) is invoked **only** from the HTTP handlers: `CreateEntity` (`impl.go:1012`) and `PatchEntity` (`impl.go:1280`). `grep ensureDefaultChecks internal/mcp/` → **no matches**: the MCP tool writes attributes straight to the store, so flipping `service:haos` `monitoring:["http"]` never regenerated its checks. The agent's fallback strategy was structurally doomed via MCP. ### A3 — `-o /dev/null` curl idiom misclassified as `config_mutation` `internal/policy/command.go:106` `curlMutateRe` matches `(?:^|\s)-(?:d|F|T|o)\b` — so `-o` (output-file) is treated as mutation. The canonical read-only reachability probe `curl -sS -o /dev/null -w '%{http_code}' …` therefore escalates to approval. This is the entire 44-minute stall. (`curlIsReadOnly` at `command.go:307` only passes for GET with no `-o`/`-d`/`-X`/`>`.) A pure GET that discards the body is the single most common health probe and shouldn't need approval. ### A4 — No platform self-knowledge doc for the check lifecycle `search_knowledge("create check entity how to add new check monitoring")` → empty. The agent re-derived the whole mechanism from source on prod (~15 min, dozens of `run`). There is no agent/operator runbook explaining: check slugs are `check:::`; `check_defs` are derived from the type's `monitoring` spec by `checkdefaults.Ensure`; Ensure runs at **seed/deploy** and on **HTTP create/patch**, not via MCP. ### A5 — False plan progress (step marked done on failure) At 20:37:58 both tool calls for step 5 returned `error: entity not found`, yet the agent advanced step 5→`done`. Plan-state integrity hole: a step whose actions error should not transition to `done`. (`session_plan_steps` confirms seq 5 = `done`.) ### A6 — No "missing-capability" escalation; self-grounding failures On detecting the dead-end (no create tool) the agent never told the operator *"I lack a tool to create entities — please create them"*; instead it tried to bypass its own platform. Grounding errors: invented IP `192.168.178.25` (real LAN is `192.168.8.x`), ran `run` against `ws:mac-mini` ("unsupported target — must be host:/lxc:/vm:"), assumed `docker` exists on hubris, assumed the docker-alias `oikos-api` resolves from hubris. The agent didn't query `get_entity("service:oikos")` for the real address. ### A7 — Sessions never reap from `executing` Last activity 20:50; status still `executing` with no turn running. There is no idle-timeout / abandoned transition when a turn ends without resolution. (Fleet-wide: 176 done / 9 failed / 1 executing; the 9 prior failures are pre-v0.15.0, mostly approval-stalls and entity-not-found — same families.) ### A8 — N+1 tool fan-out (116 calls) Dozens of redundant `list_entities`/`get_entity`/`get_relations` before proposing a plan, plus the source-reading `run` storm. Above the >30-per-turn signature; indicates weak bulk-tool use and under-constrained exploration before planning. --- ## 3. Improvement plan (ordered) **Scope decision (confirmed with operator):** general `create_entity` MCP tool **+ wire regen** — solves this case and the 67-entity blast radius (§4). ### Task 1 — `create_entity` MCP tool *(fixes A1; the centerpiece)* - Register a new tool `create_entity(slug, type, name, attributes?)` in `internal/mcp/tools.go` that **reuses** `httpapi.CreateEntity` (`impl.go:865`) / the same store path — do not hand-roll. It must run `ensureDefaultChecks` (free, since it goes through the create path). - **Approval policy:** no approval required for the entity itself — it mutates the knowledge graph, matching the existing no-approval stance of `update_entity_attributes`/`create_relationship`/`upsert_knowledge`. (Derived checks are safe/read-side; if a check kind is ever deemed mutating, gate *that* in the scheduler, not here.) - Validate `type` against `entity_types`; reject unknown slugs/types with a clear error. Idempotent on existing slug (return the existing entity, mirroring the HTTP `ETag`/conflict behavior). - Expose to the agent via the tool-list build path used by `cmd/nomos/agent.go`. ### Task 2 — MCP `update_entity_attributes` triggers `ensureDefaultChecks` *(fixes A2)* - After the attribute merge in the MCP handler, call `ensureDefaultChecks` with the post-merge entity (same args as `impl.go:1280`). This makes "set monitoring → checks regenerate" work via MCP, matching HTTP semantics. - Mind the `default_checks.go:14-19` caveat: a service whose address comes from its host edge may still produce no checks until the hosting edge exists — log/return that as an explicit result so the agent knows to create the edge next. ### Task 3 — Classifier: read-only `curl` with `-o /dev/null` *(fixes A3)* - In `internal/policy/command.go` `curlIsReadOnly`, treat `-o /dev/null` (and `--output /dev/null`) as read-only — it's a no-op sink. Keep `-o ` as mutation. Add `TestClassifyCommand_CurlDevNull_ReadOnly` next to the existing `TestClassifyCommand_CurlPipeSh_ConfigMutation`. - Coach complement: in `nomos/SOUL.md`, note that reachability probes should use `curl -I` or `-o /dev/null` GETs (now read-only) rather than POSTs. ### Task 4 — Plan-state integrity: don't mark `done` on errored actions *(fixes A5)* - In `cmd/nomos` (`agent.go`/`tasks.go` where `update_plan_step` is emitted), a step whose turn ended with only error/`not-found` tool results must **not** auto-advance to `done`; leave it `running`/`blocked` and surface the failure to the operator. Minimal: if every tool call in the step returned an `error:*` result, hold the step. ### Task 5 — Stuck-session reaping *(fixes A7)* - Add an idle sweep (extend the existing continuation/idle worker in `cmd/nomos`) that transitions a session from `executing`→`failed` (or a new `stuck`) when no turn has run for N minutes and no approval is pending. Emit an event so the UI (F3 terminal handling) clears the spinner. Pick N (recommend 30 min) — confirm in review. ### Task 6 — Missing-capability escalation + grounding *(fixes A6)* - `nomos/SOUL.md`: when a mutation tool returns `entity … not found` on a create intent, the agent must **stop and ask the operator** (or now use `create_entity`) rather than pivot to `run`/SSH/API-bypass. Forbidden: inventing IPs/subnets; instead `get_entity("service:oikos")` for the real API address. `run` targets must be `host:/lxc:/vm:` slugs (state the contract explicitly). ### Task 7 — Runbook: "how checks work / how to add monitoring" *(fixes A4)* - Upsert a knowledge doc (via `upsert_knowledge`, linked to the `agent:nomos` and `document:infrastructure/monitoring` entities) covering: check slug grammar, `checkdefaults.Ensure` triggers (seed + HTTP create/patch, now also MCP), the `monitoring` per-entity override, the host-edge caveat, and the canonical way to add monitoring to an entity (create/patch entity → checks derive). ### Task 8 — (Lower priority) exploration budget / bulk-tool use *(A8)* - `nomos/SOUL.md`: prefer `list_entities(limit)` + `get_entity_knowledge` bulk calls over N+1 `get_entity`/`get_relations` fans; cap pre-plan exploration. Optional guardrail in `agent.go` (warn at >N same-tool calls per turn). ### Recommended sequence 1 → 2 → 3 → 4 → 7 → 5 → 6 → 8. (1+2 unblock the whole task class; 3 kills the approval stall; 4+5 fix state integrity; 7 is cheap leverage; 6+8 are persona hardening.) --- ## 4. Uncovered cases — the capability-gap blast radius The existing F1–F8 plans (`2026-08-03-nomos-chat-reliability-and-ux-audit.md`, shipped v0.15.0) and the turn-scheduler review cover **only** UI / streaming / turn serialization / connection UX. **None** addresses agent *capability* or MCP↔HTTP integration. This session exposes the uncovered class: - **67 entities currently have no `check:` relationship** (DB query): 40 `lxc`, 26 `service`, 1 `vm`. Any "add monitoring to X" task fails identically until Tasks 1+2. - **Whole task families blocked by the no-create gap:** onboarding a new host/LXC/VM, declaring a new service/ingress/cert/dns, adding any check that doesn't already exist, registering a relationship target that doesn't exist yet. All currently require an operator to hand-edit `seeds/inventory.yaml` and re-seed. - **MCP↔HTTP semantic drift (generalize A2):** audit other MCP mutation tools for side-effects that the HTTP handlers perform but the MCP path skips (check regen, drift-flagging, audit fields, idempotency). Each is a latent "agent did the right thing but nothing happened" bug. - **Classifier read-only false-positives (generalize A3):** beyond `-o /dev/null`, review other common read-only idioms that escalate (`curl` with benign flags, compound read-only commands) — friction compounds into approval stalls and stuck sessions. - **No terminal/`stuck` reaping (generalize A7):** any turn that ends unresolved leaves the session `executing` forever; the UI never shows "done/failed". --- ## 5. Validation - **Task 1/2:** `go test ./internal/mcp/... ./internal/httpapi/...` — new test creates `check:http:service:haos:0` via `create_entity`, asserts the entity exists **and** that a `check_def` row was derived; then `update_entity_attributes(service:haos, monitoring:["http"])` via MCP and assert checks regenerate (currently absent). - **Task 3:** `go test ./internal/policy/` — `curl -sS -o /dev/null -w '%{http_code}' URL` ⇒ `read_only`; `curl -o /tmp/x URL` ⇒ `config_mutation`. - **Task 4:** `cmd/nomos` test — a step whose only tool result is `error:*` stays non-`done`. - **Task 5:** idle-sweep test — session with no turn for N min and no pending approval ⇒ `failed` (+ event emitted). - **End-to-end re-run:** replay the haos goal against a local nomos; expect the three checks + `ingress:`/`cert:` entities created in <15 tool calls with **zero** approvals and a `done` outcome. ## 6. Out of scope / open questions - Whether `create_entity` for sensitive types (e.g. `secret`, `key`) should require approval even though it's graph-only — recommend: same no-approval stance now, add type-specific gating later if abused. - The exact stuck-reap window N (recommend 30 min) and whether to introduce a distinct `stuck` status vs reuse `failed`. - Whether to also expose a `delete_entity`/`retire_entity` MCP tool (not needed for this case; lifecycle retirement is a separate flow).