Commit Graph

141 Commits

Author SHA1 Message Date
5e10437fe3 Phase 4 (Performance) + Phase 6 (Infrastructure) completion
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
Phase 4 — Performance:
- F1: SSH DialPool with key-by-host pooling and 5min idle TTL
- F2: In-memory entity lookup cache (TTL 60s, HTTP resolveEntityID)
- F3: Trigram GIN indexes on entities.slug and entities.name (migration 031)
- F4: Partial index on executions(classification_id) for auto-act (migration 032)
- Added missing RunOutput and RunStreaming in actuator/ (E3 gap fill)

Phase 6 — Infrastructure:
- H1: Infisical image pinned to v0.99.1
- H2: execworker daemon — polls pending executions with per-execution
  advisory locks, recovers orphaned running executions, wired as
  docker-compose service
- H3: splitSQL hardened with block comment and string-literal support,
  6 new edge-case tests (11 total)
- H4: Scheduler acquires pg_try_advisory_lock(0x01c05e6) at startup
2026-08-08 23:46:43 +02:00
7236c46e5c 0.29.1 — review-fix round on E3: RunOutput, sshKeyPath fallback, RunStreaming consolidation, signer cache, stderr in errors
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
2026-08-08 23:01:10 +02:00
75c0848a6f 0.29.0 — code-quality refactor (plan E1–E5): file splits, sqlc migration, SSH unification, test coverage
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
E1: split monolithic files — cmd/nomos (main.go → server.go + mcp.go + workers.go),
    internal/mcp/tools.go → entity_tools/ops_tools/knowledge_tools/analysis_tools,
    internal/httpapi/impl.go → domain files (entities, events, signals, ontology,
    fleet_health, client_context, client_lifecycle, entity_mutations, query_audit).
E2: migrate raw pool.Exec queries to sqlc (entities/relationships queries + generated).
E3: unify SSH — consolidate crypto/ssh dial into actuator/client.go (+client_test).
E4/E5: add tests — db/lifecycle, checkdefaults/build, ontology/preconditions, policy/risk.
2026-08-08 22:47:06 +02:00
fa79c1ea25 0.28.0 — operational hardening (plan D1–D5): CI deploy gate, versioned images, rate limiting, resource limits, health probes
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
D1: deploy.sh CI gate — read-only SHA via git ls-remote, Gitea commit-status
    poll, portable mkdir deploy lock (macOS, no flock), TOCTOU guard, token
    passed via curl --config - (not argv), graceful misconfig tolerance.
D2: version-tagged images — OIKOS_VERSION=v$VERSION, keep-last-3 prune derived
    from 'docker compose config --images'; VERSION read after pull.
D3: per-IP rate limiting — new internal/httpapi/ratelimit.go (x/time/rate),
    rightmost-XFF, /healthz exempt, ctx-driven sweep; disabled by default.
D4: mem_limit/cpus on all 10 compose services.
D5: staleness-aware health probes — new internal/health package wired into
    scheduler (:8093) and notifier (:8094); nomos already had :8092.

Two /review passes hardened the deploy lock, TOCTOU guard, token hygiene,
and XFF handling.
2026-08-08 21:31:16 +02:00
cecfd8b0e4 0.27.4 — fix: StartRefreshLoop was blocking startup, wrap in goroutine
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
2026-08-08 21:07:51 +02:00
c9d506b0f8 0.27.0 — Infisical hardening: runtime refresh, audit logging, CLI verify/audit, startup verification, SSH host key verification, interface consolidation
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
2026-08-05 23:51:01 +02:00
e3449b24c1 feat: wire Infisical secret store into API server and MCP tools
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
- Wire secretsManager in NewHandler() — instantiate InfisicalBackend
  when OIKOS_INFISICAL_SITE_URL is set (previously always nil)
- Add get_secret, list_secrets, set_secret MCP tools with nil-backend
  graceful degradation
- Add oikos secret get|set|list CLI subcommands for Infisical
- Fix Set() bug: create-before-update so new keys are created;
  add Type: "shared" to Update so it finds the right secret;
  disable SDK cache so Get returns fresh data after Set
- Clean enrollment response: remove fake infisical_client_id/
  infisical_client_secret stubs, store age key in Infisical for real
2026-08-05 23:03:27 +02:00
38c472a118 0.26.0 — transport-aware classifier escalation + standalone-server monitoring override
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
- classifyAndGate: escalate read-only commands on lxc: targets that
  touch /opt/, /etc/, /var/lib/ to config_mutation. The classifier
  scores command text only, not the SSH transport layer — SSH-ing into
  a container to read config is riskier than pct exec from the host.
- ontology: standalone-server monitoring override from inherited
  [ping, resource, updates] to [http]. VPS-like machines may not be
  SSH/ICMP-reachable from the scheduler; HTTP is the LCD liveness
  signal. Entities with full SSH can override per-entity.
2026-08-05 16:31:41 +02:00
1d0197da69 0.25.1 — get_health_summary destroyed filter + create_entity footgun doc
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
- get_health_summary: filter out state=destroyed entities (was noise
  from 20+ destroyed test LXCs, deprecated services, etc.)
- create_entity: document the monitoring footgun in the tool description
  (creating a type=check entity does NOT wire a check_def; the correct
  path is update_entity_attributes with monitoring + url attributes)
2026-08-05 16:07:59 +02:00
0920c4cb6d 0.25.0 — DNS resolution check kind (KindDNS) + VPS monitoring fix
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
Adds a new 'dns' semantic monitoring kind that probes whether a DNS name
resolves. Uses net.LookupNS (NS records) with fallback to net.LookupHost
(A/AAAA). Supports an explicit server config for split-horizon resolution.

Changes:
- seeds/ontology.yaml: dns-zone monitoring: none → [dns] (was deferred
  since 2026-06 with a comment 'no dns checker exists yet')
- seeds/inventory.yaml: host:netbird-vps monitoring: [http] (was none;
  VPS was invisible for 7 days during the 2026-07-29 outage)
- internal/checkdefaults/defaults.go: add KindDNS, buildKind case for 'dns'
  that creates a check_def at 5-minute intervals
- internal/scheduler/scheduler.go: add checkDNS probe + wire in executeCheck

