# 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 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ mac-mini (Docker host, always-on) │ │ │ │ ┌──────────────┐ ┌────────────┐ ┌───────────────────────────┐ │ │ │ PostgreSQL │ │ Infisical │ │ Hermes Agent (gateway) │ │ │ │ │ │ (secrets) │ │ + homelab skills │ │ │ │ • inventory │ │ │ │ + MCP client → API │ │ │ │ • signals │ │ │ │ + SSH keys (mounted) │ │ │ │ • ledger │ │ │ │ │ │ │ │ • knowledge │ │ │ │ Any workstation connects │ │ │ │ • state │ │ │ │ via Hermes gateway protocol │ │ │ │ • approvals │ │ │ │ │ │ │ └──────┬────────┘ └─────────────┘ └──────────┬────────────────┘ │ │ │ │ │ │ ┌──────┴─────────────────────────────────────────┴──────────────┐ │ │ │ Oikos API (REST + MCP, one service) │ │ │ │ │ │ │ │ MCP interface (agent context): │ │ │ │ get_host, list_services, search_knowledge, get_entity, │ │ │ │ get_relations, get_change_history, get_state_snapshot │ │ │ │ │ │ │ │ REST interface (operations + operator): │ │ │ │ GET /api/v1/hosts, /services, /signals, /approvals │ │ │ │ POST /api/v1/exec — gated execution (actuator) │ │ │ │ POST /api/v1/approve — operator approval │ │ │ │ POST /api/v1/deploy — trigger service deploy │ │ │ │ WS /api/v1/events — real-time signal/approval stream │ │ │ │ │ │ │ │ Policy enforcement, risk classification, ledger recording │ │ │ └──────────────────────┬───────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────┴───────────────────────────────────────────┐ │ │ │ Scheduler (Observe) + Actuator (Act) │ │ │ │ • Periodic probes (10-min) → signals in DB │ │ │ │ • Reads open signals, classifies via decide.py │ │ │ │ • Auto-acts (reversible_low) or escalates (approval) │ │ │ │ • SSH to hubris/strong for probes + execution │ │ │ └──────────────────────┬───────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────┴───────────────────────────────────────────┐ │ │ │ Notifier (abstracted, pluggable) │ │ │ │ • Matrix (current) — alerts, approval requests │ │ │ │ • Future: webhook, email, Slack, etc. │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ Deploy: Gitea webhook → docker compose build + up -d │ │ Volumes: pg-data, infisical-data, ssh-keys (ro mount), hermes-config │ │ Network: Docker bridge (internal) + host network (SSH outbound) │ └──────────────────────────────────────────────────────────────────────┘ External: Caddy (LXC 121) → mac-mini mesh IP :8090 (API), future UIs apps/105 → fallback, kept running until Docker OS proven hubris/strong → SSH targets for scheduler/actuator Any workstation → Hermes gateway (connects to agent in Docker) Gitea (LXC 104) → webhook triggers Docker rebuild ``` ## 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//` - 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)