diff --git a/docs/adr/0013-signal-triggers.md b/docs/adr/0013-signal-triggers.md index 63fad64..b42eafc 100644 --- a/docs/adr/0013-signal-triggers.md +++ b/docs/adr/0013-signal-triggers.md @@ -4,40 +4,28 @@ When Nomos is asked "what are the thermals of hubris?", here is exactly what happens: -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Nomos │ │ Oikos │ │Scheduler │ │ Hubris │ -│ (Agent) │ │ API │ │ (Docker) │ │(Proxmox) │ -└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - │ │ │ │ - │ query_metrics │ │ │ - │────────────────>│ │ │ - │ │ │ │ - │ ← cpu_pct=2.5 │ SELECT FROM │ │ - │ cpu_temp=48 │ metric_samples│ │ - │<────────────────│ │ │ - │ │ │ │ - │ │ │ │ - ══════ Every 60s (autonomous loop) ══════ │ - │ │ │ │ - │ │ │ SSH exec │ - │ │ │─────────────────>│ - │ │ │ /opt/oikos/ │ - │ │ │ checks/ │ - │ │ │ cpu_check.sh │ - │ │ │ │ - │ │ │ {"health":"ok", │ - │ │ │ "metrics": │ - │ │ │ {"cpu_pct":2.5, │ - │ │ │ "cpu_temp":48}}│ - │ │ │<─────────────────│ - │ │ │ │ - │ │ │ INSERT │ - │ │ │ metric_samples │ - │ │ │ │ - │ │ │ UPSERT signal │ - │ │ │ (dedup) │ - │ │ │ │ +```mermaid +sequenceDiagram + participant N as Nomos (Agent) + participant A as Oikos API + participant S as Scheduler (Docker) + participant H as Hubris (Proxmox) + participant T as TimescaleDB + + Note over S,H: Every 60s (autonomous loop) + S->>H: SSH exec /opt/oikos/checks/cpu_check.sh + H-->>S: {"health":"ok","metrics":{"cpu_pct":2.5,"cpu_temp":48}} + S->>T: INSERT metric_samples (cpu_pct, cpu_temp) + S->>T: UPSERT entity_status (health) + alt unhealthy + S->>T: UPSERT signal (dedup by target+kind) + end + + Note over N,T: User asks "what are the thermals of hubris?" + N->>A: MCP query_metrics(metric=["cpu_pct","cpu_temp"]) + A->>T: SELECT time_bucket(…) FROM metric_samples + T-->>A: cpu_pct=15%, cpu_temp=48°C + A-->>N: {avg, min, max} per bucket ``` ## Two Paths @@ -105,22 +93,18 @@ or on failure: ## Script Deployment -``` - Git Push Sync Timer (5min) Target Host - ┌────────┐ ┌────────────────┐ ┌──────────┐ - │ git push│ │ git pull │ │ │ - │ origin │───────────────>│ homelab-context│ │ │ - │ main │ │ │ │ │ - └────────┘ │ post-pull.sh │ │ │ - │ → tools/ │ │ │ - │ setup- │ │ │ - │ checks.sh │ │ │ - │ → checks/ │ │ │ - │ install.sh│ │ │ - │ │──cp *.sh ─>│ /opt/ │ - │ │ │ oikos/ │ - │ │ │ checks/ │ - └────────────────┘ └──────────┘ +```mermaid +sequenceDiagram + participant R as Git Repo + participant T as Sync Timer (5min) + participant H as Target Host + + Note over R,T: Operator pushes scripts + R->>T: git pull (homelab-context) + T->>T: tools/post-pull.sh + T->>T: → tools/setup-checks.sh + T->>T: → checks/install.sh + T->>H: cp *.sh → /opt/oikos/checks/ ``` ## Defining a Check @@ -145,12 +129,26 @@ curl -X POST http://oikos:8090/api/v1/checks \ ## Signal Lifecycle -``` - raised ──> acknowledged ──> acting ──> resolved - │ │ │ - ├── muted ├── muted ├── raised (retry) - │ │ │ - └── resolved └── resolved └── failed +```mermaid +stateDiagram-v2 + [*] --> raised + raised --> acknowledged + raised --> muted: mute_until set + raised --> resolved: condition cleared + + acknowledged --> acting: classification exists + acknowledged --> muted + acknowledged --> resolved + + acting --> resolved: verification passed + acting --> raised: retry budget remaining + acting --> failed + + failed --> acknowledged: operator retry + + muted --> raised: mute_until expired + + resolved --> [*] ``` Signals deduplicate: **one open signal per (target_entity_id, kind)**. @@ -177,24 +175,17 @@ Severity mapping: ## Data Flow (DB Tables) -``` - check_defs ──(scheduler reads)──> executeCheck() - │ │ - │ ├── healthy? → resolve signal, upsert entity_status - │ │ - │ └── unhealthy? → UpsertSignal(), insert metric_samples - │ - ▼ - signals ◄──── UpsertSignal (dedup by target+kind) - │ - ▼ - entity_status ◄── upsert (health, last_check_at) - - metric_samples ◄── INSERT (every cycle, healthy or not) - │ - ▼ - metric_rollups_1h ◄── continuous aggregate - metric_rollups_1d ◄── continuous aggregate +```mermaid +flowchart TD + CD[check_defs] -->|scheduler reads| EC[executeCheck] + EC -->|healthy?| RS[resolve signal + upsert entity_status] + EC -->|unhealthy?| US[UpsertSignal dedup by target+kind] + EC -->|every cycle| IM[INSERT metric_samples] + US --> S[signals] + RS --> ES[entity_status] + IM --> MS[(metric_samples)] + MS --> R1H[metric_rollups_1h continuous aggregate] + MS --> R1D[metric_rollups_1d continuous aggregate] ``` ## Prerequisites for SSH Checks diff --git a/docs/adr/0014-entity-model.md b/docs/adr/0014-entity-model.md index 552c23b..91866a4 100644 --- a/docs/adr/0014-entity-model.md +++ b/docs/adr/0014-entity-model.md @@ -1,7 +1,7 @@ # Oikos Entity Model — Types, Relationships & Interactions -**Status:** Adopted -**Date:** 2026-07-08 +**Status:** Adopted +**Date:** 2026-07-08 **Scope:** Full inventory of every entity type, relationship, state machine, and cognition pipeline — with clear markers for what is **code-real** vs **schema-only**. @@ -9,52 +9,162 @@ cognition pipeline — with clear markers for what is **code-real** vs **schema- ## 1. Entity Type Hierarchy (56 types) +```mermaid +graph TD + subgraph meta["layer: meta"] + entity["★ entity"] + end + + subgraph infrastructure["layer: infrastructure"] + subgraph physical["domain: physical"] + site + ups + sensor + peripheral + end + + subgraph compute["domain: compute"] + ce["★ compute-entity"] + machine["★ machine"] + proxmox-host + standalone-server + workstation + appliance + vm + container["★ container"] + lxc + docker-container + hypervisor + end + + subgraph network["domain: network"] + net["★ network"] + lan + mesh + vlan + network-interface + dns-zone + dns-record + ingress-route + certificate + firewall-rule + end + + subgraph storage["domain: storage"] + storage-pool + volume + backup-target + dataset + end + + subgraph software["domain: software"] + service + application + config-repo + deploy-pipeline + package-set + cluster + compose-stack + end + + subgraph external["domain: external"] + domain-registration + cloud-service + isp-link + vendor-dependency + end + end + + subgraph governance["layer: governance"] + subgraph identity["domain: identity"] + person + agent + identity-provider + account + secret + key + access-grant + end + end + + subgraph cognition["layer: cognition"] + check + signal + classification + execution + feedback + pattern + skill + approval + document + runbook + investigation + end + + entity --> ce + entity --> machine + entity --> net + entity --> container + entity --> site + entity --> ups + entity --> sensor + entity --> peripheral + entity --> vm + entity --> hypervisor + entity --> network-interface + entity --> dns-zone + entity --> dns-record + entity --> ingress-route + entity --> certificate + entity --> firewall-rule + entity --> storage-pool + entity --> volume + entity --> backup-target + entity --> dataset + entity --> service + entity --> application + entity --> config-repo + entity --> deploy-pipeline + entity --> package-set + entity --> cluster + entity --> compose-stack + entity --> domain-registration + entity --> cloud-service + entity --> isp-link + entity --> vendor-dependency + entity --> person + entity --> agent + entity --> identity-provider + entity --> account + entity --> secret + entity --> key + entity --> access-grant + entity --> check + entity --> signal + entity --> classification + entity --> execution + entity --> feedback + entity --> pattern + entity --> skill + entity --> approval + entity --> document + entity --> runbook + entity --> investigation + + ce --> machine + ce --> container + machine --> proxmox-host + machine --> standalone-server + machine --> workstation + machine --> appliance + container --> lxc + container --> docker-container + net --> lan + net --> mesh + net --> vlan ``` -layer: meta - entity ★ (abstract root) -layer: infrastructure ────────────────────────────────────────────────── - domain: physical - site ups sensor peripheral - - domain: compute - compute-entity ★ (abstract) - machine ★ (abstract) - proxmox-host standalone-server workstation appliance - vm - container ★ (abstract) - lxc docker-container - hypervisor - - domain: network - network ★ (abstract) - lan mesh vlan - network-interface dns-zone dns-record - ingress-route certificate firewall-rule - - domain: storage - storage-pool volume backup-target dataset - - domain: software - service application config-repo deploy-pipeline - package-set cluster compose-stack - - domain: external - domain-registration cloud-service isp-link vendor-dependency - -layer: governance ────────────────────────────────────────────────────── - domain: identity - person agent identity-provider account - secret key access-grant - -layer: cognition ── the OODA loop ────────────────────────────────────── - domain: cognition - check signal classification execution feedback - pattern skill approval - document runbook investigation - - ★ = abstract (cannot be instantiated; acts as polymorphic target for relationships) -``` +★ = abstract (cannot be instantiated; polymorphic target for relationships) ### Concrete instances (88 active entities) @@ -76,45 +186,26 @@ layer: cognition ── the OODA loop ────────────── ## 2. Core Sequence: Machine Onboarding -``` - Operator Oikos API DB Scheduler Target Machine - ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ - │ POST │ │ │ │ │ │ │ │ │ - │/entities│────>│Create │ │ │ │ │ │ │ - │ │ │Entity() │ │ │ │ │ │ │ - │ │ │ │────>│INSERT │ │ │ │ │ - │ │ │ │ │entities │ │ │ │ │ - │ │ │ │ │ │ │ │ │ │ - │ │ │ensure │ │ │ │ │ │ │ - │ │ │Default │────>│INSERT │ │ │ │ │ - │ │ │Checks() │ │check_defs│ │ │ │ │ - │ │ │→ ping │ │×6 │ │ │ │ │ - │ │ │→ cpu │ │ │ │ │ │ │ - │ │ │→ memory │ │ │ │ │ │ │ - │ │ │→ load │ │(target_id│ │ │ │ │ - │ │ │→ disk │ │ set) │ │ │ │ │ - │ │ │→ updates │ │ │ │ │ │ │ - │ │ │ │ │ │ │ │ │ │ - │ │<────│201 │ │ │ │ │ │ │ - │ │ │Created │ │ │ │ │ │ │ - │ │ │ │ │ │ │ │ │ - │ │ │ │ │ │ ── 30s tick ─>│ │ │ - │ │ │ │ │ │ loads │ │ │ - │ │ │ │ │ │ check_defs │ │ │ - │ │ │ │ │ │ │──SSH────>│ │ - │ │ │ │ │ │ │ /opt/ │ │ - │ │ │ │ │ │ │ oikos/ │ │ - │ │ │ │ │ │ │ checks/ │ │ - │ │ │ │ │ │ │ cpu.sh │ │ - │ │ │ │ │ │ │<──JSON───│ │ - │ │ │ │ │<─────────│INSERT │ │ │ - │ │ │ │ │metric │metric_samples │ │ │ - │ │ │ │ │samples │ │ │ │ - │ │ │ │ │ │ │ │ │ - │ │ │ │ │<─────────│UPSERT │ │ │ - │ │ │ │ │entity │entity_status │ │ │ - │ │ │ │ │status │(health) │ │ │ - │ │ │ │ │ │ │ │ │ +```mermaid +sequenceDiagram + participant O as Operator + participant A as Oikos API + participant D as DB + participant S as Scheduler + participant T as Target Machine + + O->>A: POST /api/v1/entities {type:proxmox-host, slug:host:new, …} + A->>D: INSERT INTO entities + A->>A: ensureDefaultChecks().resolveHost() → lan_ip + A->>D: INSERT check_defs × 6 (target_id set) + A-->>O: 201 Created + + Note over S,T: 30s scheduler tick + S->>D: ListEnabledCheckDefs + S->>T: SSH exec /opt/oikos/checks/cpu_check.sh + T-->>S: {"health":"ok","metrics":{"cpu_pct":2.5,"cpu_temp":48}} + S->>D: INSERT metric_samples + S->>D: UPSERT entity_status (health) ``` **What's code-real here:** @@ -124,117 +215,73 @@ layer: cognition ── the OODA loop ────────────── --- -## 3. Core Sequence: The OODA Loop (observe → orient → decide → act) +## 3. Core Sequence: The OODA Loop (observe → orient → decide → act → learn) -``` - ┌─────────────────────────────────────────────────────────────────────┐ - │ OBSERVE (Scheduler) │ - │ │ - │ Every 30s: │ - │ ┌──────────┐ ListEnabledCheckDefs ┌──────────┐ │ - │ │scheduler │─────────────────────────>│ Postgres │ │ - │ │.go:54 │ │ │ │ - │ └──────────┘ └──────────┘ │ - │ │ │ - │ ├── ping ──> exec.Command("ping", host) │ - │ ├── http ──> http.Get(url) │ - │ ├── tcp ──> net.DialTimeout("tcp", addr) │ - │ ├── disk ──> unix.Statfs(path) │ - │ ├── cert-expiry ──> tls.Dial + cert.NotAfter │ - │ └── ssh-script ──> exec.Command("ssh", host, script) │ - │ │ │ - │ ┌───────┘ │ - │ ▼ │ - │ ┌─────────────┐ │ - │ │ checkResult │ {health, signalKind, evidence, metrics}│ - │ └─────────────┘ │ - │ │ │ - │ ┌──────────┼──────────┐ │ - │ ▼ ▼ ▼ │ - │ metric_samples signals entity_status │ - │ INSERT UPSERT UPSERT │ - │ (every cycle) (dedup by (health + last_check_at) │ - │ target+kind) │ - └─────────────────────────────────────────────────────────────────────┘ +```mermaid +flowchart TB + subgraph OBSERVE["🔍 OBSERVE (Scheduler every 30s)"] + direction TB + S1["ListEnabledCheckDefs"] + S2["ping → exec.Command(ping, host)"] + S3["http → http.Get(url)"] + S4["tcp → net.DialTimeout(tcp, addr)"] + S5["disk → unix.Statfs(path)"] + S6["cert-expiry → tls.Dial + cert.NotAfter"] + S7["ssh-script → exec.Command(ssh, host, script)"] + S8["checkResult{health, signalKind, evidence, metrics}"] + S1 --> S2 & S3 & S4 & S5 & S6 & S7 + S2 & S3 & S4 & S5 & S6 & S7 --> S8 + S8 -->|INSERT| MS[("metric_samples")] + S8 -->|UPSERT| SG["signals (dedup by target+kind)"] + S8 -->|UPSERT| ES["entity_status (health)"] + end - ┌─────────────────────────────────────────────────────────────────────┐ - │ ORIENT (Classification) │ - │ │ - │ ┌──────────────────────────────────────────────────────┐ │ - │ │ For each open signal: │ │ - │ │ │ │ - │ │ classify_by_policy(signal, entity, blast_radius) │ │ - │ │ │ │ │ - │ │ ├── read_only ───────────> route: auto_act │ │ - │ │ ├── reversible_low ──────> route: auto_act │ │ - │ │ │ (if global.auto_act=on + not in never_auto_act)│ │ - │ │ ├── config_mutation ─────> route: escalate │ │ - │ │ └── destructive ─────────> route: hold │ │ - │ │ │ │ - │ │ INSERT INTO classifications │ │ - │ │ edge: classifies → signal │ │ - │ └──────────────────────────────────────────────────────┘ │ - │ │ - │ ⚠ classification creation: schema defined, NOT yet wired │ - │ (policy.ClassifySignal exists but scheduler doesn't call it) │ - └─────────────────────────────────────────────────────────────────────┘ + subgraph ORIENT["🧭 ORIENT (Classification)"] + direction TB + C1["policy.ClassifySignal(signal, entity, blast_radius)"] + C2["read_only → route: auto_act"] + C3["reversible_low → route: auto_act"] + C4["config_mutation → route: escalate"] + C5["destructive → route: hold"] + C1 --> C2 & C3 & C4 & C5 + end - ┌─────────────────────────────────────────────────────────────────────┐ - │ DECIDE (Approval Gate) │ - │ │ - │ For route=auto_act: │ - │ skip approval, execute immediately │ - │ │ - │ For route=escalate (config_mutation): │ - │ POST /api/v1/executions ──> INSERT approval (status=pending) │ - │ notifier.go sends Matrix alert with HMAC token │ - │ operator replies ✅ or ❌ │ - │ DecideApproval() → systemctl restart / apt upgrade │ - │ │ - │ For route=hold (destructive): │ - │ queued for operator, requires explicit confirmation │ - │ (never auto-executed even with global.auto_act=on) │ - └─────────────────────────────────────────────────────────────────────┘ + subgraph DECIDE["⚖️ DECIDE (Approval Gate)"] + direction TB + D1["auto_act → execute immediately"] + D2["escalate → INSERT approval (pending)"] + D3["notifier → Matrix alert + HMAC token"] + D4["operator replies ✅ or ❌"] + D5["hold → queued, never auto-executed"] + D2 --> D3 --> D4 + end - ┌─────────────────────────────────────────────────────────────────────┐ - │ ACT (Execution) │ - │ │ - │ ┌──────────┐ request_execution ┌──────────┐ │ - │ │ Nomos │───────────────────────>│ MCP tool │ │ - │ │ (agent) │ │ server.go │ │ - │ └──────────┘ └──────────┘ │ - │ │ │ - │ ┌───────────┼───────────┐ │ - │ ▼ ▼ ▼ │ - │ reversible config_ destructive │ - │ _low mutation │ - │ │ │ │ │ - │ ▼ ▼ ▼ │ - │ immediate approval hold │ - │ execute queue (never auto) │ - │ │ │ │ - │ ▼ ▼ │ - │ actuator. Matrix │ - │ Execute() alert → │ - │ (SSH exec) operator │ - │ → approves │ - │ → actuator.Execute() │ - └─────────────────────────────────────────────────────────────────────┘ + subgraph ACT["⚡ ACT (Execution)"] + direction TB + A1["Nomos → MCP request_execution"] + A2["actuator.Execute() → SSH exec"] + A3["systemctl restart / apt upgrade / pct exec"] + A1 --> A2 --> A3 + end - ┌─────────────────────────────────────────────────────────────────────┐ - │ LEARN (Patterns + Skills) │ - │ │ - │ execution ──produces──> feedback ──contributes-to──> pattern │ - │ │ │ - │ informs │ - │ ▼ │ - │ skill │ - │ │ - │ ⚠ Schema defined, NOT yet wired: │ - │ - No code writes feedback records │ - │ - No code transitions patterns hypothesized→validated │ - │ - Skill execution against JSON procedure definitions not built │ - └─────────────────────────────────────────────────────────────────────┘ + subgraph LEARN["🧠 LEARN (Patterns + Skills)"] + direction TB + L1["execution → produces → feedback"] + L2["feedback → contributes-to → pattern"] + L3["pattern → informs → skill"] + L1 --> L2 --> L3 + end + + SG --> ORIENT + ORIENT --> DECIDE + DECIDE --> ACT + ACT --> LEARN + + style OBSERVE fill:#e3f2fd + style ORIENT fill:#fff3e0 + style DECIDE fill:#fce4ec + style ACT fill:#e8f5e9 + style LEARN fill:#f3e5f5 ``` ### What's code-real in the OODA loop @@ -253,54 +300,70 @@ layer: cognition ── the OODA loop ────────────── ## 4. Relationship Types — The Edge Catalog (34 edges) ### Infrastructure Topology -``` - host:hubris ──hosts──> lxc:jellyfin, lxc:caddy, lxc:dns, ... (machine provisions LXCs) - host:strong ──hosts──> lxc:jellyfin, lxc:arriman, ... (migrated LXCs) - host:hubris ──member-of──> cluster:homelab - host:strong ──member-of──> cluster:homelab - lxc:caddy ──provides──> service:caddy - lxc:gitea ──provides──> service:gitea - lxc:dns ──provides──> service:dns - host:hubris ──mounts──> volume:library (attrs: mount_point=/mnt/library) - host:hubris ──stores-on──> pool:library-hubris + +```mermaid +graph LR + HH["host:hubris"] -->|hosts| LX1["lxc:jellyfin"] + HH -->|hosts| LX2["lxc:caddy"] + HH -->|hosts| LX3["lxc:dns"] + HH -->|hosts| LX4["lxc:gitea"] + HH -->|hosts| LX5["lxc:…"] + HS["host:strong"] -->|hosts| LX6["lxc:jellyfin"] + HS -->|hosts| LX7["lxc:arriman"] + HH -->|member-of| CL["cluster:homelab"] + HS -->|member-of| CL + LX2 -->|provides| SV1["service:caddy"] + LX4 -->|provides| SV2["service:gitea"] + LX3 -->|provides| SV3["service:dns"] + HH -->|mounts| VL["volume:library"] + HH -->|stores-on| PL["pool:library-hubris"] ``` ### Network -``` - ingress:paperless.hubris.network ──routes-to──> service:paperless - ingress:paperless.hubris.network ──secured-by──> idp:authentik - ingress:paperless.hubris.network ──uses-certificate──> cert:*.hubris.network - service:jellyfin ──authenticates-via──> idp:authentik (OIDC) - dns:paperless ──in-zone──> zone:hubris.network - dns:paperless ──resolves-to──> lxc:caddy (caddy terminates) - host:hubris ──connects-via──> lan:lab - host:strong ──connects-via──> lan:household + +```mermaid +graph LR + IG["ingress:paperless.hubris.network"] -->|routes-to| SP["service:paperless"] + IG -->|secured-by| IDP["idp:authentik"] + IG -->|uses-certificate| CRT["cert:*.hubris.network"] + SJ["service:jellyfin"] -->|authenticates-via| IDP + DNS["dns:paperless"] -->|in-zone| ZN["zone:hubris.network"] + DNS -->|resolves-to| LX["lxc:caddy"] + HH["host:hubris"] -->|connects-via| LL["lan:lab"] + HS["host:strong"] -->|connects-via| LH["lan:household"] ``` -### Service Dependencies -``` - service:jellyfin ──depends-on──> service:authentik (OIDC auth) - service:paperless ──depends-on──> service:authentik - service:arr-stack ──depends-on──> service:jellyfin - (depends-on edges feed blast_radius() — recursive CTE) +### Service Dependencies (feeds blast_radius CTE) + +```mermaid +graph LR + JF["service:jellyfin"] -->|depends-on| AK["service:authentik"] + PP["service:paperless"] -->|depends-on| AK + AR["service:arr-stack"] -->|depends-on| JF + AK -->|depends-on| CD["service:caddy"] + AK -->|depends-on| DNS["service:dns"] ``` ### Cognition (OODA edges) -``` - check:ssh-script:d419257d ──checks──> host:hubris - check:ssh-script:d419257d ──raises──> signal:cpu-pressure (when unhealthy) - signal:cpu-pressure ──about──> host:hubris - classification:xyz ──classifies──> signal:cpu-pressure - classification:xyz ──precedes──> execution:restart-xyz - execution:restart-xyz ──targets──> host:hubris - execution:restart-xyz ──performs──> agent:nomos + +```mermaid +graph LR + CK["check:ssh-script:d419257d"] -->|checks| HH["host:hubris"] + CK -->|raises| SG["signal:cpu-pressure"] + SG -->|about| HH + CL["classification:xyz"] -->|classifies| SG + CL -->|precedes| EX["execution:restart-xyz"] + EX -->|targets| HH + EX -->|performs| AG["agent:nomos"] ``` ### Governance -``` - person:dtoro ──owns──> agent:nomos - person:dtoro ──decides──> approval:xyz - idp:authentik ──authenticates──> person:dtoro + +```mermaid +graph LR + DT["person:dtoro"] -->|owns| NO["agent:nomos"] + DT -->|decides| AP["approval:xyz"] + AK["idp:authentik"] -->|authenticates| DT ``` --- @@ -308,18 +371,30 @@ layer: cognition ── the OODA loop ────────────── ## 5. Lifecycle State Machines ### Infrastructure (15 concrete types use this) -``` - planned ──> provisioning ──> active ──> migrating ──> active - │ │ │ │ - │ │ └── failed ──┘ - │ │ └── deprecated ──> destroyed - │ │ - │ └── failed ──> active (recovery) - │ - └── destroyed (cancelled) - Terminal: [destroyed] - Default: active +```mermaid +stateDiagram-v2 + [*] --> planned + planned --> provisioning + planned --> destroyed: cancelled + + provisioning --> active + provisioning --> failed + + active --> migrating + active --> failed + active --> deprecated + + migrating --> active: post-verify + migrating --> failed + + failed --> active: recovery verified + failed --> deprecated: write-off + + deprecated --> active: un-deprecate + deprecated --> destroyed + + destroyed --> [*] ``` **Real precondition checks** (code in `impl.go:1494-1579`): @@ -336,15 +411,27 @@ layer: cognition ── the OODA loop ────────────── **Soft preconditions** (always pass — operator-confirmed): `inventory-entry`, `ip-reserved`, `preflight-passed`, `backup-verified`, `replacement-live`, `caddy-backends-checked`, `un-deprecate-note`, etc. ### Signal -``` - raised ──> acknowledged ──> acting ──> resolved - │ │ │ - ├── muted ├── muted ├── raised (retry budget) - │ │ │ - └── resolved└── resolved └── failed ──> acknowledged (operator-retry) - Terminal: [resolved] - Default: raised +```mermaid +stateDiagram-v2 + [*] --> raised + raised --> acknowledged + raised --> muted: mute_until set + raised --> resolved: condition cleared + + acknowledged --> acting: classification exists + acknowledged --> muted + acknowledged --> resolved + + acting --> resolved: verification passed + acting --> raised: retry budget + acting --> failed + + failed --> acknowledged: operator retry + + muted --> raised: mute_until expired + + resolved --> [*] ``` **Implemented preconditions:** @@ -353,6 +440,49 @@ layer: cognition ── the OODA loop ────────────── **Dedup mechanism:** `UNIQUE INDEX uq_signals_open ON signals(target_entity_id, kind) WHERE state NOT IN ('resolved','failed')` — at most one open signal per (entity, kind). Repeated failures call `UpsertSignal` which increments `occurrence_count` on the existing row. +### Execution + +```mermaid +stateDiagram-v2 + [*] --> proposed + proposed --> approved + proposed --> auto_approved + proposed --> denied + proposed --> expired + + approved --> executing + auto_approved --> executing + expired --> [*] + denied --> [*] + + executing --> verified + executing --> failed + executing --> timed_out + + failed --> rolled_back + rolled_back --> verified + rolled_back --> rollback_failed + + verified --> [*] + rollback_failed --> [*] + timed_out --> [*] +``` + +### Approval + +```mermaid +stateDiagram-v2 + [*] --> pending + pending --> approved + pending --> denied + approved --> revoked + approved --> expired + + denied --> [*] + revoked --> [*] + expired --> [*] +``` + --- ## 6. What's Code-Real vs Schema-Only @@ -385,9 +515,9 @@ layer: cognition ── the OODA loop ────────────── | Component | What's Missing | |-----------|---------------| -| `classifications` auto-creation | `policy.ClassifySignal()` exists but scheduler never calls it. Signals are raised but never automatically classified. The `GetOpenSignalsForAutoAct` query would return signals with auto-act classification, but the classify step is manual-only. | +| `classifications` auto-creation | `policy.ClassifySignal()` exists but scheduler never calls it. Signals are raised but never automatically classified. | | `feedback` records | No code writes to the `feedback` table. Execution results are not analyzed for patterns. | -| Pattern auto-learning | No code transitions patterns from `hypothesized → validated`. The lifecycle requires `evidence-count≥5 + confidence≥0.7` but no aggregation runs. | +| Pattern auto-learning | No code transitions patterns from `hypothesized → validated`. Requires `evidence-count≥5 + confidence≥0.7` but no aggregation runs. | | Skill execution | Skill entities carry a JSON `procedure` field but no execution engine reads or runs it. | | `drift` check kind | Defined in OpenAPI and `check_defs.kind` enum, but no scheduler implementation exists. | @@ -401,77 +531,74 @@ layer: cognition ── the OODA loop ────────────── --- -## 7. Database Physical Schema (Key Tables) +## 7. Database Physical Schema -``` - entity_types ──FK──> lifecycle_defs - │ - │ FK (entities.type) - ▼ - entities ──FK──> entity_types - │ - ├──FK──> entity_status (dual) - ├──FK──> check_defs (dual; check_defs.target_id → entities) - ├──FK──> signals (dual; signals.target_entity_id → entities) - ├──FK──> classifications (dual) - ├──FK──> executions (dual; executions.target_entity_id → entities) - ├──FK──> feedback (dual) - ├──FK──> patterns (dual) - ├──FK──> skills (dual) - ├──FK──> approvals (dual; approvals.subject_entity_id → entities) - ├──FK──> knowledge_entities (dual) - └──>→ relationships (source_id, target_id → entities) +```mermaid +erDiagram + lifecycle_defs ||--o{ entity_types : "lifecycle_id FK" + entity_types ||--o{ entities : "type FK" + entity_types ||--o| entity_types : "parent_type FK (self-ref)" - relationship_types ──FK──> entity_types (source_type, target_type) - │ - │ FK (relationships.type) - ▼ - relationships ──FK──> entities (source_id, target_id) - │ - └── unique index: (source_id, target_id, type) WHERE valid_to IS NULL + entities ||--o| entity_status : "dual entity (shared PK)" + entities ||--o| check_defs : "dual entity (shared PK)" + entities ||--o| signals : "dual entity (shared PK)" + entities ||--o| classifications : "dual entity (shared PK)" + entities ||--o| executions : "dual entity (shared PK)" + entities ||--o| feedback : "dual entity (shared PK)" + entities ||--o| patterns : "dual entity (shared PK)" + entities ||--o| skills : "dual entity (shared PK)" + entities ||--o| approvals : "dual entity (shared PK)" + entities ||--o| knowledge_entities : "dual entity (shared PK)" - approval_rules ──FK──> entity_types (entity_type) - autonomy_settings (key/value, no FKs) - risk_classes (standalone) + entities ||--o{ relationships : "source_id FK" + entities ||--o{ relationships : "target_id FK" - metric_samples (TimescaleDB hypertable — ts dimension) - events (TimescaleDB hypertable — ts dimension, pg_notify trigger for SSE) - audit_log (TimescaleDB hypertable — ts dimension) - agent_activity (TimescaleDB hypertable — ts dimension) + check_defs }o--|| entities : "target_id FK" + signals }o--|| entities : "target_entity_id FK" + executions }o--|| entities : "target_entity_id FK" + approvals }o--|| entities : "subject_entity_id FK" + + entity_types ||--o{ relationship_types : "source_type FK" + entity_types ||--o{ relationship_types : "target_type FK" + relationship_types ||--o{ relationships : "type FK" + + entity_types ||--o{ approval_rules : "entity_type FK" + + signals ||--o| check_defs : "check_id FK" ``` **Key architectural patterns:** - **Dual entities:** `check_defs`, `signals`, `classifications`, `executions`, `feedback`, `patterns`, `skills`, `approvals`, `knowledge_entities` — all have `entity_id UUID PK REFERENCES entities(id)`. Every row is also an entity. - **Partial unique indexes:** `relationships` (current edges), `signals` (open signals), `patterns` (per-type action) — all use `WHERE` clauses for snapshot semantics. -- **TimescaleDB:** 4 hypertables with continuous aggregates and retention policies. +- **TimescaleDB hypertables:** `metric_samples`, `events`, `audit_log`, `agent_activity` — with continuous aggregates and retention policies. - **SSE fan-out:** `pg_notify('oikos_events', ...)` trigger on `events` INSERT → Go listener fan-out → SSE connections. --- ## 8. How Nomos Queries Thermals — End-to-End Trace -``` - User: "what are the thermals of hubris?" - │ - ▼ - Nomos calls MCP: query_metrics(metric=["cpu_pct","cpu_temp"]) - │ - ▼ - server.go:getMetricHistory() - │ - ▼ - SELECT time_bucket('1h', ts) AS bucket, - avg(value), min(value), max(value) - FROM metric_samples - WHERE metric IN ('cpu_pct', 'cpu_temp') - AND entity_id = (SELECT id FROM entities WHERE slug = 'host:hubris') - GROUP BY bucket - │ - ▼ - Returns: cpu_pct ≈ 15%, cpu_temp ≈ 48°C (from TimescaleDB continuous aggregate) - │ - ▼ - Nomos formats and presents results to user +```mermaid +sequenceDiagram + participant U as User + participant N as Nomos (Agent) + participant A as Oikos API + participant T as TimescaleDB + participant S as Scheduler + participant H as Hubris + + Note over S,H: Autonomous collection (every 60s) + S->>H: SSH exec cpu_check.sh + H-->>S: {"metrics":{"cpu_pct":15,"cpu_temp":48}} + S->>T: INSERT metric_samples (cpu_pct, cpu_temp) + + Note over U,N: User asks question + U->>N: "what are the thermals of hubris?" + N->>A: MCP query_metrics(metric=["cpu_pct","cpu_temp"]) + A->>T: SELECT time_bucket('1h', ts) … FROM metric_samples + T-->>A: {avg:15, min:2, max:40} (cpu_pct) + T-->>A: {avg:48, min:42, max:85} (cpu_temp) + A-->>N: time-bucketed metrics + N-->>U: "CPU at 15%, temp 48°C — normal range" ``` **What made this possible (chronologically):**