The DNS checker catches stale/unreachable zones (e.g. matrix.hubris.network
pointing to a dead VPS IP). The VPS HTTP check probes the public endpoint
every 60s, closing the 7-day monitoring gap.
2026-08-05 15:31:50 +02:00
1b9c761274 implements plan: agent execution safety — QEMU guest agent gate + health guard + policy docs
I — run pre-flights QEMU guest agent before queueing VM execution
  classifyAndGate now checks vm: targets for qemu_guest_agent attribute.
  If not_running/missing, returns immediate error instead of queuing forever.

II — policy.yaml: documented host-mutation classifier rule
  Added comment clarifying that host-level package/kernel mutations
  (apt-get install, dpkg, systemctl enable) always classify as
  config_mutation and thus need operator approval.

III — health attribute read-only in update_entity_attributes
  Strips scheduler-owned keys (health, last_check_at, last_check) from
  attribute updates with a clear message directing agents to
  get_health_summary / list_checks instead.

IV — Recorded discovered dependency edges
  vm:zimaos → depends-on → lxc:nfs-export (NFS /media/library mount)
  vm:zimaos → depends-on → host:strong (NFS /media/ludo-library mount)

Also updated the run tool description to mention both guardrails.
2026-08-05 15:25:14 +02:00
a126cfa710 0.24.0 — MCP tool improvements: type filter for get_relations, health filter for get_health_summary, live HTTP probe for ping_service
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
get_relations now accepts an optional 'types' (comma-separated) parameter
to filter relationship types — filters out the noisy exec/targets edges
that previously drowned useful host/provides edges.

get_health_summary now accepts an optional 'health' (comma-separated)
parameter to return only entities in specific health states (e.g.
'health=down,stale') instead of the full 100+ entity list.

ping_service now:
- Falls back to e.attributes->>'public_host' when 'url' is not set
  (covers LXCs that only have public_host in the graph)
- Performs a live HTTP HEAD probe against the resolved URL, returning
  the actual status code instead of just the scheduler's stale health
  state

Also: fixed matrix.hubris.network DNS record (was pointing to dead VPS),
pruned 6 dead graph edges, wired url attributes on 7 LXCs, added VPS
HTTP monitoring check, and resolved the 18k-occurrence unmonitored signal.

This session's audit is documented as
document:nomos/2026-08-05-dns-monitoring-improvements-for-strong-hosted-services.
2026-08-05 15:12:04 +02:00
0dd8c28815 feat: MCP ping tool, tightened descriptions, and Hermes client docs
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
- Add  MCP tool — lightweight connectivity check returning server
  identity, no DB hit (resolves agent connection-test friction)
- Tighten 6 tool descriptions (get_relations, get_health_summary,
  query_metrics, get_trend, get_event_timeline, ping) to be searchable
  in the first 8-12 words
- Document Hermes MCP client setup in ADR-0012 with token security caveat
- Move completed plan to plans/done/
2026-08-05 00:21:56 +02:00
4e294b3630 0.22.0 — full MCP agent surface: 19 new tools, async run, session reliability
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
Stage 1 — Foundation:
- Target validation: iptables + systemctl/docker target-type gates
- Async run for long-running commands (sleep/wait/poll loops)
- audit_log.session_id plumbing (SQL, sqlcgen, 18 call sites)

Stage 2 — External agent observe (11 tools):
- get_dashboard_summary, get_ontology, list_checks, list_executions
- get_knowledge_revisions, get_knowledge_duplicates, get_knowledge_orphans
- list_knowledge_tags, list_entity_sessions, find_entities_by
- 3 resource templates: oikos://entity/{slug}, knowledge/{id}, execution/{id}

Stage 3 — Nomos reliability:
- complete_task(success) refused without verification (upgraded from warn)
- sessionHasPlan excludes replaced steps (forces propose_plan after reopen)
- Bash syntax validation in run() (rejects literal \n, flag-space typos)
- Scope gate in SOUL.md (ask before pivoting to unrelated subsystem)

Stage 4 — External agent act (9 mutation tools):
- ack_signal, resolve_signal, mute_signal, cancel_execution
- update_check, delete_knowledge, restore_knowledge
- merge_knowledge, rename_knowledge_tag
2026-08-04 23:51:55 +02:00
c3f478b8f8 v0.21.0: agent reliability overhaul — plan integrity, target validation, observability pipelines, learning loop
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
P0 — stop the bleeding:
- prevent premature complete_task(success) when goal involves reachability
- validate run targets: block host-only commands (qm/pct/pvesh) on LXC/VM
- bump MCP client timeout 30s→120s to stop 'context deadline exceeded'

P1 — fix the plan system:
- add replaced_reason column to session_plan_steps (migration 030)
- track WHY steps are replaced (wrong_diagnosis/scope_change/superseded/etc)
- force fresh propose_plan on session resume (reopenSession marks old plan)

P2 — cognitive guardrails:
- SOUL.md scope-gate rule: ask before chasing unrelated subsystems
- auto-upsert knowledge entry on every session close

P3 — observability (all were empty/NULL):
- populate agent_activity.token_count from LLM usage (was always NULL)
- populate nomos_plan_executions linking executions to sessions
- write plan_completion_rate metric on task close

P4 — learning loop (all were empty/NULL):
- auto-classify every run call → classifications table (was 0 rows)
- auto-feedback on session close (was 0 rows)
2026-08-04 23:15:47 +02:00
20adb89650 v0.18.0: MCP entity-graph CRUD, lifecycle validation, curl -o /dev/null fix
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
- 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
c9a00a9532 feat(checks): per-entity monitoring override; service:haos opts out
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
A `monitoring` attribute on an entity now overrides its type's declaration:
"none" opts out, a list overrides the kinds. service:haos uses it to opt out —
haos blocks SSH (no process probe can reach it) and the VM is already covered
by vm:haos's vm-status check, so the redundant process check only ever reported
false-down. vm:haos -> service:haos via provides confirms the coverage.
2026-07-29 23:47:43 +02:00
eb16796bf0 feat(checks): vm-status probe + matrix cert dial-by-name
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
VMs declared monitoring [ping], but many block ICMP and lack a guest agent
(haos), so ping was the wrong probe — a powered-on VM reported "down". Add a
vm-status check: `qm status <pve_id>` on the VM's Proxmox host, which tests
"powered on" without needing the VM's network at all. vm type monitoring is
now [vm-status].

