Documentation and repo-hygiene pass following the client/server split:
Plan drift (audited all other active plans against current code):
- oikos-gaps-and-improvements.md: mark Section C and D.5 resolved (both
described cmd/hermes, renamed to cmd/nomos with a real LLM loop since);
refresh ~10 stale file:line citations; fix tool-count (33, not 28).
- liveness-drift-and-ux-cohesion.md: fix stale default-model claim (now
deepseek-v4-pro since 2026-07-10) and "not yet deployed" status.
- nomos-agent-code-review.md: fix C1's citation (one unauthenticated route
to nomos now, not two, after the client/server split).
- wails-desktop-app.md: record the production deploy outcome.
Repo structure: added missing directories to README/CONTRIBUTING layout
tables (checks/, tools/, cmd/webhook/, docs/operations/), fixed a broken
link, added ADR 0015 documenting the auth/CORS/client-split model (there
wasn't one despite CONTRIBUTING's own process requiring it), normalized
ADR 0013/0014's format drift, added an Authentication section to
AGENTS.md/CLIENTS.md (every example call was missing the now-required
bearer header).
Retired the Goose+Nomos workstation flow (bootstrap.sh --with-nomos,
tools/setup-nomos-soul.sh, .agents/operations/nomos-agent.md) and the
Caveman auto-install tooling (tools/setup-caveman.sh, tools/caveman/) —
both superseded by the production containerized Nomos agent, which has
never used either. Kept .agents/shared/caveman.md itself (the terse
writing-style convention agents still follow by reading it).
Deleted the orphaned legacy Python oikos/ directory — nothing imports it,
and bin/homelab (the CLI it was kept for) no longer exists in the repo.
Rewrote .agents/operations/agent-enrollment.md (365 -> ~110 lines) and
commands.md to match the current architecture instead of the retired
`homelab` CLI; migrated the still-true networking prerequisites (Netbird,
split-horizon DNS, SSH key distribution) into the knowledge base as a
runbook via upsert_knowledge rather than duplicating them in markdown.
Updated all 10 .agents/skills/ runbooks referencing the dead CLI with
their real MCP tool / REST API equivalents, or flagged them as needing
verification where no equivalent is confirmed yet.
Two real bugs found and fixed, not just docs:
- The tools/setup-*.sh auto-setup glob was tools/*.setup.sh in THREE
places (tools/post-pull.sh, bootstrap.sh, and internal/httpapi/impl.go's
GetClientContext handler) since the mechanism's introduction on
2026-06-02 — never matched any real filename, so no client has ever
picked up an auto-setup script via git-pull or the context-poller sync.
Fixed all three; the Go server-side fix is the one that actually matters
since it's what the current context-poller mechanism depends on.
- bootstrap.sh removed dead vestigial --gitea-token/--gitea-user flags
(parsed, never consumed) left over from an earlier clone-based model.
Also flagged, not fixed (documented as an open gap in
client-enrollment/SKILL.md): bootstrap.sh tells a freshly-enrolled client
to call POST /api/v1/clients/{slug}/activate to finish enrollment, but
that route doesn't exist in api/openapi.yaml — EnrollClient sets entities
to provisioning and nothing currently transitions them to active.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
614 lines
20 KiB
Markdown
614 lines
20 KiB
Markdown
# ADR 0014 — Entity model: types, relationships & interactions
|
||
|
||
**Status:** Accepted
|
||
**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**.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
★ = abstract (cannot be instantiated; polymorphic target for relationships)
|
||
|
||
### Concrete instances (88 active entities)
|
||
|
||
| Type | Count | Examples |
|
||
|------|-------|---------|
|
||
| `lxc` | 19 | jellyfin, caddy, dns, gitea, nextcloud, matrix, arriman… |
|
||
| `service` | 25 | caddy, authentik, dns, jellyfin, paperless, matrix… |
|
||
| `ingress-route` | 21 | *.hubris.network |
|
||
| `config-repo` | 6 | caddy-conf, gitea-customizations, mule-image… |
|
||
| `proxmox-host` | 2 | hubris, strong |
|
||
| `workstation` | 2 | mac-mini, republic-laptop |
|
||
| `standalone-server` | 1 | netbird-vps |
|
||
| `vm` | 2 | zimaos, haos |
|
||
| `storage-pool` | 3 | local-lvm-hubris, library-hubris, ludo-lvm |
|
||
| `volume` | 2 | library, media-local |
|
||
| + sites, networks, agents, documents, destroyed… | | |
|
||
|
||
---
|
||
|
||
## 2. Core Sequence: Machine Onboarding
|
||
|
||
```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:**
|
||
- `CreateEntity()` at `internal/httpapi/impl.go:811` — handles POST, validates type, calls `ensureDefaultChecks()`
|
||
- `ensureDefaultChecks()` → `internal/checkdefaults/defaults.go:144` — resolves host IP, SSH user, creates 6 check_defs rows with target_id
|
||
- Scheduler at `internal/scheduler/scheduler.go:26` — loads `ListEnabledCheckDefs`, dispatches by kind, writes metrics + signals
|
||
|
||
---
|
||
|
||
## 3. Core Sequence: The OODA Loop (observe → orient → decide → act → learn)
|
||
|
||
```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
|
||
|
||
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
|
||
|
||
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
|
||
|
||
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
|
||
|
||
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
|
||
|
||
| Phase | Table | Code | Status |
|
||
|-------|-------|------|--------|
|
||
| Observe | `check_defs`, `metric_samples` | `scheduler.go:26-215` | ✅ fully wired, 6 probe kinds |
|
||
| Observe → Orient | `signals` | `scheduler.go:130-141` (UpsertSignal) | ✅ dedup, severity, events |
|
||
| Orient | `classifications` | `policy/classify.go` (function exists) | ⚠ function defined but scheduler never calls it |
|
||
| Decide | `approvals` | `server.go:311-367` (request_execution) | ✅ escalation gate works |
|
||
| Act | `executions` | `actuator/exec.go` (SSH exec) | ✅ systemctl, apt, pct |
|
||
| Learn | `feedback`, `patterns`, `skills` | tables + list endpoints only | ⚠ schema only, no write path |
|
||
|
||
---
|
||
|
||
## 4. Relationship Types — The Edge Catalog (34 edges)
|
||
|
||
### Infrastructure Topology
|
||
|
||
```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
|
||
|
||
```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 (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)
|
||
|
||
```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
|
||
|
||
```mermaid
|
||
graph LR
|
||
DT["person:dtoro"] -->|owns| NO["agent:nomos"]
|
||
DT -->|decides| AP["approval:xyz"]
|
||
AK["idp:authentik"] -->|authenticates| DT
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Lifecycle State Machines
|
||
|
||
### Infrastructure (15 concrete types use this)
|
||
|
||
```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`):
|
||
| Transition | Precondition | How it's checked |
|
||
|------------|-------------|-----------------|
|
||
| provisioning→active | `health-check-answering` | `SELECT health FROM entity_status WHERE entity_id=$1` — must be healthy |
|
||
| provisioning→active | `age-key-enrolled-if-needed` | Checks `attributes->>'age_pubkey'` (workstation only) |
|
||
| provisioning→active | `mesh-joined-if-needed` | Checks `attributes->>'mesh_ip'` (workstation only) |
|
||
| provisioning→active | `doc-page-complete` | `SELECT count(*) FROM relationships WHERE target_id=$1 AND type='documents'` |
|
||
| deprecated→destroyed | `no-inbound-edges` | `SELECT count(*) FROM relationships WHERE target_id=$1 AND valid_to IS NULL` |
|
||
| any → terminated | `backups-verified` | Checks flag in entity attributes |
|
||
| any → terminated | `secrets-revoked` | Checks flag in entity attributes |
|
||
|
||
**Soft preconditions** (always pass — operator-confirmed): `inventory-entry`, `ip-reserved`, `preflight-passed`, `backup-verified`, `replacement-live`, `caddy-backends-checked`, `un-deprecate-note`, etc.
|
||
|
||
### Signal
|
||
|
||
```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:**
|
||
- `raised → muted`: requires `mute_until` set (MuteSignal handler, `impl.go:615-689`)
|
||
- `acting → resolved`: requires `verification-passed` (soft — operator confirms)
|
||
|
||
**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
|
||
|
||
### ✅ Fully Implemented (code exists, running in production)
|
||
|
||
| Component | File(s) | What it does |
|
||
|-----------|---------|-------------|
|
||
| Entity CRUD | `impl.go:811-966` | Create, read, patch, list entities |
|
||
| Lifecycle transitions | `impl.go:1494-1579` | Precondition checks + state transitions |
|
||
| Relationship management | `seed.go` (ingest) | Create edges with `valid_from/valid_to` |
|
||
| Client enrollment | `impl.go:1134-1236` | `POST /clients/enroll` — age keypair, Infisical, state: provisioning |
|
||
| Check definitions | `phase3.go:265-376` | CreateCheck, ListChecks, PatchCheck |
|
||
| Scheduler observe | `scheduler.go:26-215` | 6 probe kinds, metric_samples, signals, entity_status |
|
||
| Signals | `scheduler.go:81-161` | UpsertSignal (dedup), ResolveSignal, severity evaluation |
|
||
| Executions | `server.go:286-411` | request_execution MCP tool — reversible_low/config_mutation/destructive |
|
||
| Approvals | `server.go:970-1052` | createApproval, DecideApproval → executeApprovedAction |
|
||
| Notifier | `notifier/notifier.go` | Matrix alerts for pending approvals |
|
||
| Patterns | `phase3.go:1100+` | ListPatterns, PatchPattern (status/quarantine) |
|
||
| Skills | `phase3.go:1300+` | ListSkills, PatchSkill, ListSkillVersions |
|
||
| Default checks | `checkdefaults/defaults.go` | Auto-create checks on entity creation/enrollment/seed |
|
||
| TimescaleDB metrics | `metric_samples` table | Hypertable with 1h/1d continuous aggregates, 90-day retention |
|
||
| Events + SSE | `events` table + pg_notify | Real-time UI updates via SSE endpoint |
|
||
| Audit log | `audit_log` hypertable | Every mutation with actor + action |
|
||
| Knowledge entities | `knowledge_entities` | Documents, runbooks, investigations with FTS |
|
||
| MCP tools | `server.go` | 24 tools for observe/orient/decide/act |
|
||
| Blast radius | `blast_radius()` fn | Recursive CTE — depends-on + hosts + routes-to edges |
|
||
|
||
### ⚠ Schema Defined, Not Yet Wired (table exists, no active code path creates rows)
|
||
|
||
| Component | What's Missing |
|
||
|-----------|---------------|
|
||
| `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`. 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. |
|
||
|
||
### 📋 Defined in Seeds Only (ontology.yaml references, no DB schema)
|
||
|
||
| Item | Notes |
|
||
|------|-------|
|
||
| Relationship type `cluster` | Mentioned in inventory but not in ontology relationship_types |
|
||
| `certificate` entity type | Referenced in `uses-certificate` edges but no concrete certificates in inventory |
|
||
| Relationship type `powers` / `monitors` | Not defined in relationship_types |
|
||
|
||
---
|
||
|
||
## 7. Database Physical Schema
|
||
|
||
```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)"
|
||
|
||
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)"
|
||
|
||
entities ||--o{ relationships : "source_id FK"
|
||
entities ||--o{ relationships : "target_id FK"
|
||
|
||
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 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
|
||
|
||
```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):**
|
||
1. `scheduler.go` refactored to return metrics map → `checkResult{metrics}`
|
||
2. `ssh-script` check kind implemented → SSH exec to remote host
|
||
3. `cpu_check.sh` deployed to hubris → returns `{"metrics":{"cpu_pct":2.5,"cpu_temp":48}}`
|
||
4. Check created: `POST /checks {"kind":"ssh-script","target":"host:hubris","config":{"host":"192.168.8.77","script":"cpu_check.sh"}}`
|
||
5. Fixed: `InsertMetricSample` missing `ts` column → `now()` literal
|
||
6. Fixed: SSH port/user parsing bugs
|
||
7. Fixed: SSH warnings polluting JSON output
|
||
8. Scheduler loop → metrics written to TimescaleDB every 60s
|
||
9. MCP `query_metrics` reads from TimescaleDB → Nomos gets live data
|