12 KiB
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-contextwith 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_oikosuser (ordtoro) with least-privilege file perms, and note the reduced isolation in the deploy README + OIKOS.md risk section. install-macos.sh— create_oikosuser + venv (pip installthe deps), copy plists to/Library/LaunchDaemons,launchctl bootstrap system …. Idempotent, mirroringmcp/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.shpath) tolaunchctl kickstart -kany 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 arecommended_action, calloikos_decide.classify().auto-act(reversible_low, contained, confident) →start_acting()(the existing unused transition), execute viahomelab restart <svc> -y(reusescmd_restart+ its_record_changeledger write), run the Signal'sverification, thenresolve()or leave open with aresult=failedledger entry so the next pass escalates. Everything else →oikos_approve.request(...)+acknowledge(note="escalated: <id>"). 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 inoikos/policy.py— the "keep control" guarantee. - Wire
python3 oikos/act.py runinto the scheduler's launchd pass (after the observe pass) and commitsignals/+ledger/.
4. Ingress re-point (Caddy backends: apps → mac-mini mesh)
- In
dtoro/caddy-conf(separate repo, tracked-config commit+push): change the threereverse_proxybackends formcp./secrets./oikos.hubris.networkfrom192.168.8.205:<port>to mac-mini's netbird address (mac-mini-234-17.netbird.selfhosted:<port>), 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 HMACsecrets/oikos-approval-hmac.yaml, any service creds), thensops updatekeys. Edit.sops.yamlrecipient lists + re-key; commit viahomelab client add mac-mini --finalize-pubkey …path (comment-preserving). - SSH keys: provision on mac-mini the
mcp-reader.keyrestricted 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 (_sshinoikos/scheduler.py:102). Store under/etc/homelab-mcp/and_oikos's~/.sshrespectively. - Security note (document): the age-key issuer now runs on a workstation.
Record the widened surface in OIKOS.md; least-privilege
_oikosuser + keychain/file perms are the mitigation.
6. Inventory / topology truth (same-session rule)
inventory.yaml: setservices.{homelab_mcp,secrets_issuance}.backend: mac-mini; addoikos_console+oikos_schedulerservice entries backed by mac-mini; givemac-miniaruns:list (homelab-mcp, secrets-issuance, oikos-console, oikos-scheduler); removehomelab-mcp/secrets-issuancefromapps.runs. Keepsystemd_unit→ note launchd label.- Regenerate derived views:
mcp/build_host_files.py(hosts/*.yaml) andoikos/gen-topology.py(cards +topology.md). Updateknowledge/wiki/containers/105-apps.mdand add a mac-mini control-node page underknowledge/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. unusedstart_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.yamledits +sops updatekeys:homelab client add --finalize-pubkeypath inbin/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_oikosdaemon 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 -krestarts services after the pull completes. - macOS
/opt/homelab-contextalready exists (mac-mini is enrolled); daemons need it readable by_oikos.
Verification (end to end)
- Runtime:
install-macos.shon mac-mini;launchctl print system/…shows all four daemons running;curl 127.0.0.1:{9810,9820,8091}responds locally. - 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. - Observe+Act loop: trigger a
service-downSignal; confirm the launchd scheduler pass runs itraised→acting→resolved, writes arestart/result: okledger entry, and pushessignals/+ledger/. Kill-switch: setautonomy.auto_act: off→ next pass escalates instead. - Deploy-by-pull:
git pusha trivial change; within 5 min mac-mini pulls andkickstarts the changed daemon (no webhook involved). - Cutover: after decommission,
systemctl is-activeon apps shows the three servicesinactive; 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.