Files
oikos/plans/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md
dtoro fe54af30f6 plans: replace ASCII architecture diagrams with Mermaid
4 diagrams: container stack, OODA loop, knowledge graph, deploy flow
2026-07-06 22:34:53 +02:00

719 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan: Oikos — Docker-based agentic homelab OS on mac-mini
**Status:** Planned (2026-07-06, revised) — supersedes the launchd-based consolidation plan.
**Predecessor:** The original plan re-platformed Oikos services from LXC 105 (apps) to
mac-mini via launchd plists. This revision pivots to a **Docker-based** architecture:
the repo becomes a containerized homelab OS, deployed via `docker compose` on mac-mini.
## Vision
Convert this repo into a **Docker-based agentic homelab OS**. The OS is a set of
containerized services that manage the homelab autonomously, with the operator in
control. Two actors:
- **Operator** (dtoro) — owns the homelab, expresses intent ("install X", "restart
Y"), approves destructive actions. Connects from any workstation via remote Hermes
or Matrix.
- **Agent** — Hermes core + custom homelab skills, running in Docker. Executes orders,
monitors the lab, escalates when unsure.
All OS services run in Docker containers on mac-mini. The OS is deployed by a git
push (Gitea webhook → Docker rebuild). It's designed for mac-mini now, with a path
to multi-node later.
## Decisions (from operator Q&A, 2026-07-06)
| Question | Decision |
|---|---|
| Repo structure | One repo, reorganize internally. Wiki docs get restructured into the knowledge graph. |
| Hermes runtime | Runs inside Docker as part of the OS stack (gateway mode — connect from any workstation). |
| Agent → homelab access | Hybrid — mounted SSH keys for now, build actuator gateway incrementally. |
| Data storage | Migrate to PostgreSQL. `inventory.yaml` stays as declarative config; signals/ledger/state/knowledge go into DB. |
| Knowledge/context | Structured knowledge graph in Postgres — docs become typed entities linked to inventory entities. |
| Operator interface | Primary = remote Hermes from any workstation + Matrix. Console/UIs built later for specific tasks. |
| Host | mac-mini for now, designed to scale later. |
| Deploy | Git push → Gitea webhook → Docker rebuild + restart. |
| `bin/homelab` CLI | Replaced by an API. CLI becomes a thin client that calls the OS API. |
| Secrets | Migrate from SOPS+age to Infisical (open-source, API-first, Docker-native). |
| Matrix | Keep for now, abstract the notification layer for future channels. |
| MCP server | Merged into the unified API — one service, REST + MCP interfaces. |
| Agent type | Hermes core + custom homelab skills (standard Hermes, purpose-loaded). |
| apps/105 | Keep running as fallback until Docker OS is proven. |
| mac-mini cleanup | Start fresh in a new directory, clean up old artifacts later. |
## Target architecture
### Container stack on mac-mini
```mermaid
graph TB
subgraph mac-mini["mac-mini — Docker host, always-on"]
subgraph services["Docker Compose"]
PG["PostgreSQL\n• inventory (synced from YAML)\n• signals • ledger\n• knowledge graph\n• state snapshots\n• approvals"]
INF["Infisical\n(secrets manager)"]
HERMES["Hermes Agent — gateway mode\n+ homelab skills\n+ MCP client → API\n+ SSH keys (mounted)"]
API["Oikos API — REST + MCP\n\nMCP: get_host, list_services,\nsearch_knowledge, get_relations\nREST: /hosts, /services, /signals,\n/exec, /approve, /deploy, /events\n\nPolicy enforcement + risk\nclassification + ledger"]
SCHED["Scheduler (Observe)\n+ Actuator (Act)\n• 10-min probes → DB\n• Classify → auto-act or escalate\n• SSH to hubris/strong"]
NOTIFIER["Notifier — abstracted\n• Matrix (current): alerts,\napproval requests\n• Future: webhook, email, etc."]
end
DEPLOY["Gitea webhook →\ndocker compose build + up -d"]
VOLS["Volumes: pg-data, infisical-data,\nssh-keys (ro), hermes-config"]
end
HERMES -- MCP --> API
API --> PG
SCHED --> PG
SCHED -- SSH --> HUBRIS
SCHED -- SSH --> STRONG
API -- "escalate" --> NOTIFIER
NOTIFIER -- "alerts + approvals" --> MATRIX
DEPLOY -- "rebuild" --> services
subgraph external["External"]
CADDY["Caddy (LXC 121)\n→ mac-mini mesh :8090"]
APPS["apps/105 (fallback)\nkept running until proven"]
HUBRIS["hubris (PVE)"]
STRONG["strong (PVE)"]
GITEA["Gitea (LXC 104)\nwebhook trigger"]
MATRIX["Matrix (LXC 118)"]
WS["Any workstation\nHermes remote → gateway"]
end
CADDY -- reverse_proxy --> API
GITEA -- webhook --> DEPLOY
WS -- "Hermes gateway" --> HERMES
APPS -. "fallback" .- API
```
### OODA loop — the control flow
```mermaid
flowchart LR
OBSERVE["Observe\nScheduler probes:\n• HTTP health\n• disk usage\n• drift detection"] --> ORIENT["Orient\nRelations graph walk:\n• blast radius\n• lifecycle state\n• runbook match"]
ORIENT --> DECIDE["Decide\nRisk classifier:\nrisk × blast × confidence"]
DECIDE -- "auto-act\n(reversible_low,\ncontained, confident)" --> ACT["Act\nExecute via SSH/API\n→ verify → resolve\n→ ledger entry"]
DECIDE -- "escalate\n(approval needed)" --> APPROVE["Approval request\n→ Matrix ✅/❌\n→ grant or deny"]
APPROVE -- "approved" --> ACT
APPROVE -- "denied" --> RESOLVE["Resolve signal\nnote: denied"]
ACT --> OBSERVE
```
### Knowledge graph — docs to DB
```mermaid
flowchart TB
subgraph repo["Repo (git-tracked)"]
INV["inventory.yaml\ndeclarative topology"]
DOCS["docs/\n• containers/105-apps.md\n• hosts/hubris.md\n• infrastructure/dns.md\n• investigations/..."]
end
subgraph db["PostgreSQL"]
ENT["entities table\n(host:hubris, service:caddy, ...)"]
REL["relations table\n(hosts, provides, depends-on, ...)"]
KNOW["knowledge_entities table\n(doc:105-apps, doc:hubris, ...)"]
KNOWREL["knowledge_relations table\n(describes, relates-to, runbook-for)"]
end
INGEST["Ingest pipeline\n(runs on deploy)"]
INV --> INGEST --> ENT
INV --> INGEST --> REL
DOCS --> INGEST --> KNOW
DOCS --> INGEST --> KNOWREL
KNOW -. "linked by entity_id" .- ENT
```
### Deploy flow
```mermaid
flowchart LR
DEV["Operator\nedits repo"] --> PUSH["git push"] --> GITEA["Gitea\n(LXC 104)"]
GITEA -- "webhook" --> MACMINI["mac-mini\ndeploy script"]
MACMINI -- "git pull" --> REPO["repo clone"]
MACMINI -- "docker compose\nbuild + up -d" --> STACK["OS containers\nrebuilt + restarted"]
STACK -- "ingest" --> DB["PostgreSQL\nupdated"]
```
## Repo reorganization
The repo shifts from "wiki + code mixed" to "OS code + declarative config + docs as
ingest source." Proposed layout:
```
/ # repo root
├── docker-compose.yml # the OS stack definition
├── compose/ # per-service Dockerfiles + build context
│ ├── api/ # Oikos API (REST + MCP) — merges mcp/ + bin/homelab
│ │ └── Dockerfile
│ ├── scheduler/ # Observe + Act loop
│ │ └── Dockerfile
│ ├── hermes/ # Hermes agent runtime
│ │ └── Dockerfile
│ ├── postgres/ # init scripts, migrations
│ │ └── init.sql
│ └── infisical/ # Infisical config
│ └── config.yaml
├── oikos/ # Core OS Python package (reused, adapted)
│ ├── api/ # NEW — unified API server
│ │ ├── server.py # FastAPI app, REST routes + MCP protocol adapter
│ │ ├── routes/ # REST route modules
│ │ └── mcp.py # MCP protocol adapter (exposes same tools via MCP)
│ ├── db/ # NEW — database layer
│ │ ├── models.py # SQLAlchemy models: Inventory, Signal, Ledger, Knowledge, Approval
│ │ ├── migrations/ # Alembic migrations
│ │ └── ingest.py # Ingest inventory.yaml + docs into DB on deploy
│ ├── scheduler/ # Adapted from scheduler.py — reads/writes DB
│ ├── actuator/ # NEW — Act stage: reads signals, classifies, executes
│ │ └── act.py
│ ├── policy/ # Adapted — policy.yaml loader + classifier
│ ├── knowledge/ # NEW — knowledge graph ingestion from docs
│ │ └── ingest_docs.py
│ ├── notifier/ # NEW — abstracted notification (Matrix impl)
│ │ ├── base.py # Notifier interface
│ │ └── matrix.py # Matrix implementation
│ ├── decide.py # Reused — risk classifier (reads from DB)
│ ├── signal.py # Adapted — signals in DB instead of JSONL files
│ ├── approve.py # Adapted — approvals in DB
│ ├── ledger.py # Adapted — ledger in DB
│ ├── drift.py # Adapted — drift detection, writes signals to DB
│ ├── relations.py # Reused — ontology graph walk (reads from DB)
│ └── report.py # Adapted — generates reports from DB
├── hermes/ # Hermes agent config + skills (mounted into container)
│ ├── config.yaml # Hermes config (providers, models, gateway)
│ ├── SOUL.md # Agent persona (homelab-specific)
│ └── skills/ # Custom homelab skills
│ └── homelab-ops/
│ └── SKILL.md
├── inventory.yaml # Declarative config — the "kernel data structure" (stays YAML)
├── ontology.yaml # Systems model (stays YAML)
├── policy.yaml # Risk & approval policy (stays YAML)
├── docs/ # Narrative docs — ingested into knowledge graph on deploy
│ ├── containers/ # LXC/VM documentation
│ ├── hosts/ # Host documentation
│ ├── infrastructure/ # Infrastructure docs
│ └── investigations/ # Incident records
├── secrets/ # SOPS-encrypted secrets (migrated to Infisical, then removed)
└── .sops.yaml # SOPS config (deprecated after Infisical migration)
```
**What stays YAML (declarative, human-edited, git-tracked):**
- `inventory.yaml` — topology, the kernel data structure
- `ontology.yaml` — entity types, relationships, lifecycle
- `policy.yaml` — risk classes, approval rules, autonomy settings
- `hermes/config.yaml` — agent configuration
- `hermes/SOUL.md` — agent persona
**What moves to PostgreSQL (mutable, runtime state):**
- `signals/``signals` table
- `ledger/``ledger_entries` table
- `oikos/state.json``state_snapshots` table
- `approvals/``approvals` table
- `knowledge/wiki/` narrative docs → `knowledge_entities` + `knowledge_relations` tables
**What gets removed (no longer needed):**
- `mcp/server.py` — merged into `oikos/api/server.py`
- `bin/homelab` — replaced by the API; thin CLI client can be generated later
- `secrets-issuance/` — replaced by Infisical
- `oikos/systemd/` — replaced by Docker containers
- `oikos/console/` — deferred (built later as a specific UI on top of the API)
- `mcp/deploy/`, `oikos/console/deploy/` — replaced by Docker build
- `hosts/*.yaml` — generated from inventory, now served by the API directly
## Docker Compose stack
```yaml
# docker-compose.yml (simplified — full version in the repo)
services:
postgres:
image: postgres:16-alpine
volumes: [pg-data:/var/lib/postgresql/data, ./compose/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql]
environment:
POSTGRES_DB: oikos
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
secrets: [pg_password]
healthcheck: ...
infisical:
image: infisical/infisical:latest
volumes: [infisical-data:/var/lib/infisical]
environment: ...
# Secrets manager — replaces SOPS+age for runtime secret access
api:
build: ./compose/api
depends_on: [postgres, infisical]
environment:
DATABASE_URL: postgresql://oikos@postgres/oikos
HOMELAB_CONTEXT_DIR: /opt/homelab-context
volumes:
- ./inventory.yaml:/app/inventory.yaml:ro
- ./ontology.yaml:/app/ontology.yaml:ro
- ./policy.yaml:/app/policy.yaml:ro
- ./docs:/app/docs:ro
ports: ["8090:8090"] # REST + MCP
# On startup: ingest inventory.yaml + docs into DB if changed
scheduler:
build: ./compose/scheduler
depends_on: [api, postgres]
environment:
DATABASE_URL: postgresql://oikos@postgres/oikos
SSH_KEY: /run/secrets/ssh_key
volumes:
- ssh-keys:/run/secrets:ro
# Runs observe + act loop on a cron-like schedule (10-min interval)
hermes:
build: ./compose/hermes
depends_on: [api, infisical]
environment:
OIKOS_API_URL: http://api:8090
INFISICAL_TOKEN: ...
volumes:
- ./hermes:/root/.hermes:ro
- ssh-keys:/root/.ssh:ro
- hermes-data:/root/.hermes/data
ports: ["8092:8092"] # Hermes gateway port
# Agent runtime — connects to API via MCP, has SSH for direct host access
notifier:
build: ./compose/scheduler # shares image with scheduler
depends_on: [api, postgres]
# Matrix bot — listens for approval requests, sends alerts
# Abstracted via notifier interface — Matrix impl now, pluggable later
volumes:
pg-data:
infisical-data:
ssh-keys: # populated from host ~/.ssh at deploy time
hermes-data:
```
## Workstreams
### 1. Database schema + models (`oikos/db/`, new)
The foundational layer — everything else reads/writes through this.
**Tables:**
```sql
-- Inventory entities (synced from inventory.yaml on deploy)
CREATE TABLE entities (
id TEXT PRIMARY KEY, -- "host:hubris", "service:caddy"
type TEXT NOT NULL, -- host, service, lxc, vm, etc.
name TEXT NOT NULL,
domain TEXT, -- physical, compute, network, etc.
state TEXT DEFAULT 'active', -- provisioning, active, deprecated, destroyed
data JSONB NOT NULL, -- full entity record from inventory.yaml
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Entity relationships (the ontology graph)
CREATE TABLE relations (
source_id TEXT REFERENCES entities(id),
target_id TEXT REFERENCES entities(id),
type TEXT NOT NULL, -- hosts, provides, mounts, depends-on, etc.
data JSONB,
PRIMARY KEY (source_id, target_id, type)
);
-- Signals (replaces signals/*.jsonl)
CREATE TABLE signals (
id TEXT PRIMARY KEY, -- "sig-2026-07-06-0001"
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
kind TEXT NOT NULL, -- service-down, disk-threshold, drift, etc.
severity TEXT NOT NULL, -- info, warning, critical
entity_id TEXT REFERENCES entities(id),
evidence TEXT,
likely_cause TEXT,
recommended_action JSONB,
verification TEXT,
state TEXT NOT NULL DEFAULT 'raised', -- raised, acknowledged, acting, resolved, muted
mute_until TIMESTAMPTZ,
note TEXT,
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Change ledger (replaces ledger/*.jsonl)
CREATE TABLE ledger_entries (
id SERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
entity_id TEXT REFERENCES entities(id),
action TEXT NOT NULL,
risk_class TEXT NOT NULL,
result TEXT, -- ok, failed, escalated
approval_id TEXT, -- references approvals if gated
agent TEXT, -- agent identity (age pubkey or hostname)
notes TEXT
);
-- Approvals (replaces approvals/*.jsonl)
CREATE TABLE approvals (
id TEXT PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
entity_id TEXT REFERENCES entities(id),
action TEXT NOT NULL,
risk_class TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending, approved, denied, expired
ttl INTERVAL NOT NULL DEFAULT '1 hour',
decided_at TIMESTAMPTZ,
decided_by TEXT,
confirmation_phrase TEXT
);
-- State snapshots (replaces oikos/state.json)
CREATE TABLE state_snapshots (
id SERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
entity_id TEXT REFERENCES entities(id),
health TEXT, -- healthy, degraded, down, unknown
data JSONB -- full probe response
);
-- Knowledge graph (replaces narrative wiki docs)
CREATE TABLE knowledge_entities (
id TEXT PRIMARY KEY, -- "doc:105-apps", "investigation:2026-07-05-disk"
type TEXT NOT NULL, -- container-doc, host-doc, investigation, plan, runbook
entity_id TEXT REFERENCES entities(id), -- link to inventory entity if applicable
title TEXT NOT NULL,
content TEXT NOT NULL, -- markdown body
tags TEXT[],
source_path TEXT, -- original file path for traceability
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Knowledge relationships (cross-references between docs + entities)
CREATE TABLE knowledge_relations (
source_id TEXT REFERENCES knowledge_entities(id),
target_id TEXT, -- can be knowledge_entity or inventory entity
type TEXT NOT NULL, -- describes, relates-to, runbook-for, etc.
PRIMARY KEY (source_id, target_id, type)
);
```
**Ingest pipeline** (`oikos/db/ingest.py`):
- On startup / deploy: parse `inventory.yaml` → upsert into `entities` + `relations`
- Walk `docs/` directory → parse markdown, extract frontmatter → upsert into `knowledge_entities`
- Link knowledge entities to inventory entities by path convention (`docs/containers/105-apps.md``entity:lxc:apps`)
- Idempotent — safe to re-run on every deploy
**Migration from existing data:**
- Write a one-time script to import existing `signals/*.jsonl`, `ledger/*.jsonl`, `approvals/*.jsonl` into the DB
- `oikos/state.json``state_snapshots` table
### 2. Unified API server (`oikos/api/`, new — merges MCP + homelab CLI)
One FastAPI service exposing two interfaces:
**MCP interface** (`oikos/api/mcp.py`):
- Same tool surface as the current `mcp/server.py` (get_host, list_services, search_knowledge, etc.)
- Reads from PostgreSQL instead of filesystem
- `search_docs``search_knowledge` (queries the knowledge graph)
- New tools: `get_entity_relations`, `get_signal_history`, `get_ledger`
- Protocol: FastMCP over streamable HTTP (same as current)
**REST interface** (`oikos/api/routes/`):
- `GET /api/v1/hosts` — list hosts (replaces `homelab list`)
- `GET /api/v1/services` — list services
- `GET /api/v1/hosts/{name}` — host detail (replaces `homelab status`)
- `GET /api/v1/services/{name}/health` — service health (cache-first, `?live=true` for probe)
- `GET /api/v1/signals` — list signals (`?state=raised&severity=warning`)
- `POST /api/v1/signals/{id}/ack` — acknowledge
- `POST /api/v1/signals/{id}/resolve` — resolve
- `GET /api/v1/approvals` — pending approvals
- `POST /api/v1/approvals/{id}/decide` — approve/deny (operator only, Authentik-gated)
- `POST /api/v1/exec` — gated execution (the actuator entrypoint)
- Body: `{entity, action, approval_id?}`
- Classifies via `decide.py`, checks approval if needed, executes, records ledger
- `POST /api/v1/deploy/{service}` — trigger a service deploy
- `WS /api/v1/events` — real-time stream (signals, approvals, ledger entries)
- `GET /api/v1/knowledge/{entity_id}` — knowledge graph query for an entity
- `GET /api/v1/knowledge/search?q=...` — search knowledge graph
**Policy enforcement** is built into the API:
- Every mutating endpoint classifies the action via `oikos/decide.py`
- `config_mutation`/`destructive` actions require a valid approval ID
- All mutations write to the ledger automatically
**Auth:**
- MCP interface: no auth (internal, container-to-container)
- REST interface: Authentik OIDC forward-auth (via Caddy) for operator endpoints
- Internal service-to-service: shared secret / mTLS (Docker network)
### 3. Scheduler + Actuator (`oikos/scheduler/`, `oikos/actuator/`)
**Scheduler (Observe)** — adapted from `oikos/scheduler.py`:
- Runs as a Docker service on a timer (10-min interval via a loop or cron sidecar)
- Probes service health (HTTP), disk usage (SSH to hubris/strong), drift detection
- Writes signals to PostgreSQL (not JSONL files)
- Writes state snapshots to DB
- No more git commit/push of signals — they're in the DB
**Actuator (Act)** — new, `oikos/actuator/act.py`:
- Reads open signals from DB where `recommended_action` is set
- For each: classify via `decide.py`
- `auto-act` (reversible_low, contained, confident) → execute via SSH/API, verify, resolve signal, write ledger
- `escalate` → create approval request, notify via Matrix, acknowledge signal
- Loop-guard: check ledger history per (entity, action) to cap auto-retries
- Autonomy kill-switch: `policy.yaml``autonomy.auto_act: off|reversible_low`, `never_auto_act: [entity list]`
- **Phase 1**: direct SSH execution (keys mounted)
- **Phase 2**: execution through the API's `/exec` endpoint (gateway pattern)
### 4. Hermes agent container (`compose/hermes/`)
**What it is:**
- Hermes Agent runtime in a Docker container
- Runs in gateway mode — workstations connect to it remotely
- Config: `hermes/config.yaml` (providers, models, gateway port)
- Persona: `hermes/SOUL.md` (homelab-specific)
- Skills: `hermes/skills/homelab-ops/SKILL.md` (how to interact with the homelab)
**What it has access to:**
- **Oikos API** via MCP (container network, no auth needed)
- **Infisical** for secret retrieval (API token)
- **SSH keys** mounted from host (`~/.ssh``/root/.ssh:ro`) — for direct host access (hybrid approach)
- **Hermes data volume** — persistent state, session history
**How workstations connect:**
- Hermes gateway listens on port 8092
- Any workstation with Hermes CLI can connect: `hermes gateway connect mac-mini:8092`
- Or via Caddy ingress: `hermes.hubris.network` → mac-mini:8092 (mesh)
**Homelab skills** (loaded into Hermes):
- `homelab-ops` — how to use the Oikos API, classify actions, request approvals
- `homelab-deploy` — how to deploy/manage services
- `homelab-troubleshoot` — diagnostic procedures
- (Future skills added as needed)
### 5. Infisical secrets migration
**Phase 1 — Stand up Infisical:**
- Add Infisical to the Docker stack
- Configure with PostgreSQL backend (shared with Oikos DB or separate DB)
- Bootstrap admin credentials, create `hubris` project + environments (prod, staging)
**Phase 2 — Migrate secrets:**
- Write a migration script: read all `secrets/*.yaml` (SOPS-encrypted), decrypt with age key, import into Infisical
- Map each secret to Infisical's key-value structure
- Verify all secrets are imported
**Phase 3 — Wire services:**
- All OS services authenticate to Infisical via machine identity (service token)
- Services fetch secrets at startup or on-demand
- Remove `secrets/` directory and `.sops.yaml` once verified
- The age key on mac-mini stays for one-time decryption during migration, then is retired
**Phase 4 — Retire SOPS:**
- Remove SOPS dependencies from the repo
- Update inventory to reflect Infisical as the secret store
- Document the new secret access pattern
### 6. Knowledge graph ingestion (`oikos/knowledge/`)
**What it does:**
- On deploy, walks `docs/` directory
- Parses each markdown file:
- Extracts frontmatter (if any) for metadata
- Infers entity relationships from path conventions:
- `docs/containers/105-apps.md` → links to `entity:lxc:apps`
- `docs/hosts/hubris.md` → links to `entity:host:hubris`
- `docs/infrastructure/dns.md` → links to `entity:service:dns`
- Extracts cross-references (links to other docs) → `knowledge_relations`
- Tags by directory: container-doc, host-doc, infrastructure, investigation, plan
- Upserts into `knowledge_entities` + `knowledge_relations` tables
**Agent access:**
- MCP tool `search_knowledge(query)` — full-text search on knowledge graph
- MCP tool `get_entity_knowledge(entity_id)` — all docs related to an entity
- MCP tool `get_relations(entity_id)` — ontology blast-radius query (existing, adapted to DB)
**Docs as source, DB as query layer:**
- Humans still write markdown docs in `docs/`
- The DB is the query/index layer — agents never grep files
- On deploy, docs are re-ingested (idempotent)
- Future: wiki-style editing UI that writes back to `docs/` via git
### 7. Notifier abstraction (`oikos/notifier/`)
**Interface:**
```python
class Notifier(Protocol):
def send_alert(self, signal: Signal) -> None: ...
def send_approval_request(self, approval: Approval) -> None: ...
def listen_for_decisions(self) -> Iterator[ApprovalDecision]: ...
```
**Matrix implementation** (current):
- Sends alerts to `@dtoro:avispero` via Synapse (LXC 118)
- Approval requests as messages with ✅/❌ reactions
- Listens for reactions to record decisions
- Reuses existing `oikos/approve.py` Matrix delivery logic
**Future implementations** (pluggable):
- Webhook, email, Slack, etc.
- Registered in config, selected at runtime
### 8. Docker build + deploy pipeline
**Build:**
- `docker compose build` from the repo root
- Each service has a Dockerfile in `compose/<service>/`
- Shared base image: Python 3.12-slim + common deps (postgresql libs, etc.)
- Multi-stage builds for smaller images
**Deploy trigger:**
- Gitea webhook on push to `main` → hits a deploy endpoint on mac-mini
- Deploy script: `git pull && docker compose build && docker compose up -d`
- Alternatively: a "deployer" sidecar container watches Gitea and triggers rebuilds
- The existing 5-min launchd git pull can serve as a fallback deploy mechanism initially
**Health checks:**
- Each service has a Docker healthcheck
- `docker compose ps` shows health status
- API exposes `/health` for external monitoring
### 9. Ingress re-point (Caddy)
- In `dtoro/caddy-conf`: point `mcp.hubris.network` and `oikos.hubris.network` to
mac-mini's mesh address at port 8090 (API)
- Future: `hermes.hubris.network` → mac-mini:8092 (Hermes gateway)
- DNS and public URLs unchanged
- Verify Caddy → mac-mini reachability over netbird first (same as original plan)
### 10. mac-mini host setup
**Directory:**
- New directory: `~/oikos-os/` (or `/opt/oikos-os/`) — the repo clone
- This is where `docker compose` runs from
- Existing `/opt/homelab-context/` stays untouched until cleanup phase
**Prerequisites:**
- Docker Desktop (or OrbStack) — already installed
- SSH keys accessible at `~/.ssh/` (mounted into containers)
- Age key at `/etc/age/key.txt` (for one-time SOPS migration)
**Cleanup (deferred, after Docker OS is proven):**
- Stop launchd git-sync for `/opt/homelab-context`
- Remove native Hermes install at `~/.hermes/hermes-agent/`
- Remove `/opt/homelab-context` clone
- Remove any launchd plists for homelab services
### 11. Decommission apps/105 (deferred)
Keep apps/105 running all Oikos services (homelab-mcp, secrets-issuance,
oikos-console) as a fallback until the Docker OS is proven end-to-end.
**Cutover checklist (executed when ready):**
1. Verify Docker OS serves all traffic correctly
2. `systemctl disable --now homelab-mcp secrets-issuance oikos-console` on apps/105
3. Remove `/opt/homelab-mcp`, `/opt/oikos-console` checkouts on apps/105
4. Update Caddy backends to point to mac-mini exclusively
5. Remove Gitea webhooks 10, 11, 14 (or repoint to Docker deploy trigger)
## Reuse (do not reimplement)
| Existing code | What it becomes |
|---|---|
| `oikos/decide.py` | Risk classifier — adapted to read from DB, logic unchanged |
| `oikos/signal.py` | Signal engine — adapted to DB, lifecycle logic unchanged |
| `oikos/approve.py` | Approval engine — adapted to DB, Matrix delivery reused |
| `oikos/ledger.py` | Ledger — adapted to DB |
| `oikos/policy.py` + `policy.yaml` | Policy loader — unchanged, still reads YAML |
| `oikos/drift.py` | Drift detectors — adapted to write signals to DB |
| `oikos/relations.py` | Ontology graph walk — adapted to DB |
| `oikos/report.py` | Report generator — adapted to read from DB |
| `mcp/server.py` | Merged into `oikos/api/` — MCP protocol adapter on top of DB |
| `bin/homelab` logic | Migrated into API routes — same operations, REST interface |
| `oikos/scheduler.py` | Adapted — probes unchanged, output goes to DB |
| `oikos/approve.py` Matrix delivery | Reused as the Matrix notifier implementation |
## Risks / trade-offs
- **Hermes in Docker** — the agent's "world" is the container. Direct file access
to the host is limited to mounted volumes. SSH access is the bridge to the homelab.
Trade-off: less host integration, more isolation. Mitigated by the hybrid approach
(SSH now, actuator gateway later).
- **PostgreSQL as single point of failure** — if Postgres goes down, the OS loses
all state. Mitigated by Docker volume persistence + regular `pg_dump` backups
(can be automated by the scheduler itself once running).
- **Infisical bootstrapping** — migrating from SOPS to Infisical has a transition
period where both systems coexist. Keep SOPS as fallback until all services are
verified against Infisical.
- **Knowledge graph ingestion quality** — markdown-to-structured-data conversion
relies on path conventions and frontmatter. Docs without frontmatter may lose
metadata. Start simple, improve extraction over time.
- **Deploy downtime** — `docker compose up -d` may briefly interrupt services during
container replacement. Use `--scale` + health-gated rollout for zero-downtime
when needed (later).
- **Multi-agent concurrency** — PostgreSQL handles concurrent writers correctly,
but the actuator needs a lock mechanism to prevent two passes from acting on
the same signal simultaneously. Use `SELECT ... FOR UPDATE SKIP LOCKED`.
## Verification (end to end)
1. **Database:** `docker compose up postgres``psql` shows all tables; ingest
script populates entities from `inventory.yaml` and knowledge from `docs/`.
2. **API:** `curl http://localhost:8090/api/v1/hosts` returns the fleet;
MCP `list_services` tool works via the same endpoint.
3. **Scheduler:** trigger a probe pass — signals appear in DB, state snapshot written.
4. **Actuator:** raise a test `service-down` signal → actuator classifies →
auto-acts (restart) or escalates (Matrix approval request) → ledger entry written.
5. **Hermes:** connect from another workstation via `hermes gateway connect`
agent responds, can query the API via MCP, can SSH to hubris.
6. **Secrets:** Infisical running, services fetch secrets at startup, SOPS files
removed without breakage.
7. **Deploy:** `git push` → Gitea webhook → `docker compose build + up -d`
changes live within minutes.
8. **Knowledge:** MCP `search_knowledge("caddy")` returns relevant docs linked
to `entity:service:caddy`.
9. **Notifier:** raise a `critical` signal → Matrix message received →
✅ reaction → approval granted in DB.
10. **Cutover:** after all above passes, stop services on apps/105, verify
production traffic served only by Docker OS on mac-mini.
## Phasing
**Phase 1 — Foundation (DB + API):**
- PostgreSQL schema + models + migrations
- Ingest pipeline (inventory.yaml → DB, docs → knowledge graph)
- Unified API server (REST + MCP) reading from DB
- Migrate existing signals/ledger/approvals data into DB
**Phase 2 — Services (Scheduler + Actuator + Notifier):**
- Adapt scheduler to read/write DB
- Build actuator (Act stage) with kill-switch
- Matrix notifier (abstracted interface + impl)
- SSH-based execution (hybrid approach)
**Phase 3 — Agent (Hermes container):**
- Hermes Docker image
- Gateway mode config
- Homelab skills (homelab-ops)
- Connect from workstation, verify MCP + SSH access
**Phase 4 — Secrets (Infisical):**
- Stand up Infisical in the stack
- Migrate SOPS secrets
- Wire all services to Infisical
- Retire SOPS
**Phase 5 — Deploy pipeline:**
- Docker Compose fully working
- Gitea webhook → rebuild trigger
- Caddy ingress re-point to mac-mini
- Health checks + monitoring
**Phase 6 — Cutover + cleanup:**
- Verify end-to-end
- Stop apps/105 services
- Clean up mac-mini old artifacts
- Remove old deploy webhooks
## Out of scope (for now)
- Oikos Console web UI (deferred — built later on top of the API for specific tasks)
- Multi-node deployment (designed for, not implemented)
- Vector embeddings / semantic search (knowledge graph is structured-only for now)
- SSH-key-signed approval requests (same backlog as before)
- Prometheus / trend signals
- Actuator gateway pattern (Phase 2 of the hybrid — start with mounted SSH)
- Custom homelab skills beyond `homelab-ops` (add as needed)