matrix.hubris.network is a public hostname (federation) resolving to
netbird-vps, not served by the lab Caddy — so its cert-expiry check's
dial=caddy IP failed. Drop the dial for matrix; it dials by name (DNS ->
public) like wget already proved works.
2026-07-29 23:15:36 +02:00
a3914a1d41 fix(checks): process check is opt-in for url-fronted services
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
The ontology's stated intent was "http when it has a url, else a process
check", but the implementation emitted BOTH for every url-service — so ~17
fronted services carried a redundant process check that, under worst-of
aggregation, let a fragile supplementary probe (wrong unit name, unreachable
host, no guest agent) veto two healthy http checks and report the service
"down" while it was up (authentik, zimaos, house, matrix, ...).

buildKind now emits a process check only for services WITHOUT a url, or when
an explicit probe_unit opts into binary-level depth. http is the canonical
service-liveness probe (tests the real endpoint through the TLS terminator);
the redundant process checks were removed.
2026-07-29 23:03:27 +02:00
e4104eb344 fix(checks): process_check matches docker containers + prefixed systemd units
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
process_check.sh ran `systemctl is-active <entity-name>`, but a service's name
is a logical label, not its unit/container name — matrix is matrix-synapse.service
+ element-web/mautrix-* containers, authentik is authentik-server/-worker
containers. So every multi-component or docker service reported "inactive"
while up (authentik, matrix, photos, house, arr-stack, …).

Resolve in order: exact systemd unit, a unit with the name as prefix
(matrix -> matrix-synapse.service), or a running docker container whose name
contains it. checkdefaults passes a declared probe_unit/systemd_unit/container
attribute when set, for precision.
2026-07-29 22:33:02 +02:00
0929c17cbb feat(mcp): discover_infra_drift — live Proxmox vs DB guest reconciliation
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
The DB-only audit_knowledge_graph can't see guests running in Proxmox that
have no entity, or entities whose pve_id is no longer live — the drift that
the stray test LXCs were a symptom of. discover_infra_drift enumerates running
guests via pct/qm list on every proxmox host (over the same SSH/pct path the
checks use) and diffs against the DB: returns missing (live, no entity) and
ghost (DB, not live). Read-only.

Companion to audit_knowledge_graph; the skill now runs both and treats the
remaining checks (misplaced parent, undeployed scripts, seed drift) as manual.
2026-07-29 20:36:02 +02:00
6487032461 fix(remote): ignore polluted host attributes; audit surfaces them
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
resolveProxmoxHostSlug trusted attributes.host verbatim, so a value polluted
with prose — lxc:teddycloud carried host="hubris (confirmed via pct config…)" —
became a slug that never resolved, leaving its checks 'down' despite a correct
`hosts` edge. Treat an attribute containing whitespace/parens as invalid and
fall back to the canonical hosts edge.

The audit now reports `polluted_attrs` — entities whose routing-critical
attributes carry prose — so this class is visible instead of a silent
resolution failure.
2026-07-29 19:45:12 +02:00
6007e922b4 fix(scheduler): lifecycle gate excluded NULL-state check targets
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
The B1 target-state filter `tgt.state NOT IN ('deprecated','destroyed')`
evaluates to NULL (unknown) when a target's state is NULL, which the WHERE
clause treats as false — so freshly-seeded entities without an explicit state
(the 20 TLS certificates) were silently dropped from ListEnabledCheckDefs and
never monitored. Treat NULL state as active (only explicit deprecated/
destroyed is excluded): `tgt.state IS NULL OR tgt.state NOT IN (...)`.
2026-07-29 19:39:17 +02:00
2d8eb91b25 feat(cert): dial the TLS terminator directly so cert-expiry works from the container
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
checkCertExpiry now accepts a `dial` address and sets ServerName to the
hostname — it connects to the terminator's IP while SNI/cert-read use the
hostname. The scheduler container has no mesh interface and the host resolver
doesn't know the split-horizon zone, so *.hubris.network can't be dialed by
name from there; dialing Caddy's lab IP (reachable on the LAN) makes the probe
work. The builder passes through a cert entity's `dial` attribute.

Re-seed the 20 *.hubris.network certificate entities with dial=192.168.8.175
(Caddy) and uses-certificate edges; cert-expiry monitoring now has real data.
2026-07-29 19:33:45 +02:00
04775192c1 feat(checks): wire up TLS certificate expiry monitoring
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
The ontology declared monitoring [cert-expiry] on the certificate type and a
working checkCertExpiry probe existed, but checkdefaults had no cert-expiry
builder and no certificate entities were seeded — so certificate expiry, a
real failure mode, was invisible.

Add a KindCertExpiry builder (dials the cert's hostname on :443 hourly, warns
at 30d / crit at 7d) and seed certificate entities for the 20 public
*.hubris.network routes plus uses-certificate edges from each ingress route.
2026-07-29 18:41:14 +02:00
a104cb4bb4 feat(remote): route service checks through their hosting compute entity
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
A service check used to bake its hosting LXC's lan_ip and SSH it directly as
root, which failed because the scheduler key is authorized on the Proxmox hosts
but not inside every guest — leaving all 8 service process checks 'down' even
after the guest routing and scripts were fixed.

ResolveExecTargetForCheck now, for a non-guest target, walks the
provides/runs-on/hosts edges to the compute entity that runs it and routes
through that: pct/qm exec if the host is a guest, direct SSH with the host's
correct user (workstation `user` attr) if it's a machine. The guest-resolution
path is shared via resolveGuest, and the scheduler no longer needs an
isMachine special case — one resolver handles guest, machine, and service.
2026-07-29 14:00:03 +02:00
72f0f46528 fix(scheduler): resolve guest routing when check_defs.target_type is blank
Older writeCheck inserts omitted target_type, so every seed-created check_def
had a NULL/empty target_type. checkSSHScript's IsGuest check then never matched,
and guest checks silently fell back to their baked (often mesh-only) address —
keeping them 'down' even after the pct-exec routing and deployed scripts were
in place. rclone stayed down for exactly this reason after the host-hop fix.

