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
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 structureontology.yaml— entity types, relationships, lifecyclepolicy.yaml— risk classes, approval rules, autonomy settingshermes/config.yaml— agent configurationhermes/SOUL.md— agent persona
What moves to PostgreSQL (mutable, runtime state):
signals/→signalstableledger/→ledger_entriestableoikos/state.json→state_snapshotstableapprovals/→approvalstableknowledge/wiki/narrative docs →knowledge_entities+knowledge_relationstables
What gets removed (no longer needed):
mcp/server.py— merged intooikos/api/server.pybin/homelab— replaced by the API; thin CLI client can be generated latersecrets-issuance/— replaced by Infisicaloikos/systemd/— replaced by Docker containersoikos/console/— deferred (built later as a specific UI on top of the API)mcp/deploy/,oikos/console/deploy/— replaced by Docker buildhosts/*.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 intoentities+relations - Walk
docs/directory → parse markdown, extract frontmatter → upsert intoknowledge_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/*.jsonlinto the DB oikos/state.json→state_snapshotstable
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 (replaceshomelab list)GET /api/v1/services— list servicesGET /api/v1/hosts/{name}— host detail (replaceshomelab status)GET /api/v1/services/{name}/health— service health (cache-first,?live=truefor probe)GET /api/v1/signals— list signals (?state=raised&severity=warning)POST /api/v1/signals/{id}/ack— acknowledgePOST /api/v1/signals/{id}/resolve— resolveGET /api/v1/approvals— pending approvalsPOST /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
- Body:
POST /api/v1/deploy/{service}— trigger a service deployWS /api/v1/events— real-time stream (signals, approvals, ledger entries)GET /api/v1/knowledge/{entity_id}— knowledge graph query for an entityGET /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/destructiveactions 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_actionis set - For each: classify via
decide.pyauto-act(reversible_low, contained, confident) → execute via SSH/API, verify, resolve signal, write ledgerescalate→ 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
/execendpoint (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 approvalshomelab-deploy— how to deploy/manage serviceshomelab-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
hubrisproject + 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.yamlonce 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 toentity:lxc:appsdocs/hosts/hubris.md→ links toentity:host:hubrisdocs/infrastructure/dns.md→ links toentity: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_relationstables
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:avisperovia Synapse (LXC 118) - Approval requests as messages with ✅/❌ reactions
- Listens for reactions to record decisions
- Reuses existing
oikos/approve.pyMatrix delivery logic
Future implementations (pluggable):
- Webhook, email, Slack, etc.
- Registered in config, selected at runtime
8. Docker build + deploy pipeline
Build:
docker compose buildfrom 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 psshows health status- API exposes
/healthfor external monitoring
9. Ingress re-point (Caddy)
- In
dtoro/caddy-conf: pointmcp.hubris.networkandoikos.hubris.networkto 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 composeruns 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-contextclone - 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):
- Verify Docker OS serves all traffic correctly
systemctl disable --now homelab-mcp secrets-issuance oikos-consoleon apps/105- Remove
/opt/homelab-mcp,/opt/oikos-consolecheckouts on apps/105 - Update Caddy backends to point to mac-mini exclusively
- 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_dumpbackups (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 -dmay 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)
- Database:
docker compose up postgres—psqlshows all tables; ingest script populates entities frominventory.yamland knowledge fromdocs/. - API:
curl http://localhost:8090/api/v1/hostsreturns the fleet; MCPlist_servicestool works via the same endpoint. - Scheduler: trigger a probe pass — signals appear in DB, state snapshot written.
- Actuator: raise a test
service-downsignal → actuator classifies → auto-acts (restart) or escalates (Matrix approval request) → ledger entry written. - Hermes: connect from another workstation via
hermes gateway connect→ agent responds, can query the API via MCP, can SSH to hubris. - Secrets: Infisical running, services fetch secrets at startup, SOPS files removed without breakage.
- Deploy:
git push→ Gitea webhook →docker compose build + up -d→ changes live within minutes. - Knowledge: MCP
search_knowledge("caddy")returns relevant docs linked toentity:service:caddy. - Notifier: raise a
criticalsignal → Matrix message received → ✅ reaction → approval granted in DB. - 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)