Documentation and repo-hygiene pass following the client/server split:
Plan drift (audited all other active plans against current code):
- oikos-gaps-and-improvements.md: mark Section C and D.5 resolved (both
described cmd/hermes, renamed to cmd/nomos with a real LLM loop since);
refresh ~10 stale file:line citations; fix tool-count (33, not 28).
- liveness-drift-and-ux-cohesion.md: fix stale default-model claim (now
deepseek-v4-pro since 2026-07-10) and "not yet deployed" status.
- nomos-agent-code-review.md: fix C1's citation (one unauthenticated route
to nomos now, not two, after the client/server split).
- wails-desktop-app.md: record the production deploy outcome.
Repo structure: added missing directories to README/CONTRIBUTING layout
tables (checks/, tools/, cmd/webhook/, docs/operations/), fixed a broken
link, added ADR 0015 documenting the auth/CORS/client-split model (there
wasn't one despite CONTRIBUTING's own process requiring it), normalized
ADR 0013/0014's format drift, added an Authentication section to
AGENTS.md/CLIENTS.md (every example call was missing the now-required
bearer header).
Retired the Goose+Nomos workstation flow (bootstrap.sh --with-nomos,
tools/setup-nomos-soul.sh, .agents/operations/nomos-agent.md) and the
Caveman auto-install tooling (tools/setup-caveman.sh, tools/caveman/) —
both superseded by the production containerized Nomos agent, which has
never used either. Kept .agents/shared/caveman.md itself (the terse
writing-style convention agents still follow by reading it).
Deleted the orphaned legacy Python oikos/ directory — nothing imports it,
and bin/homelab (the CLI it was kept for) no longer exists in the repo.
Rewrote .agents/operations/agent-enrollment.md (365 -> ~110 lines) and
commands.md to match the current architecture instead of the retired
`homelab` CLI; migrated the still-true networking prerequisites (Netbird,
split-horizon DNS, SSH key distribution) into the knowledge base as a
runbook via upsert_knowledge rather than duplicating them in markdown.
Updated all 10 .agents/skills/ runbooks referencing the dead CLI with
their real MCP tool / REST API equivalents, or flagged them as needing
verification where no equivalent is confirmed yet.
Two real bugs found and fixed, not just docs:
- The tools/setup-*.sh auto-setup glob was tools/*.setup.sh in THREE
places (tools/post-pull.sh, bootstrap.sh, and internal/httpapi/impl.go's
GetClientContext handler) since the mechanism's introduction on
2026-06-02 — never matched any real filename, so no client has ever
picked up an auto-setup script via git-pull or the context-poller sync.
Fixed all three; the Go server-side fix is the one that actually matters
since it's what the current context-poller mechanism depends on.
- bootstrap.sh removed dead vestigial --gitea-token/--gitea-user flags
(parsed, never consumed) left over from an earlier clone-based model.
Also flagged, not fixed (documented as an open gap in
client-enrollment/SKILL.md): bootstrap.sh tells a freshly-enrolled client
to call POST /api/v1/clients/{slug}/activate to finish enrollment, but
that route doesn't exist in api/openapi.yaml — EnrollClient sets entities
to provisioning and nothing currently transitions them to active.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
2026-07-08 — Oikos gaps, broken things, and improvements
Status: In Progress — audited 2026-07-11, re-audited 2026-07-12 for
drift from the cmd/hermes→cmd/nomos rename and later fixes. Done: A1
(approval FK bug), A3 (Hermes→Nomos help text), Section C (toy NLU /
silent-wrong-answer fallback — nomos now calls real listTools() and
routes unmatched queries to /chat instead of guessing, per
cmd/nomos/main.go:444-465), D.5 (SOUL.md/actuator architecture
mismatch — nomos/SOUL.md:21-22,40 now accurately documents SSH via the
run tool), D1 (upsert_knowledge), D4-partial (general run tool).
Still open: A2 (notifier flooding/dedup), A4 (resolveHost dead code), A5
(queryRows stringly-typed columns), A6 (stale get_state_snapshot
description), B1-B5 (enrollment auth, fake Infisical creds, /query
mesh-only auth unenforced, insecure host key checking, optional
caller_pubkey), D2/D3 (no get_approval_status/list_pending_approvals/
signal ack-resolve-mute tools), E-partial (Caddyfile placeholders still
present; tool count now 33, documented in AGENTS.md as of 2026-07-12).
2026-07-12 re-audit also refreshed every cmd/hermes→cmd/nomos and
internal/mcp/server.go line-number citation below (the file grew from 28
to 33 registered tools since 2026-07-11) — content/status of each finding
unchanged, only citations moved.
Goal
Full-project review of Oikos from two vantage points — a user interacting through Hermes, and an agent working through the MCP tool surface — with every finding verified against source (file:line), plus a prioritized fix order. This plan is the map; each numbered fix is small enough to land independently.
A. Confirmed bugs (verified in source)
A1. Approvals are never created — FK violation, errors swallowed (CRITICAL)
createApproval at internal/mcp/server.go:970 inserts a fresh
uuid.NewV7() as approvals.entity_id, but
migrations/003_operations.up.sql:41 declares
entity_id UUID PRIMARY KEY REFERENCES entities(id). The INSERT always
violates the FK, and both pool.Exec errors are discarded.
Net effect: request_execution for systemctl enable/disable or
apt_upgrade marks the execution pending_approval
(internal/mcp/server.go:311, :366) and tells the agent it's queued, but no
approval row exists → the notifier never sends a Matrix alert → the execution
is orphaned forever. From the Hermes user's perspective, config mutations
silently dead-end while appearing accepted.
Contrast: request_execution (server.go:286) correctly creates a companion
entities row for the execution first — approvals just never got the same
treatment.
Fix:
- Preferred: migrate
approvalsto its ownid UUID PRIMARY KEY(not FK'd toentities), keepingsubject_entity_idas the entity link. Update notifier andDecideApprovalqueries accordingly. - Alternative (no migration): create a companion
entitiesrow like executions do. - Either way: check and log every
Execerror increateApproval, and verify theUPDATE executions SET approval_id = $2 WHERE entity_id = $1column semantics (executionsis also keyed byentity_id).
A2. Matrix message-flooding vectors (internal/notifier/notifier.go)
- Initial alert is guarded by
alert_sent_at(notifier.go:103), but the guard is written after the Matrix send (notifier.go:111). If the UPDATE fails after a successful send, the ticker re-sends every cycle. - No dedup of approvals by
(subject_entity_id, action, payload). Once A1 is fixed, every retriedrequest_executionmints a new approval → one Matrix message each. pollReactions(notifier.go:118) issues one Matrix relations GET per pending approval per 30s poll, uncapped — ignored approvals accumulate for their 1h lifetime and multiply API calls. It also re-dispatches decisions for approvals stuckpending(no "already acted" guard if the DecideApproval call errors without flipping status).
Fix: mark-then-send (or transactional outbox) for alert_sent_at; upsert/
dedup open approvals on (subject_entity_id, action, payload); cap + backoff
on reaction polling; guard against re-dispatching a decision already in
flight.
A3. Hermes "help" is broken + dead code
cmd/hermes/main.go:165 handles "what can you do"/"help" by calling
client.callTool("tools/list", nil) — a tools/call for a tool literally
named tools/list, which doesn't exist. The correct listTools() helper
(main.go:318) is dead code, never called. Fix: wire listTools() in.
A4. resolveHost never returns a per-entity SSH user
internal/mcp/server.go:1222 (was :943 — line moved) — the named return
sshUser is always ""; the per-entity user branch is dead and everything
relies on sshExec's global default fallback. Fix: read the SSH user
from entity attributes or delete the dead return to make the behavior
honest.
A5. queryRows stringifies every column
internal/mcp/server.go:1090 (was :861 — line moved) renders all values via
fmt.Sprintf("%v", ...), so numbers, bools, timestamps, and JSON all reach
agents as strings. Fix: type-preserving serialization (pass through
pgx-native values into json.Marshal) — improves every read tool at once.
A6. get_state_snapshot description is stale
internal/mcp/server.go:863 (was :689 — line moved) still advertises "disk,
drift count" — columns removed in commit 3ea43ad. Fix: update the
description.
B. Security gaps
B1. Enrollment is unauthenticated, with a false comment
internal/httpapi/server.go:111 (was :97) says "unauthenticated (IP-gated
in handler)" but EnrollClient (internal/httpapi/impl.go:1166, was
:1099) performs no IP check at all — the only gate is the target entity
being in state planned/provisioning. Caddy's @enroll matcher bypasses
Authentik. Anyone reaching oikos.hubris.network who knows (or guesses) a
planned slug receives that node's age private key in the HTTP response
body. Still open — line numbers only, substance unchanged.
Fix: enforce a real gate (mesh-CIDR check, one-time enrollment token minted when the entity is created, or both), and stop returning the age private key in the response — have the client fetch it from the secret store.
B2. Fake Infisical credentials returned to enrollees
internal/httpapi/impl.go:1260-1261 (was :1191-1192) returns
"inf_client_"+uuid / "inf_secret_"+uuid — random strings wired to
nothing. Enrolled clients hold credentials that authenticate against
nothing. Still open — line numbers only, substance unchanged.
Fix: implement CreateMachineIdentity in internal/secrets/infisical.go,
or return no credentials and document the manual step.
B3. Nomos's /query has no auth
nomos/config.yaml:9 (was hermes/config.yaml:9) sets mesh_only: true
but cmd/nomos/main.go (was cmd/hermes/main.go) never reads or enforces
it — it serves any caller on :8092, who can invoke request_execution.
Still open, now also tracked as C1 in
2026-07-11-nomos-agent-code-review.md,
deferred by the operator. Fix: enforce mesh-CIDR (or bearer token) in
the handler; fail closed.
B4. SSH host keys not verified
ssh.InsecureIgnoreHostKey() at internal/mcp/server.go:1155 (was :920).
Still open — line number only, substance unchanged.
Fix: known_hosts pinning (keys are already inventory-managed per node).
B5. list_my_secrets enumerates all node pubkeys
Without caller_pubkey, internal/mcp/server.go:879-883 (was :709-720)
returns every entity that has an age_pubkey; nothing ties the caller to
what it may list. Still open — line numbers only, substance unchanged.
Fix: require caller_pubkey and scope results to the caller's
entitlements.
C. User perspective (interacting via Hermes) — RESOLVED
Resolved as of the Hermes→Nomos rewrite (verified 2026-07-12). This
entire section described cmd/hermes, which no longer exists — Hermes was
renamed and rebuilt as cmd/nomos, a real LLM-backed agent loop, which is
exactly the recommendation below. cmd/nomos/main.go:444-465 now calls the
real listTools() for "help"/"what can you do", and routes unmatched
queries to "natural language queries belong to /chat..." instead of
silently falling back to get_health_summary. Kept below for history —
original text unchanged.
routeQueryNLU is hardcodedstrings.Contains;extractEntity(cmd/hermes/main.go:173) recognizes only 5 services (authentik, caddy, vaultwarden, gitea, immich) plusmac-mini/hubris. Any other entity → "no entity found", and any unmatched query silently falls back toget_health_summary— wrong answers that look like answers.- No conversation/session context; no follow-up capability.
- Config mutations appear accepted but silently dead-end (A1).
Recommendation: either make Hermes a real LLM-backed agent loop (Claude
API driving the 28 MCP tools) or explicitly scope it as a structured-tool
gateway: remove the toy NLU, make the fallback say "I don't understand this
query; here are the tools" (via the fixed listTools()), and document that
natural language belongs to the calling agent, not the gateway.
D. Agent perspective (MCP tooling gaps)
28 tools are registered in internal/mcp/server.go (README says 15, AGENTS.md
says 21 — both stale). Missing capabilities:
- No knowledge write. AGENTS.md tells agents to register knowledge via
POST /api/v1/knowledge/{slug}, but there is no MCP tool — MCP-only agents cannot write back what they learn. Addupsert_knowledge. - No entity/signal mutation. Create/patch entity, state transitions, and
signal ack/resolve/mute all exist in REST (
internal/httpapi/impl.go,phase3.go) but not in MCP. Add at least signal ack/resolve/mute and a policy-gated entity attribute patch. - No approval visibility. After
request_executionreturnspending_approval, an agent has no way to check or reference the approval. Addget_approval_status/list_pending_approvals. - Execution actions limited to
restart | systemctl | pct_exec | apt_upgrade— no deploy/rollback/config-edit path. Partially superseded: the generalrunMCP tool (D4-partial, done) covers arbitrary commands now;request_execution's fixed enum is still there for the specific actions it names (see 2026-07-10-general-gated-execution.md). - RESOLVED (verified 2026-07-12). Architecture/doc mismatch:
hermes/SOUL.mdclaimed "no SSH access; all mutations flow through the actuator", but the MCP server ranrestart/pct_execsynchronously over SSH from inside the api process.nomos/SOUL.md:21-22,40now accurately documents SSH access via the policy-gatedruntool — matches the architecture the general-gated-execution plan built. No longer a mismatch.
E. Doc drift / housekeeping
- RESOLVED (verified 2026-07-12): Tool counts. README 15 / AGENTS.md 21 / actual 28 was already stale by 2026-07-11 (registered tools grew to 33) — AGENTS.md now documents all 33 with the full catalog (2026-07-12).
- Still open:
compose/caddy/Caddyfile.oikosretains literal<mac-mini-mesh-ip>placeholders (this repo's copy is a reference only — see 2026-07-12-wails-desktop-app.md's "Plan review" — the real config lives indtoro/caddy-conf). - RESOLVED:
.agents/HERMES.mdrenamed to.agents/NOMOS.md; the duplicate-line bug itself is still present at.agents/NOMOS.md:11— only the file citation was stale, the underlying nit is still open. - RESOLVED (verified 2026-07-12):
plans/index.mddrift — the broken link, TRMNL/Grimmory Active/Done mismatch, and missing.hermes/plans/entries described here are no longer present in the currentplans/index.md; already fixed sometime after this plan was written. - RESOLVED (verified 2026-07-12):
plans/2026-07-05-oikos-prometheus-lxc.mdalready self-corrected both the deleted-file references and the LXC 131 collision in its own 2026-07-08 changelog — this bullet describes a pre-fix state.
F. Prioritized fix order
- A1 approval FK + error handling — unblocks the entire approval → Matrix → execution path.
- A2 notifier flooding guards — this branch's namesake.
- B1/B2 enrollment security + B3 Hermes auth.
- D MCP tool additions — approval status first, then knowledge write, then signal ops.
- C Hermes routing honesty + A3-A6, B4/B5, E drift cleanup.