writeCheck now writes target_type, and checkSSHScript resolves the type from
the target_id when the column is blank (a runtime safety net for existing rows;
the seed rows were also backfilled in the live DB).
2026-07-29 13:44:14 +02:00
b87735a111 chore: graph view, dns-zone gap, fleet deploy/cleanup tooling
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
Graph view: raise the node cap 500 -> 2000 and exclude execution/task audit
rows from the default whole-graph view so the cap is spent on actual topology
rather than ~380 cognition records that crowded out every host/lxc/service.

dns-zone monitoring [dns] -> none: no dns checker exists, so the declaration
only produced unresolvable `unmonitored` noise (requires ontology re-ingest;
coverageSweep now auto-clears the stale signals). Flip back to [dns] when a
checker lands.

Operator tooling: tools/deploy-checks.sh pushes check scripts into guests via
pct push (a pct-exec-routed check runs the script INSIDE the guest), wired
into the post-pull setup-checks hook so guests stay in sync on Proxmox hosts;
scripts/cleanup-orphan-checks.sh (dry-run by default) and
report-stray-test-lxcs.sh retire legacy cruft. VERSION 0.13.0 -> 0.14.0.

Plan: plans/2026-07-29-health-check-reality-and-knowledge-graph.md.
2026-07-29 13:37:27 +02:00
1540f74342 feat(audit): read-only knowledge-graph drift report + skill
Adds audit_knowledge_graph (MCP tool) and GET /api/v1/audit/drift (endpoint)
backed by a shared internal/audit package. One pass surfaces the structural
gaps an operator otherwise finds by accident: orphan check entities, checks
targeting deprecated/destroyed entities, probes stuck down/unknown, unmonitored
declared types, and live edges pointing at destroyed targets. Each finding
carries a suggested remediation runbook. Read-only and safe to run unattended.

Ships the knowledge-graph-audit skill (SKILL.md + seeded runbook) that
interprets the report and routes findings to the lifecycle runbooks.
2026-07-29 13:37:16 +02:00
c7729b2ef6 fix(scheduler): stop monitoring deprecated/destroyed targets
ListEnabledCheckDefs now LEFT JOINs the target entity and excludes rows whose
target is deprecated or destroyed, so retired things (secrets-issuance,
homelab-mcp, the dead secrets ingress route) stop generating permanent false
alarms instead of waiting for an operator to disable the check_def by hand.

coverageSweep's None() branch previously did nothing, so a type changed from
declared monitoring to `monitoring: none` (dns-zone) left its open
`unmonitored` signals lingering forever — a None() entity never gains a check,
so the hasCheck resolution path never fired. It now resolves those signals.
2026-07-29 13:37:08 +02:00
b8b4aa2aee feat(remote): route LXC/VM checks through the Proxmox host, not direct SSH
The scheduler SSHed each guest directly and assumed a deployed probe script
plus working root SSH at the guest's address — false for headless (nfs-export),
keyless (teddycloud), mesh-only (rclone), and macOS (mac-mini) targets, which
left 49 enabled checks stuck "down" on a healthy fleet.

Extract the MCP run tool's resolveExecTarget into a shared internal/remote
package and make it the single execution path for both the scheduler and MCP.
LXC/VM checks now host-hop via pct exec / qm guest exec through the owning
Proxmox host (no per-guest lan_ip, sshd, or authorized key needed); hosts and
workstations resolve their address and user live, so mac-mini's `user: dtoro`
is honored without a re-seed. Address preference now prefers public_ipv4 over
mesh, so netbird-vps is probeable from the scheduler container.

cpu_check.sh gains a real Darwin branch (it reported cpu_pct 0 before).
checkdefaults.resolveSSHUser reads the top-level `user` attribute too.
A machine-target resolution failure is now logged before falling back to baked
config, so a broken probe-config is distinguishable from a real outage.
2026-07-29 13:37:00 +02:00
c10f6920cd fix: blast radius walks dependency direction, and reachability survives no ICMP
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
Two things the entity window redesign surfaced but deliberately left alone.

**blast_radius answered the wrong question.** It walked source→target for every
relationship type, but which end of an edge is the dependent differs per type:
"machine hosts container" means the target breaks, while "service depends-on
service" and "ingress routes-to service" mean the SOURCE breaks. Walking
everything forwards was right for hosts/provides and backwards for everything
else — and swept in 2,800+ documents/involves/targets edges of pure bookkeeping,
so the result contained tasks and executions that cannot break.

Direction is now declared per relationship type in seeds/ontology.yaml
(blast_direction: forward | backward | none), the same shape as the entity
types' monitoring: declaration, and defaults to none so an undeclared edge
contributes nothing rather than a confidently wrong answer.

It also needed a modelling fix: `routes-to` names an ingress's BACKEND, so
nothing recorded that all 21 public hostnames are terminated by caddy. A
`served-by` edge type now says so.

  pool:ludo-lvm    2 -> 23   (every container storing on it, then their services)
  lxc:caddy        4 -> 22   (service:caddy, then all 21 ingress routes)
  service:authentik      7   (what authenticates via it)

**Every ping check was reporting down.** Not a host:strong false positive: all
seven, including ws:mac-mini — the Docker host itself. The scheduler runs in
Docker on macOS, whose VM does not route ICMP to the LAN; loopback pings succeed
and every LAN ping fails. Under health aggregation each broken probe dragged its
entity to down.

The question the check exists to answer is "is it reachable", and ICMP is only
one way to ask it. checkPing now falls back to a TCP connect before concluding
anything, which restores an honest verdict for the four hosts that are genuinely
up while leaving the genuinely unreachable ones down.

TestBlastRadiusTerminatesOnCycles asserted the old direction (caddy=1,
authentik=2 — the cycle walked the wrong way); it now asserts the corrected
depths, and its exact-node-count check is relaxed because walking the right way
also surfaces the seed's own real dependents, which are correct answers.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 09:44:17 +02:00
ad29295c93 feat(web): make the entity window a triage surface, not a data dump
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
The window rendered the same 13 collapsible sections for every entity, sorted
only by "does it have content". Audit trail carried the same visual weight as
Health, and the window answered "what data do we hold about X?" rather than
"what do I need to know, and what should I do?".

