Files
oikos/plans/2026-08-03-session-review-haos-monitoring-capability-gaps.md
dtoro 20adb89650
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
v0.18.0: MCP entity-graph CRUD, lifecycle validation, curl -o /dev/null fix
- create_entity, set_entity_state, end_relationship MCP tools
- update_entity_attributes now triggers check derivation via EnsureEntityChecks
- shared db.EnsureEntityChecks + db.ValidateTransition hooks (HTTP + MCP parity)
- curl -o /dev/null now classified read_only (was config_mutation)
- db.ErrTransitionInvalid sentinel for HTTP error-type accuracy
- SOUL.md: capability escalation, self-grounding, exploration budget rules
- Runbook: oikos check lifecycle for agent self-knowledge
2026-08-04 08:52:08 +02:00

14 KiB
Raw Permalink Blame History

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 14 genuinely done; step 5 falsely marked "done" after both its tool calls errored entity not found; steps 68 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:4519:50 — read-only exploration; run correctly blocked ("No plan… call set_goal then propose_plan"). Good guard.
  • 19:50propose_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_relationshipsource entity not found. (There is no create tool.)
  • 20:3820: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:4220: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:<kind>:<target>:<n>; 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 <realpath> 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 executingfailed (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).

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 F1F8 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}' URLread_only; curl -o /tmp/x URLconfig_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).