oikos phase 0: ontology + inventory + policy seeds, OpenAPI contract, ADRs
- seeds/ontology.yaml: 59 entity types (5 abstract, is-a hierarchy), 46 relationship types with cardinality, 6 lifecycles with terminal states and named precondition checks - seeds/inventory.yaml: 110 entities / 142 relationships translated from legacy inventory.yaml (fleet, services, ingress, storage, governance, archaeology); thin spots marked for backfill - seeds/policy.yaml: 4 risk classes, 27 approval rules (hierarchy-aware, per-entity overrides), autonomy kill-switch off (cold start) - api/openapi.yaml: full v1 REST contract (40 paths), RFC 9457 errors, cursor pagination, idempotency, ETag/If-Match, scopes; redocly-clean - docs/adr/0001-0010: initial architecture decision records - scripts/validate-seeds.py: Phase 0 gate — hierarchy, lifecycles, endpoints, cardinality, policy cross-refs (0 errors) - plan: layer CHECK gains 'meta' (root type), cardinality gains 'many-to-one' Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1636
api/openapi.yaml
Normal file
1636
api/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
7
api/redocly.yaml
Normal file
7
api/redocly.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Redocly lint config for api/openapi.yaml (CI runs: redocly lint api/openapi.yaml)
|
||||||
|
extends:
|
||||||
|
- recommended
|
||||||
|
rules:
|
||||||
|
# Every operation declares `default` → RFC 9457 problem+json instead of
|
||||||
|
# enumerating each 4XX (plan R3-3); oapi-codegen handles `default` fine.
|
||||||
|
operation-4xx-response: off
|
||||||
23
docs/adr/0001-go-single-binary.md
Normal file
23
docs/adr/0001-go-single-binary.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ADR 0001 — Go with single-binary role packaging
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, R3-4
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The OS has three long-running roles (api, scheduler+actuator+learning,
|
||||||
|
notifier) plus one-shot jobs (migrate, seed, export). Rev 2 planned three
|
||||||
|
binaries with three Dockerfiles.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
One Go binary `oikos` with role subcommands (`oikos api | scheduler |
|
||||||
|
notifier | all | migrate | seed | export`), one multi-stage Dockerfile, one
|
||||||
|
image tagged `oikos:<git-sha>`. Compose runs the image N times with
|
||||||
|
different commands (Loki/Temporal pattern). Go over Python for static
|
||||||
|
typing, small static binaries (CGO_ENABLED=0, distroless), and goroutines
|
||||||
|
for concurrent probes.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- One build, guaranteed version consistency across roles, trivial local dev
|
||||||
|
(`oikos all`), simpler rollback (retag one image).
|
||||||
|
- Full rewrite of ~4,400 Python lines (logic carries over per plan reuse map).
|
||||||
|
- All roles share a dependency set; image is slightly larger than per-role
|
||||||
|
minimal images — accepted.
|
||||||
25
docs/adr/0002-postgres-timescale-only-datastore.md
Normal file
25
docs/adr/0002-postgres-timescale-only-datastore.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# ADR 0002 — PostgreSQL + TimescaleDB as the only datastore
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The OS needs a graph (entities/relationships), operational tables
|
||||||
|
(signals/executions/approvals), a learning corpus, time-series metrics,
|
||||||
|
audit and event logs. Alternatives: dedicated graph DB (Neo4j), dedicated
|
||||||
|
TSDB (Prometheus/VictoriaMetrics), or one Postgres.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
One PostgreSQL 16 instance with the TimescaleDB extension
|
||||||
|
(timescale/timescaledb:2-pg16). Graph traversal via recursive CTEs
|
||||||
|
(cycle-safe blast_radius); time-series via hypertables + continuous
|
||||||
|
aggregates + retention policies; events via table + LISTEN/NOTIFY.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- One backup/restore/DR story, one connection pool, transactional
|
||||||
|
consistency between graph and operational writes (e.g. event emission in
|
||||||
|
the same transaction as state change).
|
||||||
|
- Postgres is the accepted SPOF — mitigated by daily pg_dump + WAL PITR +
|
||||||
|
off-host copies + monthly restore drills; streaming replication is the
|
||||||
|
future path if needed.
|
||||||
|
- Homelab graph scale (hundreds of nodes) is far below where a dedicated
|
||||||
|
graph DB pays for itself.
|
||||||
23
docs/adr/0003-db-native-ontology-yaml-seeds.md
Normal file
23
docs/adr/0003-db-native-ontology-yaml-seeds.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ADR 0003 — DB-native ontology with YAML seed manifests
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, R3-1
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Rev 1 kept inventory/ontology/policy as YAML files parsed at runtime.
|
||||||
|
Agents need graph queries (blast radius), transactional mutations with
|
||||||
|
audit, and a future UI needs to edit the model without file round-trips.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
The DB is the runtime source of truth. entity_types form an is-a hierarchy
|
||||||
|
(parent_type, is_abstract); relationship endpoint constraints may name
|
||||||
|
abstract types and validation walks the hierarchy. YAML files under seeds/
|
||||||
|
bootstrap the DB (idempotent, content-hashed via seed_versions) and serve
|
||||||
|
DR; `GET /api/v1/export` regenerates them for version control (round-trip
|
||||||
|
byte-stable, tested in CI).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Ontology changes are API calls (policy-gated), not redeploys.
|
||||||
|
- Seeds can drift from DB between exports — export is part of the routine
|
||||||
|
(commit after meaningful model edits).
|
||||||
|
- Abstract types let policy rules and relationships bind once at the right
|
||||||
|
altitude (e.g. `compute-entity provides service`).
|
||||||
21
docs/adr/0004-openapi-first.md
Normal file
21
docs/adr/0004-openapi-first.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# ADR 0004 — Contract-first OpenAPI API
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, R3-2/R3-3
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Future UIs, a CLI client, and an MCP surface must stay in sync with the
|
||||||
|
API. Code-first (Gin + generated docs) drifts.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
api/openapi.yaml (OpenAPI 3.1) is the source of truth. Server stubs via
|
||||||
|
oapi-codegen (strict server, chi router); clients generated for Go (CLI)
|
||||||
|
and TypeScript (future UI). Conventions: RFC 9457 problem+json errors,
|
||||||
|
{items, next_cursor} envelopes, cursor pagination, Idempotency-Key on
|
||||||
|
unsafe POSTs, ETag/If-Match optimistic concurrency, scopes
|
||||||
|
(operator/viewer/agent) annotated per operation. CI fails on spec/handler
|
||||||
|
drift. MCP tools wrap the same service layer.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- UI development needs only the running API (spec served at /openapi.yaml).
|
||||||
|
- Handler changes require spec changes first — deliberate friction.
|
||||||
|
- Breaking changes ship as /api/v2 side by side; v1 is additive-only.
|
||||||
18
docs/adr/0005-uuidv7-plus-slug-identity.md
Normal file
18
docs/adr/0005-uuidv7-plus-slug-identity.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# ADR 0005 — UUIDv7 + slug entity identity
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, R3-5 (resolves audit D1)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Rev 2 used TEXT primary keys ('host:hubris') — renames break FKs, and
|
||||||
|
date-string signal IDs are race-prone.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Primary keys are UUIDv7 (time-ordered, generated in Go). Every entity also
|
||||||
|
carries a unique human slug ('host:hubris'); (type, name) is unique too.
|
||||||
|
The API accepts UUID or slug everywhere; slugs may change (rename), UUIDs
|
||||||
|
never do.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Renames are metadata updates; history and edges survive.
|
||||||
|
- UUIDv7's time-ordering keeps B-tree inserts append-mostly.
|
||||||
|
- Seeds and exports use slugs (human-diffable); ingest resolves to UUIDs.
|
||||||
23
docs/adr/0006-learning-proposal-only.md
Normal file
23
docs/adr/0006-learning-proposal-only.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# ADR 0006 — Learning is proposal-only (no self-authorization)
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3 (resolves audit S3/S4/SA2)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The learning loop (feedback → patterns → skills) informs the classifier
|
||||||
|
that decides auto-act vs escalate. If learning could expand its own
|
||||||
|
autonomy, poisoned feedback (flapping services, biased probes) could
|
||||||
|
unlock destructive auto-act.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
The learning engine cannot write to governance (policy/autonomy) tables —
|
||||||
|
enforced structurally: its DB role has no grants on them. Pattern
|
||||||
|
activation (validated → active) and any autonomy expansion require operator
|
||||||
|
approval. Confidence is the Wilson lower bound capped by evidence_count/5;
|
||||||
|
anomalous feedback bursts quarantine the pattern; no skill ever
|
||||||
|
auto-promotes an action into destructive autonomy (hard-coded). Lowering
|
||||||
|
autonomy (kill-switch) is always immediate, never gated.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Cold start is slow by design — the agent escalates until trust is earned.
|
||||||
|
- The operator is the only path to more autonomy; the audit trail shows
|
||||||
|
every grant.
|
||||||
27
docs/adr/0007-threat-model.md
Normal file
27
docs/adr/0007-threat-model.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# ADR 0007 — Threat model and trust zones
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, Security model section
|
||||||
|
|
||||||
|
## Context
|
||||||
|
The control plane can restart services and (eventually) mutate config
|
||||||
|
fleet-wide. Compromise of any one container must not equal compromise of
|
||||||
|
the fleet.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Trust zones as Docker networks: net-front (Caddy→api only), net-data
|
||||||
|
(Postgres), net-ops (SSH egress, actuator only). Hermes holds no SSH keys;
|
||||||
|
the actuator uses a restricted key (command=/from= in authorized_keys)
|
||||||
|
until the /executions gateway fully brokers actions. Caddy is an explicit
|
||||||
|
trust root but the API independently validates OIDC JWTs — network origin
|
||||||
|
is defense-in-depth, never the auth (this enables the LAN break-glass API
|
||||||
|
binding; the Hermes gateway remains mesh-only). Policy changes are
|
||||||
|
dual-controlled with before/after hash auditing and a startup
|
||||||
|
hash-vs-known-good check. Approval tokens are single-use HMAC, hashed at
|
||||||
|
rest, TTL-bound.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Documented residual risks: plaintext LAN break-glass hop (emergency use),
|
||||||
|
Postgres as shared dependency of all roles, macOS host itself unmanaged
|
||||||
|
by the OS.
|
||||||
|
- Rotation cadences: actuator SSH key 6mo, machine tokens 90d, webhook
|
||||||
|
HMAC 1y — scheduler raises expiry signals 2 weeks ahead.
|
||||||
20
docs/adr/0008-forward-only-migrations.md
Normal file
20
docs/adr/0008-forward-only-migrations.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# ADR 0008 — Forward-only migrations
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3 (resolves audit D5/O1)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Down-migrations are rarely tested and lie about reversibility once data
|
||||||
|
has flowed. Rollback needs a strategy that works with real data.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
golang-migrate, embedded (//go:embed), up-only. Migrations run in a
|
||||||
|
one-shot init container with a DDL-only DB user before app roles start.
|
||||||
|
Within one deploy window migrations are additive-only (new columns
|
||||||
|
nullable, new tables optional) so previous-SHA images tolerate the new
|
||||||
|
schema. Rollback = redeploy previous image tag; if the migration itself is
|
||||||
|
the problem, pg_restore the automatic pre-deploy dump. Mistakes roll
|
||||||
|
forward via compensating migrations.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- No down.sql to write or test; the pre-deploy dump is the real safety net.
|
||||||
|
- Destructive schema changes (drop/rename) take two deploys by design.
|
||||||
19
docs/adr/0009-sse-over-websocket.md
Normal file
19
docs/adr/0009-sse-over-websocket.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# ADR 0009 — SSE over WebSocket for the event stream
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, R3-14
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Live updates (signals, executions, approvals) push server→client only.
|
||||||
|
Rev 2 specified WebSocket.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Server-Sent Events at GET /api/v1/events/stream: plain HTTP (proxies
|
||||||
|
through Caddy without upgrade handling), native browser EventSource with
|
||||||
|
auto-reconnect, Last-Event-ID resume backed by the events table. Bounded
|
||||||
|
per-subscriber buffers with drop-oldest; heartbeat comments every 15s.
|
||||||
|
Delivery is best-effort — GET /events backfills. Transactional emission +
|
||||||
|
post-commit LISTEN/NOTIFY feed the stream.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- No bidirectional channel; if one is ever needed (interactive terminals),
|
||||||
|
add WebSocket alongside — this ADR covers the event feed only.
|
||||||
20
docs/adr/0010-infisical-with-sops-fallback.md
Normal file
20
docs/adr/0010-infisical-with-sops-fallback.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# ADR 0010 — Infisical secrets with SOPS DR fallback
|
||||||
|
|
||||||
|
Status: accepted (2026-07-07) · Plan: rev 3, Phase 5 (resolves audit S9)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
SOPS+age is file-based: no runtime API, no machine identities, no
|
||||||
|
rotation tracking, and every consumer needs the age key.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
Infisical in the Docker stack; services fetch via machine identities;
|
||||||
|
secrets never in env files or plain config (config hierarchy: defaults →
|
||||||
|
file → env → Infisical, secrets only). Bootstrap root of trust: Infisical
|
||||||
|
master key in the mac-mini Keychain, backed up offline. One age key is
|
||||||
|
retained and all secrets are exported to a SOPS-encrypted fallback file
|
||||||
|
until an Infisical restore drill has passed; the fallback is refreshed on
|
||||||
|
rotation.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Chicken-and-egg is explicit: the Keychain + offline copy are the root.
|
||||||
|
- SOPS retirement is gated on a passed restore drill, not on the calendar.
|
||||||
18
docs/adr/README.md
Normal file
18
docs/adr/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Architecture Decision Records
|
||||||
|
|
||||||
|
MADR-style records for Oikos. One decision per file, numbered, never edited
|
||||||
|
after acceptance — superseding decisions get a new ADR that links back.
|
||||||
|
Statuses: proposed | accepted | superseded-by-NNNN.
|
||||||
|
|
||||||
|
| ADR | Title |
|
||||||
|
|---|---|
|
||||||
|
| [0001](0001-go-single-binary.md) | Go with single-binary role packaging |
|
||||||
|
| [0002](0002-postgres-timescale-only-datastore.md) | PostgreSQL + TimescaleDB as the only datastore |
|
||||||
|
| [0003](0003-db-native-ontology-yaml-seeds.md) | DB-native ontology with YAML seed manifests |
|
||||||
|
| [0004](0004-openapi-first.md) | Contract-first OpenAPI API |
|
||||||
|
| [0005](0005-uuidv7-plus-slug-identity.md) | UUIDv7 + slug entity identity |
|
||||||
|
| [0006](0006-learning-proposal-only.md) | Learning is proposal-only (no self-authorization) |
|
||||||
|
| [0007](0007-threat-model.md) | Threat model and trust zones |
|
||||||
|
| [0008](0008-forward-only-migrations.md) | Forward-only migrations |
|
||||||
|
| [0009](0009-sse-over-websocket.md) | SSE over WebSocket for the event stream |
|
||||||
|
| [0010](0010-infisical-with-sops-fallback.md) | Infisical secrets with SOPS DR fallback |
|
||||||
@@ -711,7 +711,8 @@ CREATE TABLE entity_types (
|
|||||||
is_abstract BOOLEAN NOT NULL DEFAULT false, -- abstract types can't be instantiated
|
is_abstract BOOLEAN NOT NULL DEFAULT false, -- abstract types can't be instantiated
|
||||||
domain TEXT NOT NULL, -- 'physical','compute','network','storage',
|
domain TEXT NOT NULL, -- 'physical','compute','network','storage',
|
||||||
-- 'software','identity','policy','cognition'
|
-- 'software','identity','policy','cognition'
|
||||||
layer TEXT NOT NULL CHECK (layer IN ('infrastructure','governance','cognition')),
|
layer TEXT NOT NULL CHECK (layer IN ('meta','infrastructure','governance','cognition')),
|
||||||
|
-- 'meta' is reserved for the abstract root type 'entity'
|
||||||
description TEXT,
|
description TEXT,
|
||||||
lifecycle_id TEXT REFERENCES lifecycle_defs(id),
|
lifecycle_id TEXT REFERENCES lifecycle_defs(id),
|
||||||
attribute_schema JSONB, -- JSON Schema for entities.attributes (D2)
|
attribute_schema JSONB, -- JSON Schema for entities.attributes (D2)
|
||||||
@@ -727,8 +728,8 @@ CREATE TABLE relationship_types (
|
|||||||
inverse TEXT,
|
inverse TEXT,
|
||||||
source_type TEXT NOT NULL REFERENCES entity_types(name), -- MAY be abstract;
|
source_type TEXT NOT NULL REFERENCES entity_types(name), -- MAY be abstract;
|
||||||
target_type TEXT NOT NULL REFERENCES entity_types(name), -- validation walks hierarchy
|
target_type TEXT NOT NULL REFERENCES entity_types(name), -- validation walks hierarchy
|
||||||
cardinality TEXT NOT NULL CHECK (cardinality IN
|
cardinality TEXT NOT NULL CHECK (cardinality IN -- source→target multiplicity
|
||||||
('one-to-one','one-to-many','many-to-many')),
|
('one-to-one','one-to-many','many-to-one','many-to-many')),
|
||||||
description TEXT,
|
description TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|||||||
284
scripts/validate-seeds.py
Normal file
284
scripts/validate-seeds.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Validate seeds/*.yaml against the Oikos meta-schema (Phase 0 gate).
|
||||||
|
|
||||||
|
Checks the same invariants the Go ingest (internal/ontology) will enforce:
|
||||||
|
|
||||||
|
ontology.yaml
|
||||||
|
- every entity type's parent exists; hierarchy is acyclic
|
||||||
|
- layer/cardinality values are legal
|
||||||
|
- lifecycle references exist; default/terminal/transition states are declared
|
||||||
|
- relationship endpoint types exist (may be abstract)
|
||||||
|
- inverse names don't collide with forward names
|
||||||
|
|
||||||
|
inventory.yaml
|
||||||
|
- slugs unique and well-formed (<prefix>:<name>)
|
||||||
|
- entity types exist and are NOT abstract
|
||||||
|
- states are legal for the type's lifecycle (walking up the hierarchy for
|
||||||
|
the lifecycle definition is not needed — lifecycle binds per type)
|
||||||
|
- relationship endpoints exist; edge type exists; endpoint entity types
|
||||||
|
are the declared source/target types or descendants (hierarchy walk)
|
||||||
|
- cardinality: one-to-one / one-to-many / many-to-one uniqueness holds
|
||||||
|
within the seed
|
||||||
|
- attributes validate against attribute_schema (if jsonschema installed)
|
||||||
|
|
||||||
|
policy.yaml
|
||||||
|
- approval_rules reference existing risk classes / entity types / entities
|
||||||
|
- autonomy values sane
|
||||||
|
|
||||||
|
Exit 0 = clean (warnings allowed), 1 = errors.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: PyYAML required (pip install pyyaml)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jsonschema
|
||||||
|
HAVE_JSONSCHEMA = True
|
||||||
|
except ImportError:
|
||||||
|
HAVE_JSONSCHEMA = False
|
||||||
|
|
||||||
|
SEEDS = Path(__file__).resolve().parent.parent / "seeds"
|
||||||
|
LAYERS = {"meta", "infrastructure", "governance", "cognition"}
|
||||||
|
CARDINALITIES = {"one-to-one", "one-to-many", "many-to-one", "many-to-many"}
|
||||||
|
|
||||||
|
errors: list[str] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def err(msg: str) -> None:
|
||||||
|
errors.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def warn(msg: str) -> None:
|
||||||
|
warnings.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def load(name: str) -> dict:
|
||||||
|
path = SEEDS / name
|
||||||
|
if not path.exists():
|
||||||
|
err(f"{name}: file missing")
|
||||||
|
return {}
|
||||||
|
with open(path) as f:
|
||||||
|
try:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
err(f"{name}: YAML parse error: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── ontology.yaml ────────────────────────────────────────────────────
|
||||||
|
onto = load("ontology.yaml")
|
||||||
|
etypes: dict = onto.get("entity_types", {})
|
||||||
|
rtypes: dict = onto.get("relationship_types", {})
|
||||||
|
lifecycles: dict = onto.get("lifecycles", {})
|
||||||
|
|
||||||
|
for name, lc in lifecycles.items():
|
||||||
|
states = lc.get("states", [])
|
||||||
|
if not states:
|
||||||
|
err(f"lifecycle {name}: no states")
|
||||||
|
continue
|
||||||
|
if lc.get("default_state") not in states:
|
||||||
|
err(f"lifecycle {name}: default_state {lc.get('default_state')!r} not in states")
|
||||||
|
for t in lc.get("terminal_states", []):
|
||||||
|
if t not in states:
|
||||||
|
err(f"lifecycle {name}: terminal state {t!r} not in states")
|
||||||
|
for frm, tos in (lc.get("transitions") or {}).items():
|
||||||
|
if frm not in states:
|
||||||
|
err(f"lifecycle {name}: transition source {frm!r} not in states")
|
||||||
|
for to, spec in (tos or {}).items():
|
||||||
|
if to not in states:
|
||||||
|
err(f"lifecycle {name}: transition target {to!r} not in states")
|
||||||
|
if spec is not None and not isinstance(spec.get("requires", []), list):
|
||||||
|
err(f"lifecycle {name}: {frm}->{to} requires must be a list")
|
||||||
|
|
||||||
|
for name, et in etypes.items():
|
||||||
|
parent = et.get("parent")
|
||||||
|
if parent is not None and parent not in etypes:
|
||||||
|
err(f"entity type {name}: parent {parent!r} not defined")
|
||||||
|
layer = et.get("layer")
|
||||||
|
if layer not in LAYERS:
|
||||||
|
err(f"entity type {name}: layer {layer!r} invalid (want {sorted(LAYERS)})")
|
||||||
|
lc = et.get("lifecycle")
|
||||||
|
if lc is not None and lc not in lifecycles:
|
||||||
|
err(f"entity type {name}: lifecycle {lc!r} not defined")
|
||||||
|
schema = et.get("attributes")
|
||||||
|
if schema is not None and HAVE_JSONSCHEMA:
|
||||||
|
try:
|
||||||
|
jsonschema.Draft202012Validator.check_schema(schema)
|
||||||
|
except jsonschema.SchemaError as e:
|
||||||
|
err(f"entity type {name}: attribute_schema is not valid JSON Schema: {e.message}")
|
||||||
|
|
||||||
|
# hierarchy acyclicity + ancestor helper
|
||||||
|
def ancestors(t: str) -> list[str]:
|
||||||
|
chain, seen = [], set()
|
||||||
|
cur = t
|
||||||
|
while cur is not None:
|
||||||
|
if cur in seen:
|
||||||
|
err(f"entity type hierarchy cycle at {cur!r}")
|
||||||
|
break
|
||||||
|
seen.add(cur)
|
||||||
|
chain.append(cur)
|
||||||
|
cur = etypes.get(cur, {}).get("parent")
|
||||||
|
return chain
|
||||||
|
|
||||||
|
for name in etypes:
|
||||||
|
ancestors(name)
|
||||||
|
|
||||||
|
def is_a(t: str, target: str) -> bool:
|
||||||
|
return target in ancestors(t)
|
||||||
|
|
||||||
|
fwd_names = set(rtypes)
|
||||||
|
for name, rt in rtypes.items():
|
||||||
|
for endpoint in ("source", "target"):
|
||||||
|
v = rt.get(endpoint)
|
||||||
|
if v not in etypes:
|
||||||
|
err(f"relationship type {name}: {endpoint} {v!r} not a defined entity type")
|
||||||
|
if rt.get("cardinality") not in CARDINALITIES:
|
||||||
|
err(f"relationship type {name}: cardinality {rt.get('cardinality')!r} invalid")
|
||||||
|
inv = rt.get("inverse")
|
||||||
|
if inv and inv in fwd_names:
|
||||||
|
err(f"relationship type {name}: inverse {inv!r} collides with a forward name")
|
||||||
|
|
||||||
|
# ─── inventory.yaml ───────────────────────────────────────────────────
|
||||||
|
inv = load("inventory.yaml")
|
||||||
|
entities: list = inv.get("entities", [])
|
||||||
|
relationships: list = inv.get("relationships", [])
|
||||||
|
|
||||||
|
slugs: dict = {}
|
||||||
|
for e in entities:
|
||||||
|
slug = e.get("slug")
|
||||||
|
if not slug or ":" not in slug:
|
||||||
|
err(f"entity {e}: slug missing or not '<prefix>:<name>'")
|
||||||
|
continue
|
||||||
|
if slug in slugs:
|
||||||
|
err(f"duplicate slug {slug!r}")
|
||||||
|
slugs[slug] = e
|
||||||
|
t = e.get("type")
|
||||||
|
if t not in etypes:
|
||||||
|
err(f"{slug}: type {t!r} not in ontology")
|
||||||
|
continue
|
||||||
|
if etypes[t].get("abstract"):
|
||||||
|
err(f"{slug}: type {t!r} is abstract — cannot be instantiated")
|
||||||
|
if not e.get("name"):
|
||||||
|
err(f"{slug}: name missing")
|
||||||
|
state = e.get("state")
|
||||||
|
lc_name = etypes[t].get("lifecycle")
|
||||||
|
if state is not None:
|
||||||
|
if lc_name is None:
|
||||||
|
err(f"{slug}: has state {state!r} but type {t!r} has no lifecycle")
|
||||||
|
elif state not in lifecycles.get(lc_name, {}).get("states", []):
|
||||||
|
err(f"{slug}: state {state!r} not in lifecycle {lc_name!r}")
|
||||||
|
schema = etypes[t].get("attributes")
|
||||||
|
if schema and HAVE_JSONSCHEMA and e.get("attributes"):
|
||||||
|
v = jsonschema.Draft202012Validator(schema)
|
||||||
|
for ve in v.iter_errors(e["attributes"]):
|
||||||
|
warn(f"{slug}: attributes: {ve.message}")
|
||||||
|
|
||||||
|
# uniqueness of (type, name)
|
||||||
|
seen_tn = set()
|
||||||
|
for e in entities:
|
||||||
|
tn = (e.get("type"), e.get("name"))
|
||||||
|
if tn in seen_tn:
|
||||||
|
err(f"duplicate (type, name): {tn}")
|
||||||
|
seen_tn.add(tn)
|
||||||
|
|
||||||
|
edge_keys = set()
|
||||||
|
by_card_src: dict = {}
|
||||||
|
by_card_tgt: dict = {}
|
||||||
|
for r in relationships:
|
||||||
|
src, tgt, rt_name = r.get("source"), r.get("target"), r.get("type")
|
||||||
|
ctx = f"edge {src} -{rt_name}-> {tgt}"
|
||||||
|
if rt_name not in rtypes:
|
||||||
|
err(f"{ctx}: relationship type not in ontology")
|
||||||
|
continue
|
||||||
|
ok = True
|
||||||
|
for label, slug in (("source", src), ("target", tgt)):
|
||||||
|
if slug not in slugs:
|
||||||
|
err(f"{ctx}: {label} entity {slug!r} not in inventory")
|
||||||
|
ok = False
|
||||||
|
if not ok:
|
||||||
|
continue
|
||||||
|
key = (src, tgt, rt_name)
|
||||||
|
if key in edge_keys:
|
||||||
|
err(f"{ctx}: duplicate edge")
|
||||||
|
edge_keys.add(key)
|
||||||
|
rt = rtypes[rt_name]
|
||||||
|
for label, slug, want in (("source", src, rt["source"]), ("target", tgt, rt["target"])):
|
||||||
|
actual = slugs[slug]["type"]
|
||||||
|
if not is_a(actual, want):
|
||||||
|
err(f"{ctx}: {label} type {actual!r} is not a {want!r} (or descendant)")
|
||||||
|
# cardinality bookkeeping (source→target multiplicity)
|
||||||
|
card = rt.get("cardinality")
|
||||||
|
if card in ("one-to-one", "many-to-one"):
|
||||||
|
# each source has at most one outgoing edge of this type
|
||||||
|
k = (rt_name, src)
|
||||||
|
if k in by_card_src:
|
||||||
|
err(f"{ctx}: cardinality {card} — source {src!r} already has a {rt_name!r} edge")
|
||||||
|
by_card_src[k] = tgt
|
||||||
|
if card in ("one-to-one", "one-to-many"):
|
||||||
|
# each target has at most one incoming edge of this type
|
||||||
|
k = (rt_name, tgt)
|
||||||
|
if k in by_card_tgt:
|
||||||
|
err(f"{ctx}: cardinality {card} — target {tgt!r} already has a {rt_name!r} edge")
|
||||||
|
by_card_tgt[k] = src
|
||||||
|
|
||||||
|
# ─── policy.yaml ──────────────────────────────────────────────────────
|
||||||
|
pol = load("policy.yaml")
|
||||||
|
rclasses: dict = pol.get("risk_classes", {})
|
||||||
|
for name, rc in rclasses.items():
|
||||||
|
if rc.get("approval_required") not in ("none", "operator", "operator_confirmed"):
|
||||||
|
err(f"risk class {name}: approval_required invalid")
|
||||||
|
|
||||||
|
rules = pol.get("approval_rules", [])
|
||||||
|
seen_rules = set()
|
||||||
|
for rule in rules:
|
||||||
|
ctx = f"rule ({rule.get('entity_type')}, {rule.get('action')}, {rule.get('scope_entity')})"
|
||||||
|
if rule.get("risk_class") not in rclasses:
|
||||||
|
err(f"{ctx}: risk_class {rule.get('risk_class')!r} not defined")
|
||||||
|
if rule.get("autonomy_level") not in ("auto", "escalate", "never"):
|
||||||
|
err(f"{ctx}: autonomy_level invalid")
|
||||||
|
et = rule.get("entity_type")
|
||||||
|
if et is not None and et not in etypes:
|
||||||
|
err(f"{ctx}: entity_type {et!r} not in ontology")
|
||||||
|
scope = rule.get("scope_entity")
|
||||||
|
if scope is not None and scope not in slugs:
|
||||||
|
err(f"{ctx}: scope_entity {scope!r} not in inventory")
|
||||||
|
key = (et, rule.get("action"), scope)
|
||||||
|
if key in seen_rules:
|
||||||
|
err(f"{ctx}: duplicate rule")
|
||||||
|
seen_rules.add(key)
|
||||||
|
# rule sanity: autonomy 'auto' requires the class to allow autonomy
|
||||||
|
rc = rclasses.get(rule.get("risk_class"), {})
|
||||||
|
if rule.get("autonomy_level") == "auto" and not rc.get("autonomy_allowed"):
|
||||||
|
err(f"{ctx}: autonomy_level=auto but risk class forbids autonomy")
|
||||||
|
|
||||||
|
auto = pol.get("autonomy_settings", {})
|
||||||
|
ga = auto.get("global.auto_act")
|
||||||
|
if ga not in ("off", "reversible_low"):
|
||||||
|
err(f"autonomy_settings: global.auto_act {ga!r} invalid (off | reversible_low)")
|
||||||
|
for k in auto:
|
||||||
|
if k.startswith("never_auto_act."):
|
||||||
|
slug = k.split(".", 1)[1]
|
||||||
|
if slug not in slugs:
|
||||||
|
warn(f"autonomy_settings: {k} references unknown entity {slug!r}")
|
||||||
|
|
||||||
|
# ─── report ───────────────────────────────────────────────────────────
|
||||||
|
print(f"entity types: {len(etypes)} ({sum(1 for e in etypes.values() if e.get('abstract'))} abstract)")
|
||||||
|
print(f"relationship types: {len(rtypes)}")
|
||||||
|
print(f"lifecycles: {len(lifecycles)}")
|
||||||
|
print(f"entities: {len(entities)}")
|
||||||
|
print(f"relationships: {len(relationships)}")
|
||||||
|
print(f"risk classes: {len(rclasses)}, rules: {len(rules)}")
|
||||||
|
if not HAVE_JSONSCHEMA:
|
||||||
|
warn("jsonschema not installed — attribute schema validation skipped")
|
||||||
|
for w in warnings:
|
||||||
|
print(f"WARN {w}")
|
||||||
|
for e in errors:
|
||||||
|
print(f"ERROR {e}")
|
||||||
|
print(f"\n{'FAIL' if errors else 'OK'} — {len(errors)} error(s), {len(warnings)} warning(s)")
|
||||||
|
sys.exit(1 if errors else 0)
|
||||||
534
seeds/inventory.yaml
Normal file
534
seeds/inventory.yaml
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
# Oikos inventory seed — entity instances + relationships.
|
||||||
|
#
|
||||||
|
# Translated from the legacy /inventory.yaml (2026-07-07). Bootstraps the
|
||||||
|
# entities/relationships tables (migration 002); after ingest the DB is
|
||||||
|
# authoritative and this file is regenerated by `GET /api/v1/export`.
|
||||||
|
#
|
||||||
|
# Slug conventions: <prefix>:<name> —
|
||||||
|
# host: (proxmox-host, standalone-server) · ws: (workstation) · lxc: · vm:
|
||||||
|
# service: · ingress: · repo: (config-repo) · pool: · volume: · mesh: · lan:
|
||||||
|
# zone: (dns-zone) · idp: · person: · agent: · cluster: · backup:
|
||||||
|
#
|
||||||
|
# `state:` omitted = the type's lifecycle default (active).
|
||||||
|
# Mount details (mount_point) are attributes on `mounts` edges.
|
||||||
|
# Known thin spots are marked # THIN: backfill later.
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
entities:
|
||||||
|
|
||||||
|
# ─── Sites, networks ───────────────────────────────────────────────
|
||||||
|
- {slug: "site:home", type: site, name: home}
|
||||||
|
- {slug: "site:ionos-dc", type: site, name: ionos-dc,
|
||||||
|
attributes: {address: IONOS datacenter (VPS)}}
|
||||||
|
- slug: "lan:lab"
|
||||||
|
type: lan
|
||||||
|
name: lab
|
||||||
|
attributes: {subnet: 192.168.8.0/24}
|
||||||
|
- slug: "lan:household"
|
||||||
|
type: lan
|
||||||
|
name: household
|
||||||
|
attributes: {subnet: 192.168.178.0/24} # Fritz LAN; static route to lab subnet
|
||||||
|
- slug: "mesh:netbird"
|
||||||
|
type: mesh
|
||||||
|
name: netbird
|
||||||
|
attributes:
|
||||||
|
provider: netbird
|
||||||
|
subnet: 100.122.0.0/16
|
||||||
|
domain: netbird.selfhosted
|
||||||
|
- slug: "mesh:tailscale"
|
||||||
|
type: mesh
|
||||||
|
name: tailscale
|
||||||
|
state: deprecated # migration to netbird in progress (infrastructure/mesh.md)
|
||||||
|
attributes: {provider: tailscale}
|
||||||
|
- slug: "zone:hubris.network"
|
||||||
|
type: dns-zone
|
||||||
|
name: hubris.network
|
||||||
|
attributes: {zone: hubris.network, authority: "Technitium (LXC 107), split-horizon"}
|
||||||
|
- slug: "zone:netbird.selfhosted"
|
||||||
|
type: dns-zone
|
||||||
|
name: netbird.selfhosted
|
||||||
|
attributes: {zone: netbird.selfhosted, authority: netbird-mgmt}
|
||||||
|
|
||||||
|
# ─── Machines ──────────────────────────────────────────────────────
|
||||||
|
- slug: "cluster:homelab"
|
||||||
|
type: cluster
|
||||||
|
name: Homelab
|
||||||
|
attributes: {quorum: "2-node, no QDevice tiebreaker yet"}
|
||||||
|
- slug: "host:hubris"
|
||||||
|
type: proxmox-host
|
||||||
|
name: hubris
|
||||||
|
attributes:
|
||||||
|
os: linux
|
||||||
|
lan_ip: 192.168.8.77
|
||||||
|
mesh: {netbird: {ip: 100.122.38.109, fqdn: proxmox-server.netbird.selfhosted}}
|
||||||
|
ssh: {port: 22, netbird_port: 22022, user: root}
|
||||||
|
age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
|
||||||
|
- slug: "host:strong"
|
||||||
|
type: proxmox-host
|
||||||
|
name: strong
|
||||||
|
attributes:
|
||||||
|
os: linux
|
||||||
|
lan_ip: 192.168.178.181
|
||||||
|
ssh: {user: root}
|
||||||
|
age_pubkey: age1rtwvdct6avjkr3cyxv3vue3vqx4d524fjfr3vk7xrnvyrylnry5sm54sn4
|
||||||
|
note: >-
|
||||||
|
PVE 9.2.3 since 2026-07-01 (formerly workstation ludo-mini). Joined
|
||||||
|
Homelab cluster same day. Not yet netbird-enrolled — reachable via
|
||||||
|
household LAN / Fritz static route only.
|
||||||
|
- slug: "host:netbird-vps"
|
||||||
|
type: standalone-server
|
||||||
|
name: netbird-vps
|
||||||
|
attributes:
|
||||||
|
os: linux
|
||||||
|
provider: ionos
|
||||||
|
control_level: partial # managed via ssh from hubris; not a homelab client
|
||||||
|
public_ipv4: 82.165.190.79
|
||||||
|
mesh: {netbird: {ip: 100.122.165.149, fqdn: netbird-ionos.netbird.selfhosted}}
|
||||||
|
ssh: {user: root}
|
||||||
|
note: netbird mgmt+signal+relay+dashboard + coturn; sshd locked to hubris pubkey
|
||||||
|
- slug: "ws:mac-mini"
|
||||||
|
type: workstation
|
||||||
|
name: mac-mini
|
||||||
|
attributes:
|
||||||
|
os: macos
|
||||||
|
user: dtoro
|
||||||
|
lan_ip: 192.168.178.182
|
||||||
|
mesh: {netbird: {fqdn: mac-mini-234-17.netbird.selfhosted}}
|
||||||
|
age_pubkey: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
|
||||||
|
note: only macOS in fleet; future Oikos OS Docker host
|
||||||
|
- slug: "ws:republic-laptop"
|
||||||
|
type: workstation
|
||||||
|
name: republic-laptop
|
||||||
|
attributes:
|
||||||
|
os: linux
|
||||||
|
user: dtoro
|
||||||
|
mesh: {netbird: {fqdn: republic-laptop.netbird.selfhosted}}
|
||||||
|
|
||||||
|
# ─── LXCs ──────────────────────────────────────────────────────────
|
||||||
|
- {slug: "lxc:jellyfin", type: lxc, name: jellyfin,
|
||||||
|
attributes: {pve_id: 101, role: media-server, lan_ip: 192.168.8.246,
|
||||||
|
public_host: media.hubris.network,
|
||||||
|
note: "VAAPI transcode via Radeon 680M passthrough; migrated hubris→strong 2026-07-05"}}
|
||||||
|
- {slug: "lxc:nfs-export", type: lxc, name: nfs-export,
|
||||||
|
attributes: {pve_id: 102, role: storage-export, lan_ip: 192.168.8.200}}
|
||||||
|
- {slug: "lxc:paperless", type: lxc, name: paperless,
|
||||||
|
attributes: {pve_id: 103, role: document-archive, lan_ip: 192.168.8.130,
|
||||||
|
public_host: paperless.hubris.network}}
|
||||||
|
- {slug: "lxc:gitea", type: lxc, name: gitea,
|
||||||
|
attributes: {pve_id: 104, role: git-server, lan_ip: 192.168.8.121,
|
||||||
|
public_host: git.hubris.network,
|
||||||
|
note: "bare repos at /mnt/library/repos/dtoro/*.git"}}
|
||||||
|
- {slug: "lxc:apps", type: lxc, name: apps,
|
||||||
|
attributes: {pve_id: 105, role: docker-apps, lan_ip: 192.168.8.205,
|
||||||
|
age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
|
||||||
|
note: "legacy Oikos host; fallback during cutover (plan A6)"}}
|
||||||
|
- {slug: "lxc:auth-outpost", type: lxc, name: auth-outpost,
|
||||||
|
attributes: {pve_id: 106, role: authentik-gateway, lan_ip: 192.168.8.6}}
|
||||||
|
- {slug: "lxc:dns", type: lxc, name: dns,
|
||||||
|
attributes: {pve_id: 107, role: dns-server, lan_ip: 192.168.8.2}}
|
||||||
|
- {slug: "lxc:nextcloud", type: lxc, name: nextcloud,
|
||||||
|
attributes: {pve_id: 114, role: file-sync, lan_ip: 192.168.8.224,
|
||||||
|
public_host: cloud.hubris.network}}
|
||||||
|
- {slug: "lxc:elementsynapse", type: lxc, name: elementsynapse,
|
||||||
|
attributes: {pve_id: 118, role: matrix-server, lan_ip: 192.168.8.242,
|
||||||
|
public_host: matrix.hubris.network,
|
||||||
|
note: "migrated hubris→strong 2026-07-05"}}
|
||||||
|
- {slug: "lxc:sophia", type: lxc, name: sophia,
|
||||||
|
attributes: {pve_id: 119, role: workshop, lan_ip: 192.168.8.109}}
|
||||||
|
- {slug: "lxc:mule-images", type: lxc, name: mule-images,
|
||||||
|
attributes: {pve_id: 120, role: photo-management, lan_ip: 192.168.8.136,
|
||||||
|
public_host: photos.hubris.network}}
|
||||||
|
- {slug: "lxc:caddy", type: lxc, name: caddy,
|
||||||
|
attributes: {pve_id: 121, role: reverse-proxy, lan_ip: 192.168.8.175,
|
||||||
|
note: "terminates all *.hubris.network; /etc/caddy is a checkout of dtoro/caddy-conf"}}
|
||||||
|
- {slug: "lxc:arriman", type: lxc, name: arriman,
|
||||||
|
attributes: {pve_id: 122, role: arr-stack, lan_ip: 192.168.8.245,
|
||||||
|
public_hosts: [jellyseerr.hubris.network, qbit.hubris.network, sab.hubris.network],
|
||||||
|
note: "homarr/radarr/sonarr/lidarr/sab/qbit/bazarr/flaresolverr/prowlarr/jellyseerr; migrated to strong 2026-07-05"}}
|
||||||
|
- {slug: "lxc:trmnl", type: lxc, name: trmnl,
|
||||||
|
attributes: {pve_id: 128, role: trmnl-middleware, lan_ip: 192.168.8.211,
|
||||||
|
public_host: trmnl.hubris.network,
|
||||||
|
note: "not yet mesh/SOPS-enrolled"}}
|
||||||
|
- {slug: "lxc:house", type: lxc, name: house,
|
||||||
|
attributes: {pve_id: 129, role: family-planner, lan_ip: 192.168.8.244,
|
||||||
|
public_host: house.hubris.network,
|
||||||
|
age_pubkey: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h,
|
||||||
|
note: "Yuvomi + WebDAV bridge to paperless; migrated to strong 2026-07-05"}}
|
||||||
|
- {slug: "lxc:grimmory", type: lxc, name: grimmory,
|
||||||
|
attributes: {pve_id: 130, role: book-library, lan_ip: 192.168.8.247,
|
||||||
|
public_host: books.hubris.network,
|
||||||
|
age_pubkey: age1uellsemnjrzgfg9fxw4jefpy05laxzggwnwhh6ny3wl7alyp6v8q0muxet}}
|
||||||
|
- {slug: "lxc:teddycloud", type: lxc, name: teddycloud,
|
||||||
|
attributes: {pve_id: 131, role: teddycloud, lan_ip: 192.168.8.150,
|
||||||
|
public_host: teddy.hubris.network,
|
||||||
|
note: "drift-caught 2026-07-06; no forward-auth gate on route; not a homelab client"}}
|
||||||
|
- {slug: "lxc:rclone", type: lxc, name: rclone,
|
||||||
|
attributes: {pve_id: 132, role: backup,
|
||||||
|
mesh: {netbird: {fqdn: rclone.netbird.selfhosted}},
|
||||||
|
age_pubkey: age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x}}
|
||||||
|
# THIN: hosting machine not recorded in legacy inventory — backfill hosts edge
|
||||||
|
- {slug: "lxc:seanime", type: lxc, name: seanime,
|
||||||
|
attributes: {pve_id: 133, role: anime-media-server, lan_ip: 192.168.8.248,
|
||||||
|
public_host: seanime.hubris.network,
|
||||||
|
note: "systemd service at /opt/seanime; uses qbittorrent on arriman"}}
|
||||||
|
- {slug: "lxc:romm", type: lxc, name: romm,
|
||||||
|
attributes: {pve_id: 134, role: rom-manager, lan_ip: 192.168.8.249,
|
||||||
|
public_host: roms.hubris.network,
|
||||||
|
note: "docker compose + MariaDB sidecar at /opt/romm"}}
|
||||||
|
|
||||||
|
# ─── VMs ───────────────────────────────────────────────────────────
|
||||||
|
- {slug: "vm:zimaos", type: vm, name: zimaos,
|
||||||
|
attributes: {pve_id: 100, role: nas-frontend-eval, lan_ip: 192.168.8.195,
|
||||||
|
public_host: zimaos.hubris.network}}
|
||||||
|
- {slug: "vm:haos", type: vm, name: haos,
|
||||||
|
attributes: {pve_id: 108, role: home-automation, lan_ip: 192.168.8.101}}
|
||||||
|
|
||||||
|
# ─── Storage ───────────────────────────────────────────────────────
|
||||||
|
- {slug: "pool:local-lvm-hubris", type: storage-pool, name: local-lvm (hubris),
|
||||||
|
attributes: {type: lvm}}
|
||||||
|
- {slug: "pool:ludo-lvm", type: storage-pool, name: ludo-lvm (strong),
|
||||||
|
attributes: {type: lvm}}
|
||||||
|
- {slug: "volume:library", type: volume, name: library,
|
||||||
|
attributes: {path: /mnt/library}} # THIN: owning pool unrecorded
|
||||||
|
- {slug: "volume:media-local", type: volume, name: media-local,
|
||||||
|
attributes: {path: /mnt/media_local}}
|
||||||
|
- {slug: "backup:proton-drive", type: backup-target, name: proton-drive,
|
||||||
|
attributes: {provider: proton, encrypted: true}}
|
||||||
|
|
||||||
|
# ─── Services ──────────────────────────────────────────────────────
|
||||||
|
- {slug: "service:proxmox-ui", type: service, name: proxmox_ui,
|
||||||
|
attributes: {url: "https://proxmox.hubris.network", port: 8006,
|
||||||
|
doc_page: knowledge/wiki/hosts/hubris.md,
|
||||||
|
risk_notes: "hypervisor UI — changes affect every guest on the node"}}
|
||||||
|
- {slug: "service:gitea", type: service, name: gitea,
|
||||||
|
attributes: {url: "https://git.hubris.network", port: 3000,
|
||||||
|
doc_page: knowledge/wiki/containers/104-gitea.md,
|
||||||
|
risk_notes: "hosts all config repos + deploy webhooks; outage blocks auto-deploy and sync"}}
|
||||||
|
- {slug: "service:caddy", type: service, name: caddy,
|
||||||
|
attributes: {doc_page: knowledge/wiki/containers/121-caddy.md,
|
||||||
|
risk_notes: "wide blast radius — every *.hubris.network route rides on it"}}
|
||||||
|
- {slug: "service:authentik", type: service, name: authentik,
|
||||||
|
attributes: {url: "https://auth.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/106-auth-outpost.md,
|
||||||
|
note: "core on VPS since 2026-05-31; LAN outpost = auth-outpost (LXC 106) 192.168.8.6:9000",
|
||||||
|
risk_notes: "SSO provider — outage locks login to OIDC/forward-auth services"}}
|
||||||
|
- {slug: "service:dns", type: service, name: dns,
|
||||||
|
attributes: {doc_page: knowledge/wiki/containers/107-dns.md,
|
||||||
|
risk_notes: "LAN-wide resolver — misconfig breaks name resolution for every client"}}
|
||||||
|
- {slug: "service:jellyfin", type: service, name: jellyfin,
|
||||||
|
attributes: {url: "https://media.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/101-jellyfin.md,
|
||||||
|
risk_notes: "native Authentik OIDC (no forward-auth gate); VAAPI depends on GPU passthrough on strong"}}
|
||||||
|
- {slug: "service:nextcloud", type: service, name: nextcloud,
|
||||||
|
attributes: {url: "https://cloud.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/114-nextcloud.md}}
|
||||||
|
- {slug: "service:paperless", type: service, name: paperless,
|
||||||
|
attributes: {url: "https://paperless.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/103-paperless.md,
|
||||||
|
risk_notes: "document archive — data irreplaceable; DB operations are destructive-class"}}
|
||||||
|
- {slug: "service:matrix", type: service, name: matrix,
|
||||||
|
attributes: {url: "https://matrix.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/118-elementsynapse.md,
|
||||||
|
risk_notes: "alert/approval channel for Oikos — outage silences agent escalation"}}
|
||||||
|
- {slug: "service:photos", type: service, name: photos,
|
||||||
|
attributes: {url: "https://photos.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/120-mule-images.md}}
|
||||||
|
- {slug: "service:arr-stack", type: service, name: arr_stack,
|
||||||
|
attributes: {doc_page: knowledge/wiki/containers/122-arriman.md,
|
||||||
|
note: "jellyseerr / qbit / sab on docker compose"}}
|
||||||
|
- {slug: "service:artifacto", type: service, name: artifacto,
|
||||||
|
attributes: {url: "https://artifacto.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/105-apps.md}}
|
||||||
|
- {slug: "service:trmnl", type: service, name: trmnl,
|
||||||
|
attributes: {url: "https://trmnl.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/128-trmnl.md,
|
||||||
|
note: "TRMNL e-ink plugin middleware (polled by TRMNL cloud)"}}
|
||||||
|
- {slug: "service:zimaos", type: service, name: zimaos,
|
||||||
|
attributes: {url: "https://zimaos.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/vms/100-zimaos.md}}
|
||||||
|
- {slug: "service:haos", type: service, name: haos,
|
||||||
|
attributes: {doc_page: knowledge/wiki/vms/108-haos.md}}
|
||||||
|
- {slug: "service:teddycloud", type: service, name: teddycloud,
|
||||||
|
attributes: {url: "https://teddy.hubris.network",
|
||||||
|
doc_page: knowledge/wiki/containers/131-teddycloud.md,
|
||||||
|
risk_notes: "no forward-auth gate — reachable by anyone on LAN/mesh"}}
|
||||||
|
- {slug: "service:homelab-mcp", type: service, name: homelab_mcp,
|
||||||
|
attributes: {port: 9810, systemd_unit: homelab-mcp,
|
||||||
|
endpoint: "https://mcp.hubris.network/mcp",
|
||||||
|
doc_page: knowledge/wiki/infrastructure/homelab-context.md,
|
||||||
|
risk_notes: "agents' primary read surface — outage degrades every agent to grepping the clone"}}
|
||||||
|
- {slug: "service:secrets-issuance", type: service, name: secrets_issuance,
|
||||||
|
attributes: {port: 9820, systemd_unit: secrets-issuance,
|
||||||
|
endpoint: "https://secrets.hubris.network/issue",
|
||||||
|
doc_page: .agents/operations/agent-enrollment.md,
|
||||||
|
risk_notes: "identity issuance — security-sensitive; key operations are destructive-class"}}
|
||||||
|
# Services derived from hosts.public_host (no legacy services entry):
|
||||||
|
- {slug: "service:house", type: service, name: house,
|
||||||
|
attributes: {url: "https://house.hubris.network", note: "Yuvomi family planner (derived)"}}
|
||||||
|
- {slug: "service:grimmory", type: service, name: grimmory,
|
||||||
|
attributes: {url: "https://books.hubris.network", note: derived}}
|
||||||
|
- {slug: "service:seanime", type: service, name: seanime,
|
||||||
|
attributes: {url: "https://seanime.hubris.network", port: 43211, note: derived}}
|
||||||
|
- {slug: "service:romm", type: service, name: romm,
|
||||||
|
attributes: {url: "https://roms.hubris.network", note: derived}}
|
||||||
|
- {slug: "service:jellyseerr", type: service, name: jellyseerr,
|
||||||
|
attributes: {url: "https://jellyseerr.hubris.network", note: derived (arriman)}}
|
||||||
|
- {slug: "service:qbit", type: service, name: qbit,
|
||||||
|
attributes: {url: "https://qbit.hubris.network", note: derived (arriman)}}
|
||||||
|
- {slug: "service:sab", type: service, name: sab,
|
||||||
|
attributes: {url: "https://sab.hubris.network", note: "derived (arriman); forward-auth gated"}}
|
||||||
|
|
||||||
|
# ─── Config repos ──────────────────────────────────────────────────
|
||||||
|
- {slug: "repo:caddy-conf", type: config-repo, name: dtoro/caddy-conf}
|
||||||
|
- {slug: "repo:gitea-customizations", type: config-repo, name: dtoro/gitea-customizations}
|
||||||
|
- {slug: "repo:mule-image", type: config-repo, name: dtoro/mule-image}
|
||||||
|
- {slug: "repo:artifacto", type: config-repo, name: dtoro/Artifacto}
|
||||||
|
- {slug: "repo:terminalito", type: config-repo, name: dtoro/terminalito}
|
||||||
|
- {slug: "repo:homelab-docs", type: config-repo, name: dtoro/Homelab-Docs}
|
||||||
|
|
||||||
|
# ─── Ingress routes (Caddy, *.hubris.network) ──────────────────────
|
||||||
|
- {slug: "ingress:proxmox.hubris.network", type: ingress-route, name: proxmox.hubris.network}
|
||||||
|
- {slug: "ingress:git.hubris.network", type: ingress-route, name: git.hubris.network}
|
||||||
|
- {slug: "ingress:auth.hubris.network", type: ingress-route, name: auth.hubris.network}
|
||||||
|
- {slug: "ingress:media.hubris.network", type: ingress-route, name: media.hubris.network}
|
||||||
|
- {slug: "ingress:cloud.hubris.network", type: ingress-route, name: cloud.hubris.network}
|
||||||
|
- {slug: "ingress:paperless.hubris.network", type: ingress-route, name: paperless.hubris.network,
|
||||||
|
attributes: {forward_auth: true}}
|
||||||
|
- {slug: "ingress:matrix.hubris.network", type: ingress-route, name: matrix.hubris.network}
|
||||||
|
- {slug: "ingress:photos.hubris.network", type: ingress-route, name: photos.hubris.network}
|
||||||
|
- {slug: "ingress:artifacto.hubris.network", type: ingress-route, name: artifacto.hubris.network}
|
||||||
|
- {slug: "ingress:trmnl.hubris.network", type: ingress-route, name: trmnl.hubris.network}
|
||||||
|
- {slug: "ingress:zimaos.hubris.network", type: ingress-route, name: zimaos.hubris.network}
|
||||||
|
- {slug: "ingress:teddy.hubris.network", type: ingress-route, name: teddy.hubris.network,
|
||||||
|
attributes: {forward_auth: false}}
|
||||||
|
- {slug: "ingress:mcp.hubris.network", type: ingress-route, name: mcp.hubris.network}
|
||||||
|
- {slug: "ingress:secrets.hubris.network", type: ingress-route, name: secrets.hubris.network}
|
||||||
|
- {slug: "ingress:house.hubris.network", type: ingress-route, name: house.hubris.network}
|
||||||
|
- {slug: "ingress:books.hubris.network", type: ingress-route, name: books.hubris.network}
|
||||||
|
- {slug: "ingress:seanime.hubris.network", type: ingress-route, name: seanime.hubris.network}
|
||||||
|
- {slug: "ingress:roms.hubris.network", type: ingress-route, name: roms.hubris.network}
|
||||||
|
- {slug: "ingress:jellyseerr.hubris.network", type: ingress-route, name: jellyseerr.hubris.network}
|
||||||
|
- {slug: "ingress:qbit.hubris.network", type: ingress-route, name: qbit.hubris.network}
|
||||||
|
- {slug: "ingress:sab.hubris.network", type: ingress-route, name: sab.hubris.network,
|
||||||
|
attributes: {forward_auth: true}}
|
||||||
|
|
||||||
|
# ─── Governance ────────────────────────────────────────────────────
|
||||||
|
- {slug: "person:dtoro", type: person, name: dtoro,
|
||||||
|
attributes: {matrix_id: "@dtoro:avispero"}}
|
||||||
|
- {slug: "idp:authentik", type: identity-provider, name: authentik,
|
||||||
|
attributes: {issuer: "https://auth.hubris.network", auth_mode: both}}
|
||||||
|
- {slug: "agent:hermes", type: agent, name: hermes,
|
||||||
|
state: planned,
|
||||||
|
attributes: {gateway_port: 8092, note: "Oikos Phase 4 — Docker gateway mode"}}
|
||||||
|
- {slug: "agent:oikos", type: agent, name: oikos,
|
||||||
|
state: planned,
|
||||||
|
attributes: {note: "the OS control loop itself (scheduler/actuator) as an actor"}}
|
||||||
|
|
||||||
|
# ─── Archaeology (state: destroyed — kept for "what happened to X?") ─
|
||||||
|
- {slug: "lxc:claudio-bot", type: lxc, name: claudio-bot, state: destroyed,
|
||||||
|
attributes: {pve_id: 123, destroyed: "2026-06-04", reason: "replaced by Hermes Agent on mac-mini"}}
|
||||||
|
- {slug: "lxc:plato", type: lxc, name: plato, state: destroyed,
|
||||||
|
attributes: {pve_id: 126, destroyed: "2026-06-28", reason: "notes workspace decommissioned; data at /mnt/library/documents/plato"}}
|
||||||
|
- {slug: "lxc:mule-photos-new", type: lxc, name: mule-photos-new, state: destroyed,
|
||||||
|
attributes: {pve_id: 127, destroyed: "2026-05-22", reason: "PhotoPrism test stack promoted to LXC 120"}}
|
||||||
|
- {slug: "lxc:heaper", type: lxc, name: heaper, state: destroyed,
|
||||||
|
attributes: {pve_id: 116, destroyed: "2026-05-14", reason: "decommissioned; data at /mnt/library/heaper"}}
|
||||||
|
- {slug: "lxc:syncthing", type: lxc, name: syncthing, state: destroyed,
|
||||||
|
attributes: {pve_id: 109, destroyed: "2026-05-14", reason: "decommissioned; library subtree was empty"}}
|
||||||
|
- {slug: "lxc:seafile", type: lxc, name: seafile, state: destroyed,
|
||||||
|
attributes: {pve_id: 125, destroyed: "2026-05-13", reason: "Seafile Pro evaluation rejected"}}
|
||||||
|
- {slug: "lxc:arr-yunohost", type: lxc, name: arr-yunohost, state: destroyed,
|
||||||
|
attributes: {pve_id: 100, destroyed: "2026-04-28", reason: "migrated to docker stack on arriman (LXC 122)"}}
|
||||||
|
- {slug: "lxc:flaresolverr", type: lxc, name: flaresolverr, state: destroyed,
|
||||||
|
attributes: {pve_id: 106, destroyed: "2026-04-28", reason: "folded into the arriman docker compose"}}
|
||||||
|
- {slug: "lxc:marimo", type: lxc, name: marimo, state: destroyed,
|
||||||
|
attributes: {pve_id: 107, destroyed: "2026-04-28", reason: decommissioned}}
|
||||||
|
- {slug: "lxc:photoprism", type: lxc, name: photoprism, state: destroyed,
|
||||||
|
attributes: {pve_id: 110, destroyed: "2026-04-28", reason: "replaced by mule-images (LXC 120)"}}
|
||||||
|
- {slug: "lxc:karakeep", type: lxc, name: karakeep, state: destroyed,
|
||||||
|
attributes: {pve_id: 111, destroyed: "2026-04-28", reason: decommissioned}}
|
||||||
|
- {slug: "lxc:immich", type: lxc, name: immich, state: destroyed,
|
||||||
|
attributes: {pve_id: 112, destroyed: "2026-04-28", reason: "replaced by mule-images (LXC 120)"}}
|
||||||
|
- {slug: "lxc:reticulum", type: lxc, name: reticulum, state: destroyed,
|
||||||
|
attributes: {pve_id: 115, destroyed: "2026-04-28", reason: decommissioned}}
|
||||||
|
|
||||||
|
relationships:
|
||||||
|
|
||||||
|
# ─── Cluster membership ────────────────────────────────────────────
|
||||||
|
- {source: "host:hubris", target: "cluster:homelab", type: member-of}
|
||||||
|
- {source: "host:strong", target: "cluster:homelab", type: member-of}
|
||||||
|
|
||||||
|
# ─── Location ──────────────────────────────────────────────────────
|
||||||
|
- {source: "host:hubris", target: "site:home", type: located-at}
|
||||||
|
- {source: "host:strong", target: "site:home", type: located-at}
|
||||||
|
- {source: "ws:mac-mini", target: "site:home", type: located-at}
|
||||||
|
- {source: "host:netbird-vps", target: "site:ionos-dc", type: located-at}
|
||||||
|
|
||||||
|
# ─── Hosting (machine → guest) ─────────────────────────────────────
|
||||||
|
- {source: "host:hubris", target: "lxc:nfs-export", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:paperless", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:gitea", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:apps", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:auth-outpost", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:dns", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:nextcloud", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:sophia", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:mule-images", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:caddy", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:trmnl", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "lxc:teddycloud", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "vm:zimaos", type: hosts}
|
||||||
|
- {source: "host:hubris", target: "vm:haos", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:jellyfin", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:elementsynapse", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:arriman", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:house", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:grimmory", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:seanime", type: hosts}
|
||||||
|
- {source: "host:strong", target: "lxc:romm", type: hosts}
|
||||||
|
# THIN: lxc:rclone hosting machine unknown — backfill
|
||||||
|
|
||||||
|
# ─── Service provision (compute → service) ─────────────────────────
|
||||||
|
- {source: "host:hubris", target: "service:proxmox-ui", type: provides}
|
||||||
|
- {source: "lxc:gitea", target: "service:gitea", type: provides}
|
||||||
|
- {source: "lxc:caddy", target: "service:caddy", type: provides}
|
||||||
|
- {source: "host:netbird-vps", target: "service:authentik", type: provides}
|
||||||
|
- {source: "lxc:dns", target: "service:dns", type: provides}
|
||||||
|
- {source: "lxc:jellyfin", target: "service:jellyfin", type: provides}
|
||||||
|
- {source: "lxc:nextcloud", target: "service:nextcloud", type: provides}
|
||||||
|
- {source: "lxc:paperless", target: "service:paperless", type: provides}
|
||||||
|
- {source: "lxc:elementsynapse", target: "service:matrix", type: provides}
|
||||||
|
- {source: "lxc:mule-images", target: "service:photos", type: provides}
|
||||||
|
- {source: "lxc:arriman", target: "service:arr-stack", type: provides}
|
||||||
|
- {source: "lxc:arriman", target: "service:jellyseerr", type: provides}
|
||||||
|
- {source: "lxc:arriman", target: "service:qbit", type: provides}
|
||||||
|
- {source: "lxc:arriman", target: "service:sab", type: provides}
|
||||||
|
- {source: "lxc:apps", target: "service:artifacto", type: provides}
|
||||||
|
- {source: "lxc:apps", target: "service:homelab-mcp", type: provides}
|
||||||
|
- {source: "lxc:apps", target: "service:secrets-issuance", type: provides}
|
||||||
|
- {source: "lxc:trmnl", target: "service:trmnl", type: provides}
|
||||||
|
- {source: "vm:zimaos", target: "service:zimaos", type: provides}
|
||||||
|
- {source: "vm:haos", target: "service:haos", type: provides}
|
||||||
|
- {source: "lxc:teddycloud", target: "service:teddycloud", type: provides}
|
||||||
|
- {source: "lxc:house", target: "service:house", type: provides}
|
||||||
|
- {source: "lxc:grimmory", target: "service:grimmory", type: provides}
|
||||||
|
- {source: "lxc:seanime", target: "service:seanime", type: provides}
|
||||||
|
- {source: "lxc:romm", target: "service:romm", type: provides}
|
||||||
|
|
||||||
|
# ─── Ingress → service ─────────────────────────────────────────────
|
||||||
|
- {source: "ingress:proxmox.hubris.network", target: "service:proxmox-ui", type: routes-to}
|
||||||
|
- {source: "ingress:git.hubris.network", target: "service:gitea", type: routes-to}
|
||||||
|
- {source: "ingress:auth.hubris.network", target: "service:authentik", type: routes-to}
|
||||||
|
- {source: "ingress:media.hubris.network", target: "service:jellyfin", type: routes-to}
|
||||||
|
- {source: "ingress:cloud.hubris.network", target: "service:nextcloud", type: routes-to}
|
||||||
|
- {source: "ingress:paperless.hubris.network", target: "service:paperless", type: routes-to}
|
||||||
|
- {source: "ingress:matrix.hubris.network", target: "service:matrix", type: routes-to}
|
||||||
|
- {source: "ingress:photos.hubris.network", target: "service:photos", type: routes-to}
|
||||||
|
- {source: "ingress:artifacto.hubris.network", target: "service:artifacto", type: routes-to}
|
||||||
|
- {source: "ingress:trmnl.hubris.network", target: "service:trmnl", type: routes-to}
|
||||||
|
- {source: "ingress:zimaos.hubris.network", target: "service:zimaos", type: routes-to}
|
||||||
|
- {source: "ingress:teddy.hubris.network", target: "service:teddycloud", type: routes-to}
|
||||||
|
- {source: "ingress:mcp.hubris.network", target: "service:homelab-mcp", type: routes-to}
|
||||||
|
- {source: "ingress:secrets.hubris.network", target: "service:secrets-issuance", type: routes-to}
|
||||||
|
- {source: "ingress:house.hubris.network", target: "service:house", type: routes-to}
|
||||||
|
- {source: "ingress:books.hubris.network", target: "service:grimmory", type: routes-to}
|
||||||
|
- {source: "ingress:seanime.hubris.network", target: "service:seanime", type: routes-to}
|
||||||
|
- {source: "ingress:roms.hubris.network", target: "service:romm", type: routes-to}
|
||||||
|
- {source: "ingress:jellyseerr.hubris.network", target: "service:jellyseerr", type: routes-to}
|
||||||
|
- {source: "ingress:qbit.hubris.network", target: "service:qbit", type: routes-to}
|
||||||
|
- {source: "ingress:sab.hubris.network", target: "service:sab", type: routes-to}
|
||||||
|
|
||||||
|
# ─── Auth edges ────────────────────────────────────────────────────
|
||||||
|
- {source: "ingress:paperless.hubris.network", target: "idp:authentik", type: secured-by}
|
||||||
|
- {source: "ingress:sab.hubris.network", target: "idp:authentik", type: secured-by}
|
||||||
|
- {source: "service:jellyfin", target: "idp:authentik", type: authenticates-via}
|
||||||
|
- {source: "idp:authentik", target: "person:dtoro", type: authenticates}
|
||||||
|
|
||||||
|
# ─── Config repos ──────────────────────────────────────────────────
|
||||||
|
- {source: "service:caddy", target: "repo:caddy-conf", type: configured-by}
|
||||||
|
- {source: "service:gitea", target: "repo:gitea-customizations", type: configured-by}
|
||||||
|
- {source: "service:photos", target: "repo:mule-image", type: configured-by}
|
||||||
|
- {source: "service:artifacto", target: "repo:artifacto", type: configured-by}
|
||||||
|
- {source: "service:trmnl", target: "repo:terminalito", type: configured-by}
|
||||||
|
- {source: "service:homelab-mcp", target: "repo:homelab-docs", type: configured-by}
|
||||||
|
- {source: "service:secrets-issuance", target: "repo:homelab-docs", type: configured-by}
|
||||||
|
|
||||||
|
# ─── Service dependencies (blast-radius edges; grow over time) ─────
|
||||||
|
- {source: "service:paperless", target: "service:authentik", type: depends-on}
|
||||||
|
- {source: "service:homelab-mcp", target: "service:gitea", type: depends-on}
|
||||||
|
- {source: "service:jellyseerr", target: "service:jellyfin", type: depends-on}
|
||||||
|
- {source: "service:seanime", target: "service:qbit", type: depends-on}
|
||||||
|
- {source: "service:house", target: "service:paperless", type: depends-on}
|
||||||
|
- {source: "service:sab", target: "service:authentik", type: depends-on}
|
||||||
|
|
||||||
|
# ─── Network membership ────────────────────────────────────────────
|
||||||
|
- {source: "host:hubris", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "host:hubris", target: "mesh:netbird", type: connects-via}
|
||||||
|
- {source: "host:strong", target: "lan:household", type: connects-via}
|
||||||
|
- {source: "ws:mac-mini", target: "lan:household", type: connects-via}
|
||||||
|
- {source: "ws:mac-mini", target: "mesh:netbird", type: connects-via}
|
||||||
|
- {source: "ws:republic-laptop", target: "mesh:netbird", type: connects-via}
|
||||||
|
- {source: "host:netbird-vps", target: "mesh:netbird", type: connects-via}
|
||||||
|
- {source: "lxc:rclone", target: "mesh:netbird", type: connects-via}
|
||||||
|
- {source: "lxc:jellyfin", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:nfs-export", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:paperless", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:gitea", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:apps", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:apps", target: "mesh:tailscale", type: connects-via}
|
||||||
|
- {source: "lxc:auth-outpost", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:dns", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:nextcloud", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:elementsynapse", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:sophia", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:mule-images", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:caddy", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:arriman", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:trmnl", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:house", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:grimmory", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:teddycloud", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:seanime", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "lxc:romm", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "vm:zimaos", target: "lan:lab", type: connects-via}
|
||||||
|
- {source: "vm:haos", target: "lan:lab", type: connects-via}
|
||||||
|
|
||||||
|
# ─── Storage ───────────────────────────────────────────────────────
|
||||||
|
- {source: "pool:ludo-lvm", target: "volume:media-local", type: contains}
|
||||||
|
- {source: "host:hubris", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:jellyfin", target: "volume:media-local", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/media_local}}
|
||||||
|
- {source: "lxc:paperless", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:gitea", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:apps", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:nextcloud", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:sophia", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:mule-images", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:arriman", target: "volume:media-local", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/media_local}}
|
||||||
|
- {source: "lxc:grimmory", target: "volume:media-local", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/media_local}}
|
||||||
|
- {source: "lxc:teddycloud", target: "volume:library", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/library}}
|
||||||
|
- {source: "lxc:seanime", target: "volume:media-local", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/media_local/anime}}
|
||||||
|
- {source: "lxc:romm", target: "volume:media-local", type: mounts,
|
||||||
|
attributes: {mount_point: /mnt/media_local}}
|
||||||
|
- {source: "lxc:jellyfin", target: "pool:ludo-lvm", type: stores-on}
|
||||||
|
- {source: "lxc:arriman", target: "pool:ludo-lvm", type: stores-on}
|
||||||
|
- {source: "lxc:grimmory", target: "pool:ludo-lvm", type: stores-on}
|
||||||
|
- {source: "lxc:romm", target: "pool:ludo-lvm", type: stores-on}
|
||||||
|
- {source: "lxc:teddycloud", target: "pool:local-lvm-hubris", type: stores-on}
|
||||||
|
- {source: "lxc:rclone", target: "backup:proton-drive", type: backs-up-to}
|
||||||
|
|
||||||
|
# ─── Governance ────────────────────────────────────────────────────
|
||||||
|
- {source: "person:dtoro", target: "agent:hermes", type: owns}
|
||||||
|
- {source: "person:dtoro", target: "agent:oikos", type: owns}
|
||||||
936
seeds/ontology.yaml
Normal file
936
seeds/ontology.yaml
Normal file
@@ -0,0 +1,936 @@
|
|||||||
|
# Oikos ontology seed — the systems model of the homelab.
|
||||||
|
#
|
||||||
|
# Bootstraps entity_types / relationship_types / lifecycle_defs on first
|
||||||
|
# deploy (migration 001). After ingest the DB is authoritative; this file
|
||||||
|
# is regenerated by `GET /api/v1/export` for DR + version control.
|
||||||
|
#
|
||||||
|
# Conventions:
|
||||||
|
# - entity type names are kebab-case
|
||||||
|
# - `parent:` builds the is-a hierarchy; `abstract: true` types cannot be
|
||||||
|
# instantiated (validation walks the hierarchy for relationship
|
||||||
|
# endpoints and policy rules — plan R3-1)
|
||||||
|
# - `layer:` one of meta | infrastructure | governance | cognition
|
||||||
|
# - relationship `cardinality:` describes source→target multiplicity:
|
||||||
|
# one-to-one | one-to-many | many-to-one | many-to-many
|
||||||
|
# - relationship endpoints may name abstract types
|
||||||
|
# - lifecycle transition `requires:` entries are NAMED CHECKS implemented
|
||||||
|
# in Go (internal/ontology); the DB stores which checks gate a
|
||||||
|
# transition, the code implements them
|
||||||
|
# - mount details (mount_point, options) live as ATTRIBUTES on `mounts`
|
||||||
|
# edges, not as a separate entity type
|
||||||
|
#
|
||||||
|
# Rule of completeness: if something can break, be changed, or hold data,
|
||||||
|
# it has an entity type here and edges to the things it touches.
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
# ─── Lifecycles ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
lifecycles:
|
||||||
|
infrastructure:
|
||||||
|
states: [planned, provisioning, active, migrating, failed, deprecated, destroyed]
|
||||||
|
default_state: active # legacy inventory entries without state are active
|
||||||
|
terminal_states: [destroyed]
|
||||||
|
transitions:
|
||||||
|
planned:
|
||||||
|
provisioning: {requires: [inventory-entry, ip-reserved, storage-pool-chosen, doc-page-stub]}
|
||||||
|
destroyed: {requires: [cancelled-note]}
|
||||||
|
provisioning:
|
||||||
|
active: {requires: [age-key-enrolled-if-needed, mesh-joined-if-needed,
|
||||||
|
ingress-live-if-public, health-check-answering,
|
||||||
|
doc-page-complete]}
|
||||||
|
failed: {requires: []}
|
||||||
|
active:
|
||||||
|
migrating: {requires: [preflight, backup-verified]}
|
||||||
|
deprecated: {requires: [replacement-live-or-role-retired]}
|
||||||
|
failed: {requires: []}
|
||||||
|
migrating:
|
||||||
|
active: {requires: [post-verify, caddy-backends-checked, mounts-checked, docs-updated]}
|
||||||
|
failed: {requires: []}
|
||||||
|
failed:
|
||||||
|
active: {requires: [recovery-verified]}
|
||||||
|
deprecated: {requires: [write-off-note]}
|
||||||
|
deprecated:
|
||||||
|
active: {requires: [un-deprecate-note]}
|
||||||
|
destroyed: {requires: [backups-verified, secrets-revoked-and-rekeyed,
|
||||||
|
ingress-and-dns-removed, no-inbound-edges,
|
||||||
|
archaeology-entry]}
|
||||||
|
|
||||||
|
signal:
|
||||||
|
states: [raised, acknowledged, acting, muted, resolved, failed]
|
||||||
|
default_state: raised
|
||||||
|
terminal_states: [resolved]
|
||||||
|
transitions:
|
||||||
|
raised:
|
||||||
|
acknowledged: {requires: []}
|
||||||
|
muted: {requires: [mute-ttl-set]}
|
||||||
|
resolved: {requires: [condition-cleared]}
|
||||||
|
acknowledged:
|
||||||
|
acting: {requires: [classification-exists]}
|
||||||
|
resolved: {requires: []}
|
||||||
|
muted: {requires: [mute-ttl-set]}
|
||||||
|
acting:
|
||||||
|
resolved: {requires: [verification-passed]}
|
||||||
|
raised: {requires: [retry-budget-remaining]}
|
||||||
|
failed: {requires: []}
|
||||||
|
failed:
|
||||||
|
acknowledged: {requires: [operator-retry]}
|
||||||
|
muted:
|
||||||
|
raised: {requires: [mute-ttl-expired]}
|
||||||
|
|
||||||
|
execution:
|
||||||
|
states: [proposed, approved, auto_approved, denied, expired, executing,
|
||||||
|
verifying, verified, failed, timed_out, cancelled, rolled_back,
|
||||||
|
rollback_failed]
|
||||||
|
default_state: proposed
|
||||||
|
terminal_states: [verified, failed, denied, expired, cancelled,
|
||||||
|
rolled_back, rollback_failed]
|
||||||
|
transitions:
|
||||||
|
proposed:
|
||||||
|
approved: {requires: [operator-approval]}
|
||||||
|
auto_approved: {requires: [autonomy-allows]}
|
||||||
|
denied: {requires: []}
|
||||||
|
approved:
|
||||||
|
executing: {requires: [approval-token-valid]}
|
||||||
|
expired: {requires: [approval-ttl-elapsed]}
|
||||||
|
auto_approved:
|
||||||
|
executing: {requires: []}
|
||||||
|
executing:
|
||||||
|
verified: {requires: [verification-passed]}
|
||||||
|
failed: {requires: []}
|
||||||
|
timed_out: {requires: []}
|
||||||
|
cancelled: {requires: [operator-abort]}
|
||||||
|
timed_out:
|
||||||
|
verifying: {requires: []} # check if the command completed anyway
|
||||||
|
verifying:
|
||||||
|
verified: {requires: [verification-passed]}
|
||||||
|
failed: {requires: []}
|
||||||
|
failed:
|
||||||
|
rolled_back: {requires: [rollback-procedure-exists]}
|
||||||
|
rollback_failed: {requires: []}
|
||||||
|
|
||||||
|
approval:
|
||||||
|
states: [pending, approved, denied, expired, revoked]
|
||||||
|
default_state: pending
|
||||||
|
terminal_states: [denied, expired, revoked]
|
||||||
|
transitions:
|
||||||
|
pending:
|
||||||
|
approved: {requires: [token-verified]}
|
||||||
|
denied: {requires: []}
|
||||||
|
expired: {requires: [ttl-elapsed]}
|
||||||
|
approved:
|
||||||
|
revoked: {requires: [not-yet-executing]}
|
||||||
|
|
||||||
|
pattern:
|
||||||
|
states: [hypothesized, validated, active, deprecated, invalidated]
|
||||||
|
default_state: hypothesized
|
||||||
|
terminal_states: [deprecated, invalidated]
|
||||||
|
transitions:
|
||||||
|
hypothesized:
|
||||||
|
validated: {requires: [evidence-count-5plus, confidence-0.7plus]}
|
||||||
|
invalidated: {requires: []}
|
||||||
|
validated:
|
||||||
|
active: {requires: [operator-approval]} # S4: never automatic
|
||||||
|
invalidated: {requires: []}
|
||||||
|
active:
|
||||||
|
deprecated: {requires: []}
|
||||||
|
invalidated: {requires: [contradicting-evidence]}
|
||||||
|
|
||||||
|
skill:
|
||||||
|
states: [drafted, tested, active, refined, failed, deprecated]
|
||||||
|
default_state: drafted
|
||||||
|
terminal_states: [deprecated]
|
||||||
|
transitions:
|
||||||
|
drafted:
|
||||||
|
tested: {requires: [test-execution-recorded]}
|
||||||
|
deprecated: {requires: []}
|
||||||
|
tested:
|
||||||
|
active: {requires: [operator-approval]}
|
||||||
|
failed: {requires: []}
|
||||||
|
failed:
|
||||||
|
drafted: {requires: []}
|
||||||
|
active:
|
||||||
|
refined: {requires: [new-version-created]}
|
||||||
|
deprecated: {requires: []}
|
||||||
|
refined:
|
||||||
|
active: {requires: [operator-approval]}
|
||||||
|
|
||||||
|
# ─── Entity types ──────────────────────────────────────────────────────
|
||||||
|
# domain: physical | compute | network | storage | software | external |
|
||||||
|
# identity | cognition
|
||||||
|
|
||||||
|
entity_types:
|
||||||
|
|
||||||
|
# Root
|
||||||
|
entity:
|
||||||
|
abstract: true
|
||||||
|
domain: meta
|
||||||
|
layer: meta
|
||||||
|
description: Root abstract type. Relationship endpoints that accept any
|
||||||
|
entity (documented-by, procedure-for, checks) reference this.
|
||||||
|
|
||||||
|
# ── Infrastructure / physical ──
|
||||||
|
site:
|
||||||
|
parent: entity
|
||||||
|
domain: physical
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Physical location (home, VPS datacenter).
|
||||||
|
attributes: {type: object, properties: {address: {type: string}}}
|
||||||
|
ups:
|
||||||
|
parent: entity
|
||||||
|
domain: physical
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Uninterruptible power supply.
|
||||||
|
attributes: {type: object, properties: {vendor: {type: string}, va: {type: integer}}}
|
||||||
|
sensor:
|
||||||
|
parent: entity
|
||||||
|
domain: physical
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Environmental sensor.
|
||||||
|
peripheral:
|
||||||
|
parent: entity
|
||||||
|
domain: physical
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Attached hardware (GPU, e-ink display, dongle).
|
||||||
|
|
||||||
|
# ── Infrastructure / compute ──
|
||||||
|
compute-entity:
|
||||||
|
parent: entity
|
||||||
|
abstract: true
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
description: Anything that executes workloads (machine, VM, container).
|
||||||
|
machine:
|
||||||
|
parent: compute-entity
|
||||||
|
abstract: true
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
description: Physical machine. Always instantiated as a subtype.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cpu_arch: {type: string}
|
||||||
|
ram_gb: {type: number}
|
||||||
|
os: {type: string, enum: [linux, macos]}
|
||||||
|
lan_ip: {type: string}
|
||||||
|
mesh: {type: object}
|
||||||
|
ssh: {type: object}
|
||||||
|
age_pubkey: {type: string}
|
||||||
|
proxmox-host:
|
||||||
|
parent: machine
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Machine running Proxmox VE.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {pve_version: {type: string}}
|
||||||
|
standalone-server:
|
||||||
|
parent: machine
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Machine outside PVE management (e.g. external VPS).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
hypervisor: {type: string}
|
||||||
|
provider: {type: string}
|
||||||
|
control_level: {type: string, enum: [full, partial, none]}
|
||||||
|
public_ipv4: {type: string}
|
||||||
|
workstation:
|
||||||
|
parent: machine
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Operator machine (may also host services, e.g. mac-mini).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {user: {type: string}}
|
||||||
|
appliance:
|
||||||
|
parent: machine
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Vendor appliance with limited management access.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {vendor: {type: string}, model: {type: string}}
|
||||||
|
vm:
|
||||||
|
parent: compute-entity
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Virtual machine.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pve_id: {type: integer}
|
||||||
|
vcpus: {type: integer}
|
||||||
|
memory_mb: {type: integer}
|
||||||
|
lan_ip: {type: string}
|
||||||
|
public_host: {type: string}
|
||||||
|
role: {type: string}
|
||||||
|
container:
|
||||||
|
parent: compute-entity
|
||||||
|
abstract: true
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
description: OS-level container (LXC or Docker).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {runtime: {type: string}}
|
||||||
|
lxc:
|
||||||
|
parent: container
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Proxmox LXC container.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pve_id: {type: integer}
|
||||||
|
lan_ip: {type: string}
|
||||||
|
public_host: {type: string}
|
||||||
|
public_hosts: {type: array, items: {type: string}}
|
||||||
|
role: {type: string}
|
||||||
|
mesh: {type: object}
|
||||||
|
age_pubkey: {type: string}
|
||||||
|
destroyed: {type: string}
|
||||||
|
reason: {type: string}
|
||||||
|
docker-container:
|
||||||
|
parent: container
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Docker container (the OS models its own stack with these).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {image: {type: string}}
|
||||||
|
hypervisor:
|
||||||
|
parent: entity
|
||||||
|
domain: compute
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Hypervisor software running on a machine (PVE, KVM, OrbStack).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {type: {type: string}, version: {type: string}}
|
||||||
|
|
||||||
|
# ── Infrastructure / network ──
|
||||||
|
network:
|
||||||
|
parent: entity
|
||||||
|
abstract: true
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
description: A network things connect to.
|
||||||
|
lan:
|
||||||
|
parent: network
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Local area network.
|
||||||
|
attributes: {type: object, properties: {subnet: {type: string}}}
|
||||||
|
mesh:
|
||||||
|
parent: network
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Overlay mesh network (NetBird, Tailscale).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
provider: {type: string}
|
||||||
|
subnet: {type: string}
|
||||||
|
domain: {type: string}
|
||||||
|
vlan:
|
||||||
|
parent: network
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Tagged VLAN.
|
||||||
|
attributes: {type: object, properties: {tag: {type: integer}}}
|
||||||
|
network-interface:
|
||||||
|
parent: entity
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
description: Optional per-interface refinement (mac, ip). The seed uses
|
||||||
|
coarse connects-via edges; interfaces can be backfilled later.
|
||||||
|
attributes: {type: object, properties: {mac: {type: string}, ip: {type: string}}}
|
||||||
|
dns-zone:
|
||||||
|
parent: entity
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: DNS zone (e.g. split-horizon hubris.network).
|
||||||
|
attributes: {type: object, properties: {zone: {type: string}, authority: {type: string}}}
|
||||||
|
dns-record:
|
||||||
|
parent: entity
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
description: Individual DNS record.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {name: {type: string}, record_type: {type: string}, value: {type: string}}
|
||||||
|
ingress-route:
|
||||||
|
parent: entity
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Public hostname → upstream mapping (Caddy).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pattern: {type: string}
|
||||||
|
upstream: {type: string}
|
||||||
|
forward_auth: {type: boolean}
|
||||||
|
certificate:
|
||||||
|
parent: entity
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
description: TLS certificate.
|
||||||
|
attributes: {type: object, properties: {issuer: {type: string}, expires: {type: string}}}
|
||||||
|
firewall-rule:
|
||||||
|
parent: entity
|
||||||
|
domain: network
|
||||||
|
layer: infrastructure
|
||||||
|
description: Firewall / port-forward rule.
|
||||||
|
|
||||||
|
# ── Infrastructure / storage ──
|
||||||
|
storage-pool:
|
||||||
|
parent: entity
|
||||||
|
domain: storage
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Storage pool (LVM, ZFS, NFS).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type: {type: string}
|
||||||
|
capacity_gb: {type: number}
|
||||||
|
volume:
|
||||||
|
parent: entity
|
||||||
|
domain: storage
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Named volume / dataset within a pool. Mount details live as
|
||||||
|
attributes on `mounts` edges.
|
||||||
|
attributes: {type: object, properties: {size_gb: {type: number}, path: {type: string}}}
|
||||||
|
backup-target:
|
||||||
|
parent: entity
|
||||||
|
domain: storage
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Where backups land (Proton Drive, PBS).
|
||||||
|
attributes: {type: object, properties: {provider: {type: string}, encrypted: {type: boolean}}}
|
||||||
|
dataset:
|
||||||
|
parent: entity
|
||||||
|
domain: storage
|
||||||
|
layer: infrastructure
|
||||||
|
description: Logical data collection worth tracking independently of its
|
||||||
|
volume (e.g. paperless documents).
|
||||||
|
|
||||||
|
# ── Infrastructure / software ──
|
||||||
|
service:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: A running service with consumers.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
url: {type: string}
|
||||||
|
port: {type: integer}
|
||||||
|
health: {type: string}
|
||||||
|
endpoint: {type: string}
|
||||||
|
systemd_unit: {type: string}
|
||||||
|
doc_page: {type: string}
|
||||||
|
risk_notes: {type: string}
|
||||||
|
note: {type: string}
|
||||||
|
application:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
description: Deployed application/package a service runs.
|
||||||
|
attributes: {type: object, properties: {version: {type: string}}}
|
||||||
|
config-repo:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
description: Git repo holding tracked configuration.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {url: {type: string}, branch: {type: string}}
|
||||||
|
deploy-pipeline:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
description: Automated deploy path (webhook → script).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {trigger: {type: string}, target_path: {type: string}}
|
||||||
|
package-set:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
description: Managed package baseline for a host class.
|
||||||
|
cluster:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Proxmox cluster.
|
||||||
|
attributes: {type: object, properties: {quorum: {type: string}}}
|
||||||
|
compose-stack:
|
||||||
|
parent: entity
|
||||||
|
domain: software
|
||||||
|
layer: infrastructure
|
||||||
|
lifecycle: infrastructure
|
||||||
|
description: Docker Compose stack (the Oikos OS itself is one).
|
||||||
|
attributes: {type: object, properties: {path: {type: string}}}
|
||||||
|
|
||||||
|
# ── Infrastructure / external ──
|
||||||
|
domain-registration:
|
||||||
|
parent: entity
|
||||||
|
domain: external
|
||||||
|
layer: infrastructure
|
||||||
|
description: Registered public domain.
|
||||||
|
attributes: {type: object, properties: {registrar: {type: string}, expires: {type: string}}}
|
||||||
|
cloud-service:
|
||||||
|
parent: entity
|
||||||
|
domain: external
|
||||||
|
layer: infrastructure
|
||||||
|
description: External SaaS/cloud dependency.
|
||||||
|
isp-link:
|
||||||
|
parent: entity
|
||||||
|
domain: external
|
||||||
|
layer: infrastructure
|
||||||
|
description: Internet uplink.
|
||||||
|
vendor-dependency:
|
||||||
|
parent: entity
|
||||||
|
domain: external
|
||||||
|
layer: infrastructure
|
||||||
|
description: Vendor the lab depends on (registrar, IONOS, Proton).
|
||||||
|
|
||||||
|
# ── Governance / identity ──
|
||||||
|
person:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
description: Human actor (operator).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {matrix_id: {type: string}, oidc_sub: {type: string}, email: {type: string}}
|
||||||
|
agent:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
lifecycle: infrastructure # agents are deployed/retired like infrastructure
|
||||||
|
description: Software agent actor (Hermes, the Oikos control loop).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
provider: {type: string}
|
||||||
|
model: {type: string}
|
||||||
|
gateway_port: {type: integer}
|
||||||
|
identity-provider:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
description: OIDC / forward-auth provider (Authentik).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
issuer: {type: string}
|
||||||
|
client_id: {type: string}
|
||||||
|
auth_mode: {type: string, enum: [oidc, forward-auth, both]}
|
||||||
|
account:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
description: An account a person/agent holds on a service.
|
||||||
|
secret:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
description: Managed secret (Infisical path).
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {path: {type: string}, rotation_days: {type: integer}}
|
||||||
|
key:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
description: Cryptographic key (SSH, age).
|
||||||
|
access-grant:
|
||||||
|
parent: entity
|
||||||
|
domain: identity
|
||||||
|
layer: governance
|
||||||
|
description: Grant of access to a secret/scope.
|
||||||
|
attributes: {type: object, properties: {scope: {type: string}, expires: {type: string}}}
|
||||||
|
|
||||||
|
# ── Cognition ──
|
||||||
|
check:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
description: Probe definition (checks-as-data, R3-7). Typed row in check_defs.
|
||||||
|
signal:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
lifecycle: signal
|
||||||
|
description: Something needing attention. Typed row in signals.
|
||||||
|
classification:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
description: A classifier decision with full reasoning. Typed row in classifications.
|
||||||
|
execution:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
lifecycle: execution
|
||||||
|
description: An action the OS performed. Typed row in executions.
|
||||||
|
feedback:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
description: What was learned from an execution. Typed row in feedback.
|
||||||
|
pattern:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
lifecycle: pattern
|
||||||
|
description: Generalized rule extracted from feedback. Typed row in patterns.
|
||||||
|
skill:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
lifecycle: skill
|
||||||
|
description: Codified, versioned procedure. Typed rows in skills.
|
||||||
|
approval:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
lifecycle: approval
|
||||||
|
description: Operator approval request/decision. Typed row in approvals.
|
||||||
|
document:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
description: Knowledge document ingested from docs/.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {title: {type: string}, source_path: {type: string}, content_hash: {type: string}}
|
||||||
|
runbook:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
description: Step-by-step procedure for an entity/action.
|
||||||
|
attributes:
|
||||||
|
type: object
|
||||||
|
properties: {risk_class: {type: string}, source_path: {type: string}}
|
||||||
|
investigation:
|
||||||
|
parent: entity
|
||||||
|
domain: cognition
|
||||||
|
layer: cognition
|
||||||
|
description: Recorded investigation/postmortem.
|
||||||
|
|
||||||
|
# ─── Relationship types ────────────────────────────────────────────────
|
||||||
|
# cardinality is source→target: e.g. `hosts` one-to-many = one machine
|
||||||
|
# hosts many compute entities; each hosted entity has one hosting machine.
|
||||||
|
|
||||||
|
relationship_types:
|
||||||
|
|
||||||
|
# Infrastructure topology
|
||||||
|
hosts:
|
||||||
|
inverse: runs-on
|
||||||
|
source: machine
|
||||||
|
target: compute-entity
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Machine hosts a VM/container (hubris hosts lxc:apps).
|
||||||
|
runs-hypervisor:
|
||||||
|
inverse: hypervisor-on
|
||||||
|
source: machine
|
||||||
|
target: hypervisor
|
||||||
|
cardinality: one-to-one
|
||||||
|
description: Machine runs hypervisor software.
|
||||||
|
member-of:
|
||||||
|
inverse: has-member
|
||||||
|
source: proxmox-host
|
||||||
|
target: cluster
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: PVE host belongs to a cluster.
|
||||||
|
part-of:
|
||||||
|
inverse: comprises
|
||||||
|
source: docker-container
|
||||||
|
target: compose-stack
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Docker container belongs to a compose stack.
|
||||||
|
provides:
|
||||||
|
inverse: provided-by
|
||||||
|
source: compute-entity
|
||||||
|
target: service
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Compute entity provides a service (lxc:gitea provides service:gitea).
|
||||||
|
runs:
|
||||||
|
inverse: run-by
|
||||||
|
source: service
|
||||||
|
target: application
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Service runs an application.
|
||||||
|
configured-by:
|
||||||
|
inverse: configures
|
||||||
|
source: entity
|
||||||
|
target: config-repo
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Entity's config is tracked in a repo (mutations = commit+push).
|
||||||
|
deploys-to:
|
||||||
|
inverse: deployed-by
|
||||||
|
source: deploy-pipeline
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Pipeline deploys to a service/host.
|
||||||
|
routes-to:
|
||||||
|
inverse: routed-via
|
||||||
|
source: ingress-route
|
||||||
|
target: service
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Public hostname routes to a service.
|
||||||
|
secured-by:
|
||||||
|
inverse: secures
|
||||||
|
source: ingress-route
|
||||||
|
target: identity-provider
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Route gated by forward-auth.
|
||||||
|
uses-certificate:
|
||||||
|
inverse: certifies
|
||||||
|
source: ingress-route
|
||||||
|
target: certificate
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Route served with this certificate.
|
||||||
|
authenticates-via:
|
||||||
|
inverse: authenticates-service
|
||||||
|
source: service
|
||||||
|
target: identity-provider
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Service uses native OIDC (jellyfin authenticates-via authentik).
|
||||||
|
in-zone:
|
||||||
|
inverse: contains-record
|
||||||
|
source: dns-record
|
||||||
|
target: dns-zone
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Record belongs to a zone.
|
||||||
|
resolves-to:
|
||||||
|
inverse: resolved-from
|
||||||
|
source: dns-record
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Record points at an ingress route or host.
|
||||||
|
depends-on:
|
||||||
|
inverse: dependency-of
|
||||||
|
source: service
|
||||||
|
target: service
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Runtime dependency (blast-radius edge).
|
||||||
|
connects-via:
|
||||||
|
inverse: connects
|
||||||
|
source: compute-entity
|
||||||
|
target: network
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Coarse network membership (host on LAN / mesh).
|
||||||
|
has-interface:
|
||||||
|
inverse: interface-of
|
||||||
|
source: compute-entity
|
||||||
|
target: network-interface
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Optional per-interface refinement.
|
||||||
|
interface-on:
|
||||||
|
inverse: has-endpoint
|
||||||
|
source: network-interface
|
||||||
|
target: network
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Interface attaches to a network.
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
mounts:
|
||||||
|
inverse: mounted-by
|
||||||
|
source: compute-entity
|
||||||
|
target: volume
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Compute entity mounts a volume. Edge attributes carry
|
||||||
|
mount_point and options.
|
||||||
|
stores-on:
|
||||||
|
inverse: stores-for
|
||||||
|
source: compute-entity
|
||||||
|
target: storage-pool
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Rootfs/data lives on a pool.
|
||||||
|
contains:
|
||||||
|
inverse: contained-in
|
||||||
|
source: storage-pool
|
||||||
|
target: volume
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Pool contains a volume.
|
||||||
|
holds-dataset:
|
||||||
|
inverse: dataset-on
|
||||||
|
source: volume
|
||||||
|
target: dataset
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Volume holds a tracked dataset.
|
||||||
|
backs-up-to:
|
||||||
|
inverse: backup-of
|
||||||
|
source: entity
|
||||||
|
target: backup-target
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Entity's data is backed up to a target.
|
||||||
|
|
||||||
|
# Physical / external
|
||||||
|
powered-by:
|
||||||
|
inverse: powers
|
||||||
|
source: machine
|
||||||
|
target: ups
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Machine on UPS power.
|
||||||
|
located-at:
|
||||||
|
inverse: location-of
|
||||||
|
source: machine
|
||||||
|
target: site
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Machine's physical site.
|
||||||
|
registered-with:
|
||||||
|
inverse: registrar-of
|
||||||
|
source: domain-registration
|
||||||
|
target: vendor-dependency
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Domain registered with a registrar.
|
||||||
|
|
||||||
|
# Governance
|
||||||
|
owns:
|
||||||
|
inverse: owned-by
|
||||||
|
source: person
|
||||||
|
target: agent
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Person owns/controls an agent.
|
||||||
|
authenticates:
|
||||||
|
inverse: authenticated-by
|
||||||
|
source: identity-provider
|
||||||
|
target: person
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: IdP authenticates a person.
|
||||||
|
holds-grant:
|
||||||
|
inverse: granted-to
|
||||||
|
source: agent
|
||||||
|
target: access-grant
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Agent holds an access grant.
|
||||||
|
grants:
|
||||||
|
inverse: granted-by
|
||||||
|
source: access-grant
|
||||||
|
target: secret
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Grant covers a secret.
|
||||||
|
can-decrypt:
|
||||||
|
inverse: readable-by
|
||||||
|
source: compute-entity
|
||||||
|
target: secret
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Host can decrypt a secret (legacy SOPS; Infisical grants later).
|
||||||
|
|
||||||
|
# Cognition
|
||||||
|
checks:
|
||||||
|
inverse: checked-by
|
||||||
|
source: check
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Check probes an entity.
|
||||||
|
raises:
|
||||||
|
inverse: raised-by
|
||||||
|
source: check
|
||||||
|
target: signal
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Check raised a signal.
|
||||||
|
about:
|
||||||
|
inverse: subject-of
|
||||||
|
source: signal
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Signal concerns an entity.
|
||||||
|
classifies:
|
||||||
|
inverse: classified-as
|
||||||
|
source: classification
|
||||||
|
target: signal
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Classification of a signal.
|
||||||
|
precedes:
|
||||||
|
inverse: follows
|
||||||
|
source: classification
|
||||||
|
target: execution
|
||||||
|
cardinality: one-to-one
|
||||||
|
description: Classification that led to an execution.
|
||||||
|
targets:
|
||||||
|
inverse: targeted-by
|
||||||
|
source: execution
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Execution acts on an entity.
|
||||||
|
requires-approval:
|
||||||
|
inverse: approves
|
||||||
|
source: execution
|
||||||
|
target: approval
|
||||||
|
cardinality: one-to-one
|
||||||
|
description: Execution gated by an approval.
|
||||||
|
performs:
|
||||||
|
inverse: performed-by
|
||||||
|
source: agent
|
||||||
|
target: execution
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Agent performed an execution.
|
||||||
|
decides:
|
||||||
|
inverse: decided-by
|
||||||
|
source: person
|
||||||
|
target: approval
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Person decided an approval.
|
||||||
|
produces:
|
||||||
|
inverse: produced-by
|
||||||
|
source: execution
|
||||||
|
target: feedback
|
||||||
|
cardinality: one-to-one
|
||||||
|
description: Execution produced feedback.
|
||||||
|
contributes-to:
|
||||||
|
inverse: built-from
|
||||||
|
source: feedback
|
||||||
|
target: pattern
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Feedback supports a pattern.
|
||||||
|
informs:
|
||||||
|
inverse: informed-by
|
||||||
|
source: pattern
|
||||||
|
target: skill
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Pattern informs a skill.
|
||||||
|
guides:
|
||||||
|
inverse: guided-by
|
||||||
|
source: skill
|
||||||
|
target: classification
|
||||||
|
cardinality: one-to-many
|
||||||
|
description: Skill guided a classification.
|
||||||
|
documents:
|
||||||
|
inverse: documented-by
|
||||||
|
source: document
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-one
|
||||||
|
description: Document describes an entity.
|
||||||
|
procedure-for:
|
||||||
|
inverse: has-procedure
|
||||||
|
source: runbook
|
||||||
|
target: entity
|
||||||
|
cardinality: many-to-many
|
||||||
|
description: Runbook applies to an entity.
|
||||||
103
seeds/policy.yaml
Normal file
103
seeds/policy.yaml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Oikos policy seed — risk classes, approval rules, autonomy settings.
|
||||||
|
#
|
||||||
|
# Bootstraps risk_classes / approval_rules / autonomy_settings (migration
|
||||||
|
# 005). After ingest the DB is authoritative; runtime policy changes go
|
||||||
|
# through the dual-control meta-approval flow (plan S3) and are exported
|
||||||
|
# back here via `GET /api/v1/export`.
|
||||||
|
#
|
||||||
|
# Adapted from legacy oikos/policy.yaml (2026-07-07):
|
||||||
|
# - `commands:`/`mcp_tools:` maps are gone — read endpoints and MCP read
|
||||||
|
# tools are read_only by construction and never classified; mutating
|
||||||
|
# API calls classify via approval_rules below.
|
||||||
|
# - `actions:` map became approval_rules keyed on (entity_type, action);
|
||||||
|
# entity types may be abstract (rule inherits down the hierarchy,
|
||||||
|
# most-specific match wins: scope_entity > concrete type > ancestor).
|
||||||
|
# - `service_overrides:` became scope_entity rules.
|
||||||
|
# - `lifecycle_overrides:` became autonomy_settings keys read by the
|
||||||
|
# classifier.
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
risk_classes:
|
||||||
|
read_only:
|
||||||
|
description: Observes state; cannot change anything.
|
||||||
|
approval_required: none
|
||||||
|
autonomy_allowed: true
|
||||||
|
reversible_low:
|
||||||
|
description: >-
|
||||||
|
Changes runtime state in a way a single follow-up command undoes
|
||||||
|
(restart, cache clear, sync pull). No config or data changes.
|
||||||
|
approval_required: none # still subject to global.auto_act + rules below
|
||||||
|
autonomy_allowed: true
|
||||||
|
config_mutation:
|
||||||
|
description: >-
|
||||||
|
Changes tracked configuration or deployed software: repo edit + push,
|
||||||
|
deploy trigger, Caddy/Gitea/app config, package upgrades. Reversible
|
||||||
|
via git, but affects other consumers.
|
||||||
|
approval_required: operator # Matrix ✅/❌ (single-use HMAC token)
|
||||||
|
autonomy_allowed: false
|
||||||
|
destructive:
|
||||||
|
description: >-
|
||||||
|
Destroys or irreversibly alters data/entities: container destroy,
|
||||||
|
disk format, DB wipe, secret rotation, client revocation.
|
||||||
|
approval_required: operator_confirmed
|
||||||
|
autonomy_allowed: false
|
||||||
|
|
||||||
|
approval_rules:
|
||||||
|
# ── Generic rules on (possibly abstract) types ──
|
||||||
|
- {entity_type: service, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: service, action: cache-clear, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: service, action: sync-pull, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: service, action: db-wipe, risk_class: destructive, autonomy_level: never}
|
||||||
|
- {entity_type: compose-stack, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: docker-container, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: machine, action: apt-upgrade, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: machine, action: reboot, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: machine, action: format-disk, risk_class: destructive, autonomy_level: never}
|
||||||
|
- {entity_type: config-repo, action: edit, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: deploy-pipeline, action: trigger, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: lxc, action: create, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: lxc, action: migrate, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: lxc, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: lxc, action: destroy, risk_class: destructive, autonomy_level: never}
|
||||||
|
- {entity_type: vm, action: restart, risk_class: reversible_low, autonomy_level: auto}
|
||||||
|
- {entity_type: vm, action: destroy, risk_class: destructive, autonomy_level: never}
|
||||||
|
- {entity_type: dns-record, action: change, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: ingress-route, action: change, risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
- {entity_type: storage-pool, action: change, risk_class: destructive, autonomy_level: never}
|
||||||
|
- {entity_type: secret, action: rotate, risk_class: destructive, autonomy_level: never}
|
||||||
|
- {entity_type: key, action: revoke, risk_class: destructive, autonomy_level: never}
|
||||||
|
|
||||||
|
# ── Governance objects (the OS's own levers — always operator-gated) ──
|
||||||
|
- {entity_type: pattern, action: activate, risk_class: config_mutation, autonomy_level: never}
|
||||||
|
- {entity_type: skill, action: activate, risk_class: config_mutation, autonomy_level: never}
|
||||||
|
|
||||||
|
# ── Per-entity overrides (wide blast radius) ──
|
||||||
|
- {entity_type: service, action: restart, scope_entity: "service:caddy",
|
||||||
|
risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
# everything *.hubris.network rides on caddy
|
||||||
|
- {entity_type: service, action: restart, scope_entity: "service:dns",
|
||||||
|
risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
# LAN-wide resolver
|
||||||
|
- {entity_type: service, action: restart, scope_entity: "service:authentik",
|
||||||
|
risk_class: config_mutation, autonomy_level: escalate}
|
||||||
|
# SSO provider — restart locks logins fleet-wide
|
||||||
|
|
||||||
|
autonomy_settings:
|
||||||
|
# Global kill-switch. Cold start = off: the agent escalates everything
|
||||||
|
# until patterns validate and the operator raises this (plan: trust is earned).
|
||||||
|
global.auto_act: "off" # off | reversible_low
|
||||||
|
|
||||||
|
# Per-entity hard blocks (checked even when global.auto_act is on)
|
||||||
|
never_auto_act.service:caddy: "true"
|
||||||
|
never_auto_act.service:dns: "true"
|
||||||
|
never_auto_act.service:authentik: "true"
|
||||||
|
never_auto_act.host:hubris: "true"
|
||||||
|
never_auto_act.host:strong: "true"
|
||||||
|
|
||||||
|
# Lifecycle-state classifier overrides (from legacy lifecycle_overrides)
|
||||||
|
lifecycle_override.provisioning.config_mutation: reversible_low
|
||||||
|
# no dependents yet — config changes are cheap
|
||||||
|
lifecycle_override.deprecated.refuse: new-inbound-edges
|
||||||
|
lifecycle_override.destroyed.refuse: all
|
||||||
|
# any action targeting a destroyed entity raises a drift signal instead
|
||||||
Reference in New Issue
Block a user