Measured against prod: host:hubris has 223 relations, 1,601 events, 2.7M metric
samples and 148 executions; an ingress route has three facts. Both got 13
identical headers. Expanding a host put ~540 interactive elements on screen.

- **A verdict header that never collapses.** Not just "down" but *why*:
  "ping failing · 5 of 6 checks passing". That line did not previously exist
  and could not have — checks rendered as configuration, never as results.
- **Sections composed per type.** A document has no checks, metrics or blast
  radius; a signal or execution is a record, not a thing. Infrastructure gets
  Status/Impact/Activity/Metrics/Reference, knowledge types lead with Content,
  records get a minimal view. Unknown types fall back to infrastructure so a
  new entity type is never a blank window.
- **Status replaces Monitoring**, showing each check's own verdict and when it
  last ran — the section that answers the header's "why".
- **Impact** finally calls /entities/{id}/blast-radius. The endpoint has existed
  since the first API and had no frontend caller anywhere, despite
  .agents/OIKOS.md naming blast radius as the reason the ontology exists. Its
  outgoing-edges-only limitation is stated in the UI rather than hidden.
- **Activity merges four lists** (executions, signals, events, agent activity)
  that were telling one story in four places.
- **Relations cap at 8 with a drill-in** — 540 interactive elements down to 126.
- **Ask Nomos** opens a task pre-scoped to what you are looking at, seeded with
  the verdict just computed, via an optional draft threaded through
  openNewTaskWindow -> NewTaskChat -> ChatThread.

Requires exposing check_defs.last_health/last_run_at through the API (the
columns landed with the health-aggregation work but were never surfaced).
Adding a fourth enum containing "unknown" made oapi-codegen disambiguate all
enum constants by type prefix, so metrics.go moves to gen.TrendDirection*.

Verdict derivation and type->section composition live in $lib/entityView.ts as
pure functions with 15 unit tests, including the host:strong case that
motivated this.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 09:24:28 +02:00
6ca6d5b352 fix(scheduler): derive entity health from all its checks, not the last one
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
host:strong logged 226 health.changed events in one hour, oscillating
down/healthy while the host was fine throughout. host:hubris did it 126 times.

runCheck wrote entity_status.health on every check completion, so an entity's
health was simply whichever of its checks finished most recently. A host with
six checks reported whichever facet happened to be sampled last, and one
failing probe alternating with five passing ones flapped forever. resolveSignal
forced "healthy" too, a second path by which one passing probe erased another
probe's genuine failure.

On this fleet the trigger is a known false positive: the scheduler's network
vantage point cannot ICMP host:strong, so its ping check fails while every
ssh-script check succeeds. Under last-writer-wins that single probe declared
the whole host down, twice a minute.

Each check now records its own verdict (check_defs.last_health, migration 027)
and the entity's health is the worst across its enabled checks. A failing probe
now degrades the entity honestly and *stably*, without erasing what the other
five report, and health.changed fires only when that aggregate actually moves.
Checks that have never run are ignored rather than counted as unknown, so
adding a check cannot drag a known-good entity down before it has a verdict.

Also declares service:oikos in the seed. The previous commit re-pointed the mcp
ingress at it, but the entity only ever existed in the production database — so
a fresh seed (a new install, or a DR restore) failed on an unresolvable edge.
Caught by seeding an empty database rather than a copy of prod, which is the
only way that class of bug shows up.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 21:33:48 +02:00
98e19bb14a Merge remote-tracking branch 'origin/main' into claude/coolify-oikos-comparison-d8c2d3 2026-07-28 14:05:36 +02:00
d7b526a112 fix(scheduler): honour check_defs.interval_s, and renumber migrations off main
ListEnabledCheckDefs selected interval_s but never filtered on it, so every
enabled check ran on every 30s pass and the declared per-check intervals were
decorative. Invisible at 17 enabled checks; at ~180 it would have meant ~126
SSH connections every 30s (~363k/day) and `apt update` on every machine every
30 seconds — 14,400 mirror hits a day to answer a question that changes daily.

- check_defs.last_run_at (migration 026) + a due-ness predicate in the query.
  A column rather than scheduler memory because this control plane restarts on
  every deploy, and an in-memory map would re-fire every check on each restart.
- runCheck stamps last_run_at before processing the result, so a permanently
  failing check backs off to its interval instead of re-running every pass.
- updates and backup-freshness drop to daily. Both answer questions whose
  answers change about once a day; 60s was just the shared ssh-script default.
- last_run_at is seeded to a random offset within the interval so checks
  created by the same seed do not stay in lockstep — otherwise ~165 probes
  land in the same instant each minute instead of spread across it.
  Deliberately not in the upsert's DO UPDATE: a re-seed must not re-herd them.

Steady state becomes ~180k SSH/day (down from ~363k) and 5 apt runs/day
(down from 14,400), with each 60s check landing at its own point in the minute.

Also renumbers 022→023, 023→024, 024→025: origin/main added its own
022_knowledge_revisions, and prod has already applied version 22. Left
colliding, prod would have skipped the monitoring_spec migration entirely and
then failed the seed on a missing column.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 14:03:30 +02:00
1dca2cfd7a feat(observability): restore monitoring coverage, make gaps visible, stream executions
Monitoring coverage was 3 of 89 active entities. Three bugs, each hidden by
discarded errors in checkdefaults:

- writeCheck generated a fresh uuid, inserted the check entity ON CONFLICT
  (slug) DO NOTHING, then wrote a check_defs row referencing it. On any
  re-seed the slug already existed, the entity insert no-oped, and the FK
  violated — aborting the ingest transaction and surfacing as an unrelated
  failure several entities later. Re-seeding has been broken since; prod's
  coverage was frozen at its first successful seed. This is what
  TestSeedIngestIdempotentAndNoDuplicateEdges had been reporting.
- shortSlug truncated to the last 8 chars, so all 21 ingress routes collapsed
  to ".network" and overwrote each other; service:jellyfin collided with
  lxc:jellyfin.
- The ssh-script checker never read the `args` config checkdefaults wrote, so
  process_check.sh always ran without its unit name and returned "unknown".

