adr: convert all diagrams to Mermaid (sequenceDiagram, stateDiagram-v2, flowchart, erDiagram, graph)
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

0013-signal-triggers.md:
- Thermals query: sequenceDiagram (Nomos→API→Scheduler→Hubris→TimescaleDB)
- Script deployment: sequenceDiagram
- Signal lifecycle: stateDiagram-v2
- DB data flow: flowchart

0014-entity-model.md:
- Entity type hierarchy: graph (56 types, 3 layers, 7 domains)
- Machine onboarding: sequenceDiagram
- OODA loop (5 phases): flowchart with color-coded subgraphs
- Infrastructure topology: graph
- Network relationships: graph
- Service dependencies: graph
- Cognition OODA edges: graph
- Governance: graph
- Infrastructure lifecycle: stateDiagram-v2
- Signal lifecycle: stateDiagram-v2
- Execution lifecycle: stateDiagram-v2
- Approval lifecycle: stateDiagram-v2
- DB physical schema: erDiagram
- Thermals query trace: sequenceDiagram
This commit is contained in:
2026-07-08 22:38:19 +02:00
parent 551497e0b3
commit a39e67b6e9
2 changed files with 497 additions and 379 deletions

View File

@@ -4,40 +4,28 @@
When Nomos is asked "what are the thermals of hubris?", here is exactly what happens: When Nomos is asked "what are the thermals of hubris?", here is exactly what happens:
``` ```mermaid
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ sequenceDiagram
Nomos │ │ Oikos │ │Scheduler │ │ Hubris │ participant N as Nomos (Agent)
│ (Agent) │ │ API │ │ (Docker) │ │(Proxmox) │ participant A as Oikos API
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ participant S as Scheduler (Docker)
│ │ │ │ participant H as Hubris (Proxmox)
│ query_metrics │ │ │ participant T as TimescaleDB
│────────────────>│ │ │
│ │ │ │ Note over S,H: Every 60s (autonomous loop)
│ ← cpu_pct=2.5 │ SELECT FROM │ │ S->>H: SSH exec /opt/oikos/checks/cpu_check.sh
│ cpu_temp=48 │ metric_samples│ │ 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
══════ Every 60s (autonomous loop) ══════ │ S->>T: UPSERT signal (dedup by target+kind)
│ │ │ │ end
│ │ │ SSH exec │
│ │ │─────────────────>│ Note over N,T: User asks "what are the thermals of hubris?"
│ │ │ /opt/oikos/ │ N->>A: MCP query_metrics(metric=["cpu_pct","cpu_temp"])
│ │ │ checks/ │ A->>T: SELECT time_bucket(…) FROM metric_samples
│ │ │ cpu_check.sh │ T-->>A: cpu_pct=15%, cpu_temp=48°C
│ │ │ │ A-->>N: {avg, min, max} per bucket
│ │ │ {"health":"ok", │
│ │ │ "metrics": │
│ │ │ {"cpu_pct":2.5, │
│ │ │ "cpu_temp":48}}│
│ │ │<─────────────────│
│ │ │ │
│ │ │ INSERT │
│ │ │ metric_samples │
│ │ │ │
│ │ │ UPSERT signal │
│ │ │ (dedup) │
│ │ │ │
``` ```
## Two Paths ## Two Paths
@@ -105,22 +93,18 @@ or on failure:
## Script Deployment ## Script Deployment
``` ```mermaid
Git Push Sync Timer (5min) Target Host sequenceDiagram
┌────────┐ ┌────────────────┐ ┌──────────┐ participant R as Git Repo
│ git push│ │ git pull │ │ │ participant T as Sync Timer (5min)
│ origin │───────────────>│ homelab-context│ │ │ participant H as Target Host
│ main │ │ │ │ │
└────────┘ │ post-pull.sh │ │ │ Note over R,T: Operator pushes scripts
│ → tools/ │ │ │ R->>T: git pull (homelab-context)
│ setup- │ │ │ T->>T: tools/post-pull.sh
│ checks.sh │ │ │ T->>T: → tools/setup-checks.sh
│ → checks/ │ │ │ T->>T: → checks/install.sh
│ install.sh│ │ │ T->>H: cp *.sh → /opt/oikos/checks/
│ │──cp *.sh ─>│ /opt/ │
│ │ │ oikos/ │
│ │ │ checks/ │
└────────────────┘ └──────────┘
``` ```
## Defining a Check ## Defining a Check
@@ -145,12 +129,26 @@ curl -X POST http://oikos:8090/api/v1/checks \
## Signal Lifecycle ## Signal Lifecycle
``` ```mermaid
raised ──> acknowledged ──> acting ──> resolved stateDiagram-v2
│ │ │ [*] --> raised
├── muted ├── muted ├── raised (retry) raised --> acknowledged
│ │ │ raised --> muted: mute_until set
└── resolved └── resolved └── failed 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)**. Signals deduplicate: **one open signal per (target_entity_id, kind)**.
@@ -177,24 +175,17 @@ Severity mapping:
## Data Flow (DB Tables) ## Data Flow (DB Tables)
``` ```mermaid
check_defs ──(scheduler reads)──> executeCheck() flowchart TD
│ │ CD[check_defs] -->|scheduler reads| EC[executeCheck]
│ ├── healthy?resolve signal, upsert entity_status EC -->|healthy?| RS[resolve signal + upsert entity_status]
│ │ EC -->|unhealthy?| US[UpsertSignal dedup by target+kind]
│ └── unhealthy? → UpsertSignal(), insert metric_samples EC -->|every cycle| IM[INSERT metric_samples]
US --> S[signals]
RS --> ES[entity_status]
signals ◄──── UpsertSignal (dedup by target+kind) IM --> MS[(metric_samples)]
MS --> R1H[metric_rollups_1h continuous aggregate]
MS --> R1D[metric_rollups_1d continuous aggregate]
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
``` ```
## Prerequisites for SSH Checks ## Prerequisites for SSH Checks

