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:
2026-07-07 00:17:15 +02:00
parent ea3b2c3662
commit 18cb79caf9
18 changed files with 3741 additions and 3 deletions

View 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.

View 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.

View 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`).

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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
View 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 |