Coverage is now 75/89. Monitoring is declared per entity type in
seeds/ontology.yaml and resolved through the is-a hierarchy, so a type can say
it warrants nothing (site, lan, mesh, cluster) and never be reported as a gap.
coverageSweep raises an `unmonitored` signal only where a type declares
monitoring it lacks — 8 real gaps, no false positives.

Also:
- entity_types.attribute_schema was never ingested: the seed loader read
  "attribute_schema" but the YAML says "attributes", so all 60 types stored
  JSON null.
- ListExecutions ignored its declared target/action/correlation_id filters and
  paginated on a non-unique target slug, dropping and repeating rows.
- started_at was captured but only written at terminal state, so a running
  execution reported NULL for its whole life. The three MCP auto-run copies
  wrote no timing at all; they are now one autoRun helper.
- SSH output was buffered to completion and discarded entirely on timeout.
  Both sshExec copies now stream through a shared execlog sink into
  execution_logs, and keep partial output when a command is cancelled.
- executions.correlation_id was a random per-execution uuid that correlated
  nothing; it is now the chat session id, which is what lets the chat tail
  live output.
- reversible_low had no auto-run branch despite policy declaring it
  unattended. Since computeCommandRisk never returns it, the class only arises
  when an agent declares it over a read_only command — so gating it penalised
  candor without adding safety.
- backup-target gains a backup-freshness checker (portable find -mmin, since
  the first target is on macOS), resolving its host by walking backs-up-to
  backwards. The pre-deploy pg_dump is now a tracked backup target.

UI: an Executions section on entity detail with live output tailing, and
streamed output under a running `run` call in the chat timeline.

Migrations 022-024. Ops.svelte and context.ts exclude execution.output from
their refetch triggers, which would otherwise fire once a second per command.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 13:51:14 +02:00
ce0e4142ff feat(api): add knowledge base write path, revision history, and drift tooling
The Knowledge page was read-only from the HTTP API — the only writer was
the agent's MCP upsert_knowledge tool. Adds create/update/soft-delete/
restore/trash endpoints, a DB-trigger-backed revision history (catches
both the web UI and the MCP tool), and maintenance endpoints: duplicate
detection (pg_trgm + complete-linkage clustering), tag rename/normalize,
orphan detection, and merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 22:50:29 +02:00
ccbf6a8aac fix(nomos): sessions blocked on an execution approval now show "Needs input" and never idle-close
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
A config_mutation/destructive run() queued for approval never touched
agent_sessions.status — only ask_operator did that, setting
awaiting_input. So a task blocked on an execution approval was
indistinguishable from one still genuinely working: the frontend's
"Needs input" bucket only checks status===awaiting_input (never lit
up for these), and the idle-sweep safety net only excludes
awaiting_input from its stale-task query, so after ~30 minutes idle
it would nudge the agent and then auto-close the task with
outcome=partial while the approval was still sitting there undecided.

classifyAndGate now flips the session into awaiting_input the moment
an execution is queued (internal/mcp/server.go), and DecideApproval
flips it back to executing once the approval is approved, denied, or
revoked (internal/httpapi/approvals.go) — mirroring askOperator /
answerQuestion's existing pattern for session_questions. Both emit
task.status so the board updates live.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 12:28:52 +02:00
e055a7c6ce feat(nomos): session-review improvements (P0/P1/P2 from 2026-07-20 audit)
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
Classifier now unwraps pct exec / qm guest exec / bash -c / sh -c / sudo
and env-var assignments before classification, so read-only inspection
wrapped in pct exec no longer escalates to config_mutation. curl GET
(default method, no -d/-F/-T/-o/>) is read-only. Eliminates the three
duplicate rclone sessions (a51e2086, 8acea2e3, cb8c8a4a) that bounced
off the classifier for the same goal.

New classify_command MCP tool: command-scoped preflight that returns the
exact risk class run would assign. Documented in SOUL.md with guidance
to pre-classify before run when the verdict is uncertain.

set_goal surfaces prior partial/failed sessions from the last 24h so the
agent picks up the thread instead of rediscovering it.

completeTask auto-closes in-flight plan steps (pending/running -> done
on success, skipped on partial/failure), so one-step plans no longer
need the per-step running->done dance right before completion.

Migration 021 adds blocker + closed_at to agent_sessions. completeTask
sets closed_at once and derives a structured blocker reason
(approval_timeout, user_abandoned, classifier_overreach, model_refusal,
tool_error, ...) from the last assistant message.

/sessions list now carries message_count, tool_call_count,
duration_seconds (server-side aggregates — no more N+1 transcript
fetches to audit a fleet). GET /sessions/{id} returns both metadata
and messages. New query params filter + paginate: outcome, status,
entity_id, blocker, since (RFC3339 or Go duration), cursor, limit.

Titles now prefer the goal when set; sessions without a goal fall back
to the first assistant text.

New GET /sessions/{id}/tool_calls flat view for audit scripts.

Plan: plans/2026-07-20-session-review-ten-sessions.md. VERSION 0.7.12 -> 0.7.13.
2026-07-20 11:32:31 +02:00
544afae77f feat(nomos): retry cap, vm: targets, inspect_path, goal supersession, runbooks
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
Session-review implementation for the three sessions audited in
plans/2026-07-18-session-review-three-sessions.md. v0.7.11 → v0.7.12.

P0.1 — retry cap + investigate-before-retry (cmd/nomos/retrycap.go,
agent.go): after 3 identical failing run calls in a single turn, refuse
to dispatch the call again and return a directive to investigate *why*
(ps/strace/lsof) or surface the blocker. Per-turn scope so a fresh turn
after the operator responds can retry once more. Session 1e9c7691's 20+
identical chown retries (knfsd held a kernel lock on the exported NFS
dir) is the direct motivation.

P0.2 + P1.8 + P2.10 — SOUL.md guidance: hung command is not a failed
command (investigate before retry); ask before proposing a multi-step
migration; multi-goal sessions summarize the arc not just the last goal.

P1.3 — two new runbook entities in seeds/knowledge.yaml:
  - nfs-exported-dir-mutation-hang (the knfsd fchownat lock procedure:
    killall → exportfs -u → mutate → exportfs -a → verify)
  - netbird-mgmt-oidc-race-after-upgrade (docker restart netbird-mgmt
    after ~30s for the traefik/authentik OIDC race)

