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:
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 |
|
||||
Reference in New Issue
Block a user