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

@@ -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):**