View File

@@ -9,52 +9,162 @@ cognition pipeline — with clear markers for what is **code-real** vs **schema-
## 1. Entity Type Hierarchy (56 types) ## 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 ────────────────────────────────────────────────── ★ = abstract (cannot be instantiated; polymorphic target for relationships)
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)
```
### Concrete instances (88 active entities) ### Concrete instances (88 active entities)
@@ -76,45 +186,26 @@ layer: cognition ── the OODA loop ──────────────
## 2. Core Sequence: Machine Onboarding ## 2. Core Sequence: Machine Onboarding
``` ```mermaid
Operator Oikos API DB Scheduler Target Machine sequenceDiagram
┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ participant O as Operator
│ POST │ │ │ │ │ │ │ │ │ participant A as Oikos API
│/entities│────>│Create │ │ │ │ │ │ │ participant D as DB
│ │ │Entity() │ │ │ │ │ │ │ participant S as Scheduler
│ │ │ │────>│INSERT │ │ │ │ │ participant T as Target Machine
│ │ │ │ │entities │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ O->>A: POST /api/v1/entities {type:proxmox-host, slug:host:new, …}
│ │ensure │ │ │ │ │ │ │ A->>D: INSERT INTO entities
│ │Default │────>│INSERT │ │ │ │ │ A->>A: ensureDefaultChecks().resolveHost() → lan_ip
│ │ │Checks() │ │check_defs│ │ │ │ │ A->>D: INSERT check_defs × 6 (target_id set)
│ │→ ping │ │×6 │ │ │ │ │ A-->>O: 201 Created
│ │ │→ cpu │ │ │ │ │ │ │
│ │→ memory │ │ │ │ │ │ │ Note over S,T: 30s scheduler tick
│ │→ load │ │(target_id│ │ │ │ │ S->>D: ListEnabledCheckDefs
│ │ │→ disk │ │ set) │ │ │ │ │ S->>T: SSH exec /opt/oikos/checks/cpu_check.sh
│ │→ updates │ │ │ │ │ │ │ T-->>S: {"health":"ok","metrics":{"cpu_pct":2.5,"cpu_temp":48}}
│ │ │ │ │ │ │ │ │ S->>D: INSERT metric_samples
│ │<────│201 │ │ │ │ │ │ │ S->>D: UPSERT entity_status (health)
│ │ │Created │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ ── 30s tick ─>│ │ │
│ │ │ │ │ │ loads │ │ │
│ │ │ │ │ │ check_defs │ │ │
│ │ │ │ │ │ │──SSH────>│ │
│ │ │ │ │ │ │ /opt/ │ │
│ │ │ │ │ │ │ oikos/ │ │
│ │ │ │ │ │ │ checks/ │ │
│ │ │ │ │ │ │ cpu.sh │ │
│ │ │ │ │ │ │<──JSON───│ │
│ │ │ │ │<─────────│INSERT │ │ │
│ │ │ │ │metric │metric_samples │ │ │
│ │ │ │ │samples │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │<─────────│UPSERT │ │ │
│ │ │ │ │entity │entity_status │ │ │
│ │ │ │ │status │(health) │ │ │
│ │ │ │ │ │ │ │ │
``` ```
**What's code-real here:** **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)
``` ```mermaid
┌─────────────────────────────────────────────────────────────────────┐ flowchart TB
│ OBSERVE (Scheduler) │ subgraph OBSERVE["🔍 OBSERVE (Scheduler every 30s)"]
│ │ direction TB
Every 30s: │ S1["ListEnabledCheckDefs"]
┌──────────┐ ListEnabledCheckDefs ┌──────────┐ │ S2["ping → exec.Command(ping, host)"]
│scheduler │─────────────────────────>│ Postgres │ │ S3["http → http.Get(url)"]
│.go:54 │ │ │ │ S4["tcp → net.DialTimeout(tcp, addr)"]
│ └──────────┘ └──────────┘ │ S5["disk → unix.Statfs(path)"]
│ │ S6["cert-expiry → tls.Dial + cert.NotAfter"]
├── ping ──> exec.Command("ping", host) │ S7["ssh-script → exec.Command(ssh, host, script)"]
├── http ──> http.Get(url) │ S8["checkResult{health, signalKind, evidence, metrics}"]
├── tcp ──> net.DialTimeout("tcp", addr) │ S1 --> S2 & S3 & S4 & S5 & S6 & S7
├── disk ──> unix.Statfs(path) │ S2 & S3 & S4 & S5 & S6 & S7 --> S8
├── cert-expiry ──> tls.Dial + cert.NotAfter │ S8 -->|INSERT| MS[("metric_samples")]
└── ssh-script ──> exec.Command("ssh", host, script) │ S8 -->|UPSERT| SG["signals (dedup by target+kind)"]
│ │ │ S8 -->|UPSERT| ES["entity_status (health)"]
│ ┌───────┘ │ end
│ ▼ │
│ ┌─────────────┐ │
│ │ checkResult │ {health, signalKind, evidence, metrics}│
│ └─────────────┘ │
│ │ │
│ ┌──────────┼──────────┐ │
│ ▼ ▼ ▼ │
│ metric_samples signals entity_status │
│ INSERT UPSERT UPSERT │
│ (every cycle) (dedup by (health + last_check_at) │
│ target+kind) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ subgraph ORIENT["🧭 ORIENT (Classification)"]
│ ORIENT (Classification) │ direction TB
│ │ C1["policy.ClassifySignal(signal, entity, blast_radius)"]
┌──────────────────────────────────────────────────────┐ │ C2["read_only → route: auto_act"]
│ For each open signal: │ │ C3["reversible_low → route: auto_act"]
│ │ C4["config_mutation → route: escalate"]
classify_by_policy(signal, entity, blast_radius) │ │ C5["destructive → route: hold"]
│ │ │ │ │ C1 --> C2 & C3 & C4 & C5
│ ├── read_only ───────────> route: auto_act │ │ end
│ │ ├── 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 DECIDE["⚖️ DECIDE (Approval Gate)"]
│ DECIDE (Approval Gate) │ direction TB
│ │ D1["auto_act → execute immediately"]
│ For route=auto_act: │ D2["escalate → INSERT approval (pending)"]
skip approval, execute immediately │ D3["notifier → Matrix alert + HMAC token"]
│ │ D4["operator replies ✅ or ❌"]
For route=escalate (config_mutation): │ D5["hold → queued, never auto-executed"]
POST /api/v1/executions ──> INSERT approval (status=pending) │ D2 --> D3 --> D4
notifier.go sends Matrix alert with HMAC token │ end
│ 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 ACT["⚡ ACT (Execution)"]
│ ACT (Execution) │ direction TB
│ │ A1["Nomos → MCP request_execution"]
┌──────────┐ request_execution ┌──────────┐ │ A2["actuator.Execute() → SSH exec"]
│ Nomos │───────────────────────>│ MCP tool │ │ A3["systemctl restart / apt upgrade / pct exec"]
│ │ (agent) │ │ server.go │ │ A1 --> A2 --> A3
│ └──────────┘ └──────────┘ │ end
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ reversible config_ destructive │
│ _low mutation │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ immediate approval hold │
│ execute queue (never auto) │
│ │ │ │
│ ▼ ▼ │
│ actuator. Matrix │
│ Execute() alert → │
│ (SSH exec) operator │
│ → approves │
│ → actuator.Execute() │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐ subgraph LEARN["🧠 LEARN (Patterns + Skills)"]
│ LEARN (Patterns + Skills) │ direction TB
│ │ L1["execution → produces → feedback"]
execution ──produces──> feedback ──contributes-to──> pattern L2["feedback contributes-to pattern"]
│ │ │ L3["pattern → informs → skill"]
│ informs │ L1 --> L2 --> L3
│ ▼ │ end
│ skill │
│ │ SG --> ORIENT
⚠ Schema defined, NOT yet wired: │ ORIENT --> DECIDE
- No code writes feedback records │ DECIDE --> ACT
│ - No code transitions patterns hypothesized→validated │ ACT --> LEARN
│ - Skill execution against JSON procedure definitions not built │
└─────────────────────────────────────────────────────────────────────┘ 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 ### 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) ## 4. Relationship Types — The Edge Catalog (34 edges)
### Infrastructure Topology ### Infrastructure Topology
```
host:hubris ──hosts──> lxc:jellyfin, lxc:caddy, lxc:dns, ... (machine provisions LXCs) ```mermaid
host:strong ──hosts──> lxc:jellyfin, lxc:arriman, ... (migrated LXCs) graph LR
host:hubris ──member-of──> cluster:homelab HH["host:hubris"] -->|hosts| LX1["lxc:jellyfin"]
host:strong ──member-of──> cluster:homelab HH -->|hosts| LX2["lxc:caddy"]
lxc:caddy ──provides──> service:caddy HH -->|hosts| LX3["lxc:dns"]
lxc:gitea ──provides──> service:gitea HH -->|hosts| LX4["lxc:gitea"]
lxc:dns ──provides──> service:dns HH -->|hosts| LX5["lxc:…"]
host:hubris ──mounts──> volume:library (attrs: mount_point=/mnt/library) HS["host:strong"] -->|hosts| LX6["lxc:jellyfin"]
host:hubris ──stores-on──> pool:library-hubris 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 ### Network
```
ingress:paperless.hubris.network ──routes-to──> service:paperless ```mermaid
ingress:paperless.hubris.network ──secured-by──> idp:authentik graph LR
ingress:paperless.hubris.network ──uses-certificate──> cert:*.hubris.network IG["ingress:paperless.hubris.network"] -->|routes-to| SP["service:paperless"]
service:jellyfin ──authenticates-via──> idp:authentik (OIDC) IG -->|secured-by| IDP["idp:authentik"]
dns:paperless ──in-zone──> zone:hubris.network IG -->|uses-certificate| CRT["cert:*.hubris.network"]
dns:paperless ──resolves-to──> lxc:caddy (caddy terminates) SJ["service:jellyfin"] -->|authenticates-via| IDP
host:hubris ──connects-via──> lan:lab DNS["dns:paperless"] -->|in-zone| ZN["zone:hubris.network"]
host:strong ──connects-via──> lan:household 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 Dependencies (feeds blast_radius CTE)
```
service:jellyfin ──depends-on──> service:authentik (OIDC auth) ```mermaid
service:paperless ──depends-on──> service:authentik graph LR
service:arr-stack ──depends-on──> service:jellyfin JF["service:jellyfin"] -->|depends-on| AK["service:authentik"]
(depends-on edges feed blast_radius() — recursive CTE) 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) ### Cognition (OODA edges)
```
check:ssh-script:d419257d ──checks──> host:hubris ```mermaid
check:ssh-script:d419257d ──raises──> signal:cpu-pressure (when unhealthy) graph LR
signal:cpu-pressure ──about──> host:hubris CK["check:ssh-script:d419257d"] -->|checks| HH["host:hubris"]
classification:xyz ──classifies──> signal:cpu-pressure CK -->|raises| SG["signal:cpu-pressure"]
classification:xyz ──precedes──> execution:restart-xyz SG -->|about| HH
execution:restart-xyz ──targets──> host:hubris CL["classification:xyz"] -->|classifies| SG
execution:restart-xyz ──performs──> agent:nomos CL -->|precedes| EX["execution:restart-xyz"]
EX -->|targets| HH
EX -->|performs| AG["agent:nomos"]
``` ```
### Governance ### Governance
```
person:dtoro ──owns──> agent:nomos ```mermaid
person:dtoro ──decides──> approval:xyz graph LR
idp:authentik ──authenticates──> person:dtoro 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 ## 5. Lifecycle State Machines
### Infrastructure (15 concrete types use this) ### Infrastructure (15 concrete types use this)
```
planned ──> provisioning ──> active ──> migrating ──> active
│ │ │ │
│ │ └── failed ──┘
│ │ └── deprecated ──> destroyed
│ │
│ └── failed ──> active (recovery)
└── destroyed (cancelled)
Terminal: [destroyed] ```mermaid
Default: active 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`): **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. **Soft preconditions** (always pass — operator-confirmed): `inventory-entry`, `ip-reserved`, `preflight-passed`, `backup-verified`, `replacement-live`, `caddy-backends-checked`, `un-deprecate-note`, etc.
### Signal ### Signal
```
raised ──> acknowledged ──> acting ──> resolved
│ │ │
├── muted ├── muted ├── raised (retry budget)
│ │ │
└── resolved└── resolved └── failed ──> acknowledged (operator-retry)
Terminal: [resolved] ```mermaid
Default: raised 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:** **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. **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 ## 6. What's Code-Real vs Schema-Only
@@ -385,9 +515,9 @@ layer: cognition ── the OODA loop ──────────────
| Component | What's Missing | | 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. | | `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. | | 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. | | `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
``` ```mermaid
entity_types ──FK──> lifecycle_defs erDiagram
lifecycle_defs ||--o{ entity_types : "lifecycle_id FK"
│ FK (entities.type) entity_types ||--o{ entities : "type FK"
entity_types ||--o| entity_types : "parent_type FK (self-ref)"
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)
relationship_types ──FK──> entity_types (source_type, target_type) entities ||--o| entity_status : "dual entity (shared PK)"
entities ||--o| check_defs : "dual entity (shared PK)"
│ FK (relationships.type) entities ||--o| signals : "dual entity (shared PK)"
entities ||--o| classifications : "dual entity (shared PK)"
relationships ──FK──> entities (source_id, target_id) entities ||--o| executions : "dual entity (shared PK)"
entities ||--o| feedback : "dual entity (shared PK)"
└── unique index: (source_id, target_id, type) WHERE valid_to IS NULL 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) entities ||--o{ relationships : "source_id FK"
autonomy_settings (key/value, no FKs) entities ||--o{ relationships : "target_id FK"
risk_classes (standalone)
metric_samples (TimescaleDB hypertable — ts dimension) check_defs }o--|| entities : "target_id FK"
events (TimescaleDB hypertable — ts dimension, pg_notify trigger for SSE) signals }o--|| entities : "target_entity_id FK"
audit_log (TimescaleDB hypertable — ts dimension) executions }o--|| entities : "target_entity_id FK"
agent_activity (TimescaleDB hypertable — ts dimension) 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:** **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. - **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. - **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. - **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 ## 8. How Nomos Queries Thermals — End-to-End Trace
``` ```mermaid
User: "what are the thermals of hubris?" sequenceDiagram
participant U as User
participant N as Nomos (Agent)
Nomos calls MCP: query_metrics(metric=["cpu_pct","cpu_temp"]) participant A as Oikos API
participant T as TimescaleDB
participant S as Scheduler
server.go:getMetricHistory() participant H as Hubris
Note over S,H: Autonomous collection (every 60s)
SELECT time_bucket('1h', ts) AS bucket, S->>H: SSH exec cpu_check.sh
avg(value), min(value), max(value) H-->>S: {"metrics":{"cpu_pct":15,"cpu_temp":48}}
FROM metric_samples S->>T: INSERT metric_samples (cpu_pct, cpu_temp)
WHERE metric IN ('cpu_pct', 'cpu_temp')
AND entity_id = (SELECT id FROM entities WHERE slug = 'host:hubris') Note over U,N: User asks question
GROUP BY bucket 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
Returns: cpu_pct ≈ 15%, cpu_temp ≈ 48°C (from TimescaleDB continuous aggregate) 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
Nomos formats and presents results to user N-->>U: "CPU at 15%, temp 48°C — normal range"
``` ```
**What made this possible (chronologically):** **What made this possible (chronologically):**