adr: convert all diagrams to Mermaid (sequenceDiagram, stateDiagram-v2, flowchart, erDiagram, graph)
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:
@@ -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
|
||||
|
||||
@@ -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):**
|
||||
|
||||
Reference in New Issue
Block a user