P1.4 — setGoal emits task.superseded event when prior goal is overwritten
by a different goal (store.go, TestSetGoal_SupersededEvent). Session
55927f0a had two set_goal calls with the first silently abandoned.

P1.5 — inspect_path MCP tool: runs mount/df/ls/stat for one path across
up to 8 targets in one parallel call, replacing the 15+ run-call
fact-gathering fan-out sessions 1 and 2 each spent on cross-target path
tracing (tools.go, server.go: inspectPathAcrossTargets, inspectOneTarget).

P1.6 — vm: target support in run via qm guest exec (no more SSH-hop
with nested quoting). Extracted shared resolveProxmoxHostSlug for
LXC + VM, with hosts-relationship fallback when attributes.host is
absent (server.go, tools.go). Session 55927f0a's SSH-hop workarounds
for vm:zimaos are the direct motivation.

Deferred (documented in plan): P1.7 (approval window auto-extend on
timeout) and P2.9 (long-running command PENDING detection) — both
addressed at lower cost by the retry cap. Session 3's poll-after-timeout
pattern already works; the cap protects against the failure mode.
2026-07-19 00:09:39 +02:00
646373a676 fix(httpapi): GetGraph 500s when rel_type is omitted
req.Params.RelType is *[]string; passing the nil pointer straight
through as a pgx query arg (both in the blast_radius() call and in
ListGraphEdges) panics because pgx can't infer the array element type
from a nil *[]string, only from a concrete (possibly nil) []string.
Dereference once up front instead. Also affected the sqlc-based
ListGraphEdges path added by the R3 refactor, which had the same bug.

Add a regression test for GET /api/v1/graph?root=X&depth=N with no
rel_type.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 23:25:41 +02:00
7dc1c1ae39 docs: document the non-OpenAPI routes carve-out (R11)
10 routes are registered manually on the chi router in server.go rather
than generated from openapi.yaml. Added a 'Non-OpenAPI routes' comment
block at the top of NewHandler listing each route with its structural
reason for the carve-out:

  - Auth/infra: /healthz, /api/v1/auth/oidc-*, /oidc-callback — bypass
    auth middleware or aren't JSON API
  - SSE override: /api/v1/events/stream — re-registered for Flush()
  - Ad-hoc aggregations: /knowledge/recent, /knowledge/content/{id},
    /activity/recent, /activity/session/{id}, /learning/timeline,
    /learning/trend — derived shapes with no schema type yet

Updated .agents/dev/CONTRIBUTING.md §OpenAPI codegen with the carve-out
policy: if an ad-hoc route stabilizes, promote it to openapi.yaml with a
proper schema and migrate the serve* function to a strict handler.
2026-07-17 23:02:08 +02:00
c96c795126 test: add unit tests for 6 previously-untested packages (R7)
Added pure unit tests for all packages that had 0% coverage. Where pure
logic was entangled with DB calls, extracted testable helpers first.

internal/domain (0% -> 100%):
- TestIsNil, TestCanTransition (all 30 state transitions), TestSentinelErrors,
  TestSignalTransitionsComplete

internal/learning (0% -> 26.2%):
- Refactored processGroup to extract 4 pure helpers: countOutcomes,
  computeConfidence, shouldValidate, shouldQuarantine
- TestWilsonLowerBound (monotonicity, edge cases, sample-size cap)
- TestCountOutcomes, TestComputeConfidence, TestShouldValidate,
  TestShouldQuarantine (table-driven)
- Remaining gap: extractPatterns/processGroup DB calls need make test-db

internal/policy (39% -> 50%):
- Extracted determineRoute from ClassifySignal (pure route logic)
- TestDetermineRoute (7 cases covering global/entity kill-switches, approval)
- Remaining gap: ClassifySignal/computeBlastRadius need DB mock

internal/knowledge (0% -> 14.2%):
- TestContentHash, TestStr, TestStrSlice, TestMapVal, TestToPGArray
- Documented latent bug: toPGArray doesn't escape " or \\ in tags
- Remaining gap: ingest* functions need make test-db

internal/actuator (0% -> 14.7%):
- TestSSHErrorClassString, TestClassifySSHError (11 cases incl. net.Error mock)
- TestParseProcedure, TestSetDefaultSSHTimeout
- Circuit breaker full state-machine test (open/close/reset/per-target)
- Remaining gap: ExecuteProcedure/ProvisionLXC need SSH+DB fixtures

internal/scheduler (0% -> 7.3%):
- TestParsePingLatency (Linux/macOS formats), TestAllowlistedScript
- TestEvaluateSeverity (threshold logic, crit:0 skip, signalKind fallback)
- Remaining gap: checkHTTP/checkTCP need httptest; runCheckPass needs DB

internal/notifier (0% -> 6.4%):
- TestHashToken, TestGenerateApprovalToken (HMAC re-derivation)
- Remaining gap: checkReaction/sendMatrixAlert need httptest; DB funcs need
  make test-db

All tests pass with -race. domain hits its 60% gate at 100%. The remaining
packages need integration tests (make test-db) and/or httptest-based tests
to reach their coverage gates — tracked as follow-up.
2026-07-17 22:54:44 +02:00
fb39a48bef refactor: split phase3.go + extract MCP tool registry (R4)
internal/httpapi/phase3.go (2627 lines, 12+ resource domains) split into
15 per-resource files:
- actuator.go: SSH execution machinery (initSSH, sshExec, resolveRunTarget,
  executeApprovedAction, jsonErr, gatewayPreflightPassed, resolveTemplate)
- checks.go, classifications.go, executions.go, approvals.go, patterns.go,
  skills.go, approval_rules.go, autonomy.go, risk_classes.go,
  relationships.go, entity_types.go, metrics.go, agent_activity.go,
  helpers.go — one file per resource domain, each with its own imports.

internal/mcp/server.go: newServer (708 lines, 33 inline tool registrations)
refactored to a registry pattern:
- internal/mcp/tools.go (new): toolReg struct + allTools() returning all 33
  tool definitions. Handler logic moved verbatim — no changes to tool names,
  descriptions, schemas, or behavior.
- server.go: newServer is now 9 lines (iterate registry, AddTool each).
  -699 lines.

