From 14448a7dd9993dd2352aa5b103ac81af5c6aec41 Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 6 Jul 2026 21:56:56 +0200 Subject: [PATCH] chore: add plan --- ...idate-oikos-control-plane-onto-mac-mini.md | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 plans/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md diff --git a/plans/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md b/plans/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md new file mode 100644 index 0000000..b1717bd --- /dev/null +++ b/plans/2026-07-06-consolidate-oikos-control-plane-onto-mac-mini.md @@ -0,0 +1,220 @@ +# Plan: Consolidate the Oikos control plane onto mac-mini + +**Status:** Planned (2026-07-06) — approved, not yet executed. + +## Context + +The goal is an agentic OS that runs the homelab and keeps the operator in +control. Today the Oikos control plane is scattered and, worse, mis-placed: +`homelab-mcp`, `secrets-issuance`, and `oikos-console` all run on **LXC 105 +(apps) — a container inside hubris**, `oikos-scheduler` (the Observe stage) is +undeployed, and the Hermes agent runtime is spread across several clients. Two +problems follow: + +- **The observer lives inside the observed.** A scheduler on apps/105 can't + report "hubris is down" — it dies with hubris. The Observe/Act loop needs an + always-on node *outside* both hypervisors. +- **Deploy is fragmented.** Three Gitea webhooks (ids 10/11/14); webhook 14 + (console) has been 403-broken for a cause never found, so console never + auto-redeploys. + +**Decision:** make **mac-mini** the single Oikos control node — it's always-on, +external to both hubris and strong, on the mesh, can SSH/curl both, and already +runs Hermes. Move *everything* (MCP, secrets-issuance, console, scheduler, the +new actuator, Hermes) there; apps/105 stops running Oikos. Because mac-mini +already has a launchd git-sync (5-min pull), we replace all three Gitea webhooks +with pull-based deploy — which eliminates the webhook-403 problem entirely. + +This is a **macOS re-platforming** (systemd → launchd) plus an **ingress +re-point**. The public URLs (`mcp.`/`secrets.`/`oikos.hubris.network`) and DNS +are unchanged — only the Caddy backend and the runtime host move. + +## Target architecture + +``` +Caddy (LXC 121, ingress) ──reverse_proxy over netbird──▶ mac-mini (always-on, macOS) + mcp.hubris.network ─────────────────────────────▶ homelab-mcp :9810 + secrets.hubris.network ─────────────────────────────▶ secrets-issuance :9820 + oikos.hubris.network ─────────────────────────────▶ oikos-console :8091 + +mac-mini also runs (no ingress, mesh/SSH outward): + oikos-scheduler + actuator (launchd StartInterval 600s) ──ssh/curl──▶ hubris, strong + Hermes agent runtime (already here) + +Single synced checkout: /opt/homelab-context (5-min launchd git pull = deploy trigger) +apps/105: Oikos services stopped + removed; keeps its non-Oikos roles (artifacto, plantuml) +``` + +## Workstreams + +### 1. macOS runtime — launchd plists (`deploy/macos/`, new) + +One `LaunchDaemon` plist per service (run at boot, no GUI login), mirroring each +systemd unit's `ExecStart`/`Environment`, replacing `[Timer]` with launchd keys: + +| Service | plist | launchd equivalent of | +|---|---|---| +| `homelab-mcp` | `network.hubris.homelab-mcp.plist` | `mcp/deploy/homelab-mcp.service` (KeepAlive) | +| `secrets-issuance` | `network.hubris.secrets-issuance.plist` | its systemd unit (KeepAlive) | +| `oikos-console` | `network.hubris.oikos-console.plist` | `oikos/console/deploy/oikos-console.service` (KeepAlive) | +| `oikos-scheduler` | `network.hubris.oikos-scheduler.plist` | `oikos/systemd/oikos-scheduler.{service,timer}` (`StartInterval` 600) | + +- **Single checkout, shared venv.** All services run in-place from + `/opt/homelab-context` with one shared `.venv` (gitignored), instead of the + Linux per-service checkouts (`/opt/homelab-mcp`, `/opt/oikos-console`). This is + the concrete consolidation win: one clone, one sync, N daemons. +- **Confinement gap (tradeoff, document it):** the systemd units use + `ProtectSystem=strict`, `ReadOnlyPaths`, `NoNewPrivileges`, `PrivateTmp` — + launchd has no equivalent. Mitigate by running the daemons as a dedicated + non-root `_oikos` user (or `dtoro`) with least-privilege file perms, and note + the reduced isolation in the deploy README + OIKOS.md risk section. +- **`install-macos.sh`** — create `_oikos` user + venv (`pip install` the deps), + copy plists to `/Library/LaunchDaemons`, `launchctl bootstrap system …`. + Idempotent, mirroring `mcp/deploy/deploy.sh`'s shape. + +### 2. macOS deploy = the existing pull-sync (retire the 3 webhooks) + +- Extend mac-mini's existing launchd git-sync post-pull hook (the enrollment + `tools/post-pull.sh` path) to `launchctl kickstart -k` any service whose files + changed after a pull. Pull-based deploy replaces webhook-push deploy. +- **Retire Gitea webhooks 10, 11, 14** (operator step in Gitea UI). This removes + the never-solved console-webhook 403 by deleting the webhook, not fixing it. +- Update `knowledge/wiki/infrastructure/auto-deploy.md`: mac-mini services deploy + by pull-sync, not webhook; the webhook shape now applies only to remaining + in-lab config repos. + +### 3. Build the actuator + autonomy kill-switch (the Act stage) + +Still required — it's the control loop, now living on mac-mini. + +- **`oikos/act.py`** (new): read open Signals (`oikos_signal.list_signals( + state="raised")`); for each with a `recommended_action`, call + `oikos_decide.classify()`. `auto-act` (reversible_low, contained, confident) + → `start_acting()` (the existing unused transition), execute via + `homelab restart -y` (reuses `cmd_restart` + its `_record_change` ledger + write), run the Signal's `verification`, then `resolve()` or leave open with a + `result=failed` ledger entry so the next pass escalates. Everything else → + `oikos_approve.request(...)` + `acknowledge(note="escalated: ")`. A + ledger-history loop-guard caps auto-retries per (entity, kind). +- **Autonomy kill-switch** in `oikos/policy.yaml` (`autonomy.auto_act: + off|reversible_low`, `never_auto_act:` entity list) + loaders in + `oikos/policy.py` — the "keep control" guarantee. +- Wire `python3 oikos/act.py run` into the scheduler's launchd pass (after the + observe pass) and commit `signals/` + `ledger/`. + +### 4. Ingress re-point (Caddy backends: apps → mac-mini mesh) + +- In `dtoro/caddy-conf` (separate repo, tracked-config commit+push): change the + three `reverse_proxy` backends for `mcp.`/`secrets.`/`oikos.hubris.network` + from `192.168.8.205:` to **mac-mini's netbird address** + (`mac-mini-234-17.netbird.selfhosted:`), since Caddy (8.x) and mac-mini + (178.x) are cross-subnet — mesh is the robust path. **DNS and public URLs + unchanged.** +- **Verify Caddy → mac-mini reachability first** (Caddy container must resolve + + reach the netbird name; confirm netbird is on the caddy LXC or route via + hubris). If mesh isn't reachable from Caddy, fall back to the 178.x LAN IP and + confirm hubris routes 8.x↔178.x (it's dual-homed: hubris 178.181, apps 8.205). + +### 5. Secrets & SSH keys on mac-mini + +- **SOPS recipients:** grant mac-mini's `age_pubkey` + (`age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs`) on every + secret the moved services decrypt at runtime (secrets-issuance's issuing key, + approval HMAC `secrets/oikos-approval-hmac.yaml`, any service creds), then + `sops updatekeys`. Edit `.sops.yaml` recipient lists + re-key; commit via + `homelab client add mac-mini --finalize-pubkey …` path (comment-preserving). +- **SSH keys:** provision on mac-mini the `mcp-reader.key` restricted key the MCP + server SSH-proxies to hubris with (`HOMELAB_MCP_SSH_KEY`), plus root SSH access + to hubris + strong for the scheduler's disk probe (`_ssh` in + `oikos/scheduler.py:102`). Store under `/etc/homelab-mcp/` and `_oikos`'s + `~/.ssh` respectively. +- **Security note (document):** the age-key *issuer* now runs on a workstation. + Record the widened surface in OIKOS.md; least-privilege `_oikos` user + + keychain/file perms are the mitigation. + +### 6. Inventory / topology truth (same-session rule) + +- `inventory.yaml`: set `services.{homelab_mcp,secrets_issuance}.backend: + mac-mini`; add `oikos_console` + `oikos_scheduler` service entries backed by + mac-mini; give `mac-mini` a `runs:` list (homelab-mcp, secrets-issuance, + oikos-console, oikos-scheduler); remove `homelab-mcp`/`secrets-issuance` from + `apps.runs`. Keep `systemd_unit`→ note launchd label. +- Regenerate derived views: `mcp/build_host_files.py` (hosts/*.yaml) and + `oikos/gen-topology.py` (cards + `topology.md`). Update + `knowledge/wiki/containers/105-apps.md` and add a mac-mini control-node page + under `knowledge/wiki/hosts/` (or note on the workstation). + +### 7. Decommission on apps/105 (operator steps, documented not executed) + +`systemctl disable --now homelab-mcp secrets-issuance oikos-console` + their +`-deploy` webhook units on apps; remove `/opt/homelab-mcp`, `/opt/oikos-console` +checkouts. Captured in the deploy README as a cutover checklist. + +### 8. Docs + +`.agents/OIKOS.md` (control node = mac-mini; loop closed; risks), +`auto-deploy.md` (pull-sync), `deploy/macos/README.md` (cutover runbook), +`.agents/skills/autopilot-pass/SKILL.md` (supervised actuator run), host/service +cards (regenerated). + +## Critical files + +| File | Change | +|---|---| +| `deploy/macos/*.plist` + `install-macos.sh` + `README.md` | **New** — launchd runtime + cutover | +| `oikos/act.py` | **New** — actuator (Act stage) | +| `oikos/policy.yaml`, `oikos/policy.py` | Autonomy kill-switch + loaders | +| `oikos/systemd/run-scheduler.sh` → macOS variant | Observe+Act pass, commit signals/+ledger/ | +| `inventory.yaml` | Move service backends apps→mac-mini; mac-mini `runs:` | +| `hosts/*.yaml`, `oikos/cards/*`, `topology.md` | Regenerated | +| `dtoro/caddy-conf` (external repo) | 3 reverse_proxy backends → mac-mini mesh | +| `.sops.yaml` | Grant mac-mini recipient on moved-service secrets; re-key | +| `bin/homelab` | Optional `autopilot` subcommand | +| Docs: `OIKOS.md`, `auto-deploy.md`, `105-apps.md` | Reflect consolidation | + +## Reuse (do not reimplement) + +- Deploy scaffolding shape: `mcp/deploy/deploy.sh`, `oikos/console/deploy/`. +- Service definitions to port: `mcp/deploy/homelab-mcp.service`, + `oikos/console/deploy/oikos-console.service`, `oikos/systemd/*`. +- Actuator primitives: `oikos_decide.classify` (`decide.py:64`), signal + transitions incl. unused `start_acting` (`signal.py:125`), `cmd_restart` + + `_record_change` (`bin/homelab:603,190`), `oikos_approve.request` + (`bin/homelab:1370`), `oikos_ledger.history` (`ledger.py:59`). +- Comment-preserving inventory + `.sops.yaml` edits + `sops updatekeys`: + `homelab client add --finalize-pubkey` path in `bin/homelab`. + +## Risks / assumptions + +- **Caddy → mac-mini reachability** over netbird is unverified — workstream 4 + verifies before cutover; LAN 178.x is the fallback (hubris is dual-homed). +- **Lost sandboxing** on macOS (no `ProtectSystem`/`ReadOnlyPaths`) — mitigated + by a least-privilege `_oikos` daemon user, not eliminated. +- **secrets-issuance on a workstation** widens the security surface — accepted + per scope decision; documented. +- **Single-checkout race:** a git pull mid-request could serve half-updated code + — post-pull `kickstart -k` restarts services after the pull completes. +- macOS `/opt/homelab-context` already exists (mac-mini is enrolled); daemons + need it readable by `_oikos`. + +## Verification (end to end) + +1. **Runtime:** `install-macos.sh` on mac-mini; `launchctl print system/…` shows + all four daemons running; `curl 127.0.0.1:{9810,9820,8091}` responds locally. +2. **Ingress:** after the caddy-conf push + reload, `curl -I + https://mcp.hubris.network/mcp`, `.../` (console → 302 Authentik), + `secrets.hubris.network/health` → 200/302 through Caddy to mac-mini. +3. **Observe+Act loop:** trigger a `service-down` Signal; confirm the launchd + scheduler pass runs it `raised→acting→resolved`, writes a `restart`/`result: + ok` ledger entry, and pushes `signals/`+`ledger/`. Kill-switch: set + `autonomy.auto_act: off` → next pass escalates instead. +4. **Deploy-by-pull:** `git push` a trivial change; within 5 min mac-mini pulls + and `kickstart`s the changed daemon (no webhook involved). +5. **Cutover:** after decommission, `systemctl is-active` on apps shows the three + services `inactive`; production traffic served only by mac-mini; MCP tools + + console + a test enrollment all still work. + +## Out of scope (per prior direction) + +SSH-signed approval requests, Authentik step-up on the console POST, Prometheus / +temperature trend signals, and the generalize-to-any-homelab refactor.