- 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
14 KiB
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;
runcorrectly 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 classifiedconfig_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+checksedge. - 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), thenrun find/grep/catacross/opt/homelab-context/**/*.goto reverse-engineer check creation. Readscheckdefaults.go,monitoring.go,default_checks.go,checks.go,coverage.go. - 20:42 — sets
service:haosmonitoring: ["http"]viaupdate_entity_attributes, hopingcheckdefaults.Ensure()auto-generates. It does not (see A2). - 20:42–20:50 — tries to reach the REST API directly:
psqlon hubris (cmd 127),docker execon hubris (docker absent),curl http://192.168.178.25:8090(wrong subnet; real net is192.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). Finalupdate_entity_attributesoningress:/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?)ininternal/mcp/tools.gothat reuseshttpapi.CreateEntity(impl.go:865) / the same store path — do not hand-roll. It must runensureDefaultChecks(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
typeagainstentity_types; reject unknown slugs/types with a clear error. Idempotent on existing slug (return the existing entity, mirroring the HTTPETag/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
ensureDefaultCheckswith the post-merge entity (same args asimpl.go:1280). This makes "set monitoring → checks regenerate" work via MCP, matching HTTP semantics. - Mind the
default_checks.go:14-19caveat: 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.gocurlIsReadOnly, treat-o /dev/null(and--output /dev/null) as read-only — it's a no-op sink. Keep-o <realpath>as mutation. AddTestClassifyCommand_CurlDevNull_ReadOnlynext to the existingTestClassifyCommand_CurlPipeSh_ConfigMutation. - Coach complement: in
nomos/SOUL.md, note that reachability probes should usecurl -Ior-o /dev/nullGETs (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.gowhereupdate_plan_stepis emitted), a step whose turn ended with only error/not-foundtool results must not auto-advance todone; leave itrunning/blockedand surface the failure to the operator. Minimal: if every tool call in the step returned anerror:*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 fromexecuting→failed(or a newstuck) 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 returnsentity … not foundon a create intent, the agent must stop and ask the operator (or now usecreate_entity) rather than pivot torun/SSH/API-bypass. Forbidden: inventing IPs/subnets; insteadget_entity("service:oikos")for the real API address.runtargets must behost:/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 theagent:nomosanddocument:infrastructure/monitoringentities) covering: check slug grammar,checkdefaults.Ensuretriggers (seed + HTTP create/patch, now also MCP), themonitoringper-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: preferlist_entities(limit)+get_entity_knowledgebulk calls over N+1get_entity/get_relationsfans; cap pre-plan exploration. Optional guardrail inagent.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): 40lxc, 26service, 1vm. 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.yamland 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 (curlwith benign flags, compound read-only commands) — friction compounds into approval stalls and stuck sessions. - No terminal/
stuckreaping (generalize A7): any turn that ends unresolved leaves the sessionexecutingforever; the UI never shows "done/failed".
5. Validation
- Task 1/2:
go test ./internal/mcp/... ./internal/httpapi/...— new test createscheck:http:service:haos:0viacreate_entity, asserts the entity exists and that acheck_defrow was derived; thenupdate_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/nomostest — a step whose only tool result iserror:*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 adoneoutcome.
6. Out of scope / open questions
- Whether
create_entityfor 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
stuckstatus vs reusefailed. - Whether to also expose a
delete_entity/retire_entityMCP tool (not needed for this case; lifecycle retirement is a separate flow).