No function logic, names, or signatures changed. go vet, build, and all
tests pass (httpapi, mcp, db, policy).
2026-07-17 22:41:40 +02:00
d2950dd09d refactor: sqlc vs raw SQL — hybrid approach (R3)
Deleted 8 genuinely unused sqlc queries (no inline equivalent):
- UpsertCurrentRelationship, ListEntitiesCapped, ListEntityStatus,
  UpdateSignalState, InsertClassification, InsertFeedback, InsertSkill,
  UpsertCurrentRelationship — all had zero call sites.

Migrated 9 inline raw SQL sites to use sqlc queries:
- GetOntology (impl.go): ListEntityTypes, ListRelationshipTypes,
  ListLifecycleDefs — replaces 3 raw pool.Query blocks with typed sqlcgen
  calls, eliminating manual row scanning.
- EndRelationship (phase3.go): EndCurrentRelationship — replaces tx.Exec
  with sqlcgen.New(tx).EndCurrentRelationship.
- checkPrecondition (impl.go): GetEntityStatus — replaces tx.QueryRow +
  manual Scan with sqlcgen.New(tx).GetEntityStatus.
- GetEntityRelations (impl.go): ListEntityRelations — replaces raw pool.Query
  + scanRelationships helper (now deleted).
- GetGraph (impl.go): ListGraphEdges — replaces raw pool.Query +
  scanRelationships.
- resolveEntityID (impl.go): GetEntityBySlug/GetEntityByID — replaces
  raw pool.QueryRow + Scan.
- createApproval (mcp/server.go): InsertApproval — replaces raw pool.Exec
  with sqlcgen.InsertApproval.

Deleted scanRelationships helper (was only used by the two migrated
graph queries above).

Regenerated sqlcgen — also picks up stale model updates (AgentSession,
SessionPlanStep, SessionQuestion, etc. from recent migrations).

Documented the carve-out in .agents/dev/CONTRIBUTING.md §SQL conventions:
sqlc is the default; raw pool.Query/Exec is reserved for LISTEN/NOTIFY,
dynamic WHERE builders, blast_radius(), and COPY.

go vet, build, httpapi/mcp/db tests all pass. -383/+170 lines.
2026-07-17 22:24:23 +02:00
c3973e7ac9 refactor: delete dead Go code (R1)
- internal/httpapi/stubs.go: delete — 5-line comment-only orphan file with
  no declarations; its own comment said the stubs live in phase3.go.
- internal/notifier/notifier.go: delete VerifyApprovalToken — zero call
  sites; phase3.go:DecideApproval reimplements the check inline (noted as
  dead in docs/mbse). hashToken stays (used by generateApprovalToken).
- internal/checkdefaults/defaults.go: unexport ResolveHost, ForEntityType,
  ShortSlug, DefaultInterval — only called within the package. Ensure stays
  exported (called by internal/db/seed.go).

go vet, go build, and affected tests pass.
2026-07-17 22:06:46 +02:00
7ef8446825 fix(agent+ui): whatsapp session audit — approvals, stuck indicator, stale execs
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
P1: add docker compose (logs|ps|top|config|images|port|cp) to read-only
allowlist. docker compose logs was classified as config_mutation, causing
individual approval cards for read-only inspection commands.

P2: remove approval entries from activityLog. They were always status=running
and never transitioned to done (the derived store builds from tool-call
text, not execution status), causing AgentIndicator to latch onto a stale
'Approval: ...' entry and never clear — even after the session completed.

P3: remove InlineApproval from Chat.svelte. The green 'Completed in 1s on
lxc:...' boxes were noise in the chat stream. Approval UX belongs in the
Operations page (already has it via Ops.svelte), not inline in the chat.

P4: stale execution cleanup. Startup sweep (mark >1hr non-terminal as
cancelled) + 5-min periodic sweep (mark >10min non-terminal as cancelled).
98 orphaned executions accumulated from eval testing (39 running from
apt_upgrade:audit timeouts, 19 pending_approval, 3 approved).

P5: refuse second config_mutation run when an approval is already pending
for the session. Without this, the agent queues N individual approvals
before the operator can respond — confirmed in session 20757eb9 (two
approval cards for what should have been one plan-level approval).

VERSION 0.7.0 → 0.7.1
2026-07-15 22:19:30 +02:00
e3fa6736c0 feat(agent): plan-first gate, iterative follow-ups, reasoning persistence
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
P1 plan-first: run handler refuses without propose_plan (structural gate,
not SOUL.md prose). Plan window decoupled from set_goal — config_mutation
auto-run only on operator approval (assent window). Closes the approval-free
config_mutation hole confirmed in session d0d562e0.

P2 iteration: reopenSession flips terminal→executing, marks prior plan steps
replaced, clears outcome. proposePlan excludes replaced from in-flight check,
bumps generation. A follow-up on a completed session starts a new sub-task
with a fresh plan — no more errPlanInFlight dead end.

P3 reasoning: accumulate per-iteration text into the persisted row instead
of overwriting with the last text event. Reload shows intermediate thinking,
not just the final summary.

P4 read-only allowlist: add find, tree, locate, systemctl list-timers/
list-unit-files/show, timedatectl, hostnamectl, systemd-analyze, rclone
ls/lsl/md5sum/check/cryptcheck. Fixes the find misclassification from
d0d562e0.

P5 eval harness: new assertion kinds (proposes_plan, plan_before_run,
plan_generations), multi-turn followups, fetch /sessions/{id}/plan. Four
manifests under evals/.

P6 SOUL.md: strip degenerate-case carve-out, add ITERATE step, update
set_goal guidance.

VERSION 0.6.0 → 0.7.0
2026-07-15 09:36:27 +02:00
49dfaa77e6 fix(api): sort graph nodes by degree instead of alphabetically
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
The unrooted graph endpoint caps at 500 entities with ORDER BY e.slug, which fills the cap with exec:* rows and excludes every host/lxc/service/vm entity. Since edges require both endpoints in the node set (ANY/ANY), 99.9% of edges were dropped — 500 nodes but only 1 edge survived.

Fix: select the 500 most-connected entities (by relationship count descending) so the topology is preserved. Result: 500 nodes, 900 edges across all relationship types.
2026-07-14 22:02:58 +02:00