Files
oikos/plans/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md
dtoro bead722fac plans: pivot oikos consolidation to docker-based agentic homelab OS
Supersedes the launchd-based consolidation plan. Key changes:
- Docker-based deployment on mac-mini (docker compose)
- PostgreSQL for all mutable state (signals, ledger, knowledge graph)
- Infisical replaces SOPS+age for secrets management
- Unified API merges MCP server + homelab CLI (REST + MCP interfaces)
- Hermes agent runs in Docker (gateway mode, connect from any workstation)
- Knowledge graph in Postgres replaces narrative wiki files as agent context
- Structured entity relationships link docs to inventory entities
- Hybrid SSH access (mounted keys now, actuator gateway later)
- Git push → Gitea webhook → Docker rebuild = deploy trigger
- 6-phase rollout: DB → services → agent → secrets → deploy → cutover
2026-07-06 22:32:21 +02:00

34 KiB

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

# 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:

-- 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.mdentity: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.jsonstate_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_docssearch_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.yamlautonomy.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:

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 downtimedocker 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 postgrespsql 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)