Oikos Week 1: kernel policy, ontology, service contract, topology gen
Adds the Oikos agent-OS kernel: oikos/policy.yaml (risk classes + approval rules for every homelab/MCP command), oikos/ontology.yaml (8-domain systems model, typed relationships, node lifecycle), and OIKOS.md (OODA loop operating brief, linked from AGENTS.md). Extends inventory.yaml with a stable service contract (doc_page, config_repo, risk_notes) on all 17 services, and a structured archaeology: section for the 13 destroyed LXCs (was scattered comments + a narrative table). Fixes stale drift found in the process: authentik's backend pointed at a retired LXC (124); core has run on the VPS since 2026-05-31. Adds oikos/gen-topology.py, generating infrastructure/topology.md (Mermaid compute/ingress + storage views) from inventory.yaml. build_host_files.py now carries state/storage/depends_on into generated hosts/*.yaml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,11 @@ You are running on a machine that is part of the **hubris** homelab. The full
|
|||||||
context is in this checkout at `/opt/homelab-context/`. This file is the entry
|
context is in this checkout at `/opt/homelab-context/`. This file is the entry
|
||||||
point. Read it once at start, then keep working.
|
point. Read it once at start, then keep working.
|
||||||
|
|
||||||
|
The operating model — OODA loop, risk classes, approval rules, the ontology,
|
||||||
|
and node lifecycle — is defined in [OIKOS.md](OIKOS.md). Before any mutation,
|
||||||
|
classify the action against `oikos/policy.yaml`; when the class requires
|
||||||
|
approval, stop and ask the operator.
|
||||||
|
|
||||||
## 1. Who you are
|
## 1. Who you are
|
||||||
|
|
||||||
Run `hostname` (Linux) or `scutil --get LocalHostName` (macOS), then read:
|
Run `hostname` (Linux) or `scutil --get LocalHostName` (macOS), then read:
|
||||||
|
|||||||
107
OIKOS.md
Normal file
107
OIKOS.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Oikos — the operating model
|
||||||
|
|
||||||
|
Oikos (Greek: *household*) is the agent operating system layered on this
|
||||||
|
repo. It is not new infrastructure: `inventory.yaml` is the kernel data
|
||||||
|
structure, the `homelab` CLI and MCP server are the syscall surface, and
|
||||||
|
this page defines the rules everything above them follows.
|
||||||
|
|
||||||
|
Read this after [AGENTS.md](AGENTS.md). Machine-readable companions:
|
||||||
|
[oikos/ontology.yaml](oikos/ontology.yaml) (systems model),
|
||||||
|
[oikos/policy.yaml](oikos/policy.yaml) (risk & approval).
|
||||||
|
|
||||||
|
## The kernel loop: OODA
|
||||||
|
|
||||||
|
Every Oikos activity — scheduled probe, agent task, operator request — is
|
||||||
|
one pass through **Observe → Orient → Decide → Act**:
|
||||||
|
|
||||||
|
1. **Observe** — probes, drift detectors, and agent findings produce
|
||||||
|
**Signals** (structured records, not loose messages): pending updates,
|
||||||
|
high temperature, low disk, service down, cert expiry, stale backup,
|
||||||
|
inventory drift.
|
||||||
|
2. **Orient** — walk the ontology graph: what entity is affected, what
|
||||||
|
depends on it (blast radius), its lifecycle state, whether a runbook
|
||||||
|
matches, what the ledger says about past attempts.
|
||||||
|
3. **Decide** — the classifier scores **risk class × blast radius ×
|
||||||
|
confidence** and routes:
|
||||||
|
- **auto-act**: within autonomy policy, high confidence, contained radius
|
||||||
|
- **escalate**: operator approval via Matrix (✅/❌ reaction; destructive
|
||||||
|
actions additionally need a typed confirmation phrase)
|
||||||
|
- **queue**: informational — console + reports
|
||||||
|
The classifier can only *lower* autonomy relative to policy, never raise
|
||||||
|
it. When in doubt, escalate.
|
||||||
|
4. **Act** — execute through `homelab` commands or runbooks (never ad-hoc
|
||||||
|
SSH), then **verify** with the action's verification command, write a
|
||||||
|
**ledger** entry, resolve the Signal, and update docs in the same session.
|
||||||
|
|
||||||
|
## Primitives
|
||||||
|
|
||||||
|
| Primitive | What it is | Lives in |
|
||||||
|
|---|---|---|
|
||||||
|
| Host / Service | topology entities | `inventory.yaml` (+ generated `hosts/*.yaml`) |
|
||||||
|
| Secret | SOPS+age encrypted value, per-client recipients | `secrets/` + `.sops.yaml` |
|
||||||
|
| Runbook | executable workflow with risk class + verification | `runbooks/` (Week 2) |
|
||||||
|
| Signal | something needing attention, with lifecycle | `signals/` ledger (Week 3) |
|
||||||
|
| Change | one mutation: who, what, risk, approval, verification | `ledger/` (Week 2) |
|
||||||
|
| Approval | short-TTL signed grant for a gated action | approval engine (Week 3) |
|
||||||
|
| Incident | investigation narrative | `investigations/` |
|
||||||
|
| Plan | design doc for non-trivial work | `plans/` |
|
||||||
|
| Agent | enrolled client identity = its age pubkey | `inventory.yaml` + `.sops.yaml` |
|
||||||
|
|
||||||
|
## Risk classes (enforced, not advisory)
|
||||||
|
|
||||||
|
From [oikos/policy.yaml](oikos/policy.yaml):
|
||||||
|
|
||||||
|
- **read_only** — status, logs, docs, inventory. Unattended.
|
||||||
|
- **reversible_low** — restart, cache clear, sync pull. Unattended + ledger.
|
||||||
|
- **config_mutation** — tracked-config edits (commit+push, never local),
|
||||||
|
deploys, upgrades, DNS/ingress changes. Operator approval.
|
||||||
|
- **destructive** — destroy, format, wipe, rotate, revoke. Approval +
|
||||||
|
typed confirmation phrase.
|
||||||
|
|
||||||
|
Lifecycle gates modify these: `provisioning` nodes are freely mutable
|
||||||
|
(nothing depends on them); `deprecated` nodes accept no new dependents;
|
||||||
|
anything touching a `destroyed` node is drift.
|
||||||
|
|
||||||
|
## The systems model
|
||||||
|
|
||||||
|
Eight domains — physical, compute, network, storage, software,
|
||||||
|
identity & access, operations, external — cover everything in the lab;
|
||||||
|
entities are connected by typed edges (`hosts`, `provides`, `mounts`,
|
||||||
|
`stores-on`, `routes-to`, `can-decrypt`, `depends-on`, `backs-up-to`, …)
|
||||||
|
defined in [oikos/ontology.yaml](oikos/ontology.yaml). Rule of
|
||||||
|
completeness: **if it can break, be changed, or hold data, it has an
|
||||||
|
entity and edges.** Blast-radius questions ("what breaks if strong goes
|
||||||
|
down?") are graph walks, not doc archaeology.
|
||||||
|
|
||||||
|
Nodes move through an explicit lifecycle —
|
||||||
|
`planned → provisioning → active → migrating → deprecated → destroyed` —
|
||||||
|
stored as `state:` in inventory (absent = active). Destroyed nodes live in
|
||||||
|
the `archaeology:` section. Each transition is a runbook checklist;
|
||||||
|
deprecation completes only when inbound edges reach zero.
|
||||||
|
|
||||||
|
Generated views: [infrastructure/topology.md](infrastructure/topology.md)
|
||||||
|
(Mermaid, regenerated from inventory).
|
||||||
|
|
||||||
|
## Conventions carried forward
|
||||||
|
|
||||||
|
- Inventory is the truth; live state wins over narrative docs.
|
||||||
|
- Prefer `homelab` CLI and MCP over ad-hoc SSH.
|
||||||
|
- Meaningful changes update docs in the same session.
|
||||||
|
- Secrets are decrypted locally via per-client keys; never into docs/comments.
|
||||||
|
- Tracked configs change by commit + push, not local edits.
|
||||||
|
- Netbird is the preferred mesh path for new traffic.
|
||||||
|
- Agents are terse ([CAVEMAN.md](CAVEMAN.md)), verify claims, and fix
|
||||||
|
collateral drift when found.
|
||||||
|
|
||||||
|
## Build status (30-day roadmap, started 2026-07-05)
|
||||||
|
|
||||||
|
- **Week 1 (this)**: policy, ontology, service contract, archaeology,
|
||||||
|
topology generator, this brief.
|
||||||
|
- **Week 2**: context cards, `homelab service <name> …`, change ledger,
|
||||||
|
`node relations`, runbooks.
|
||||||
|
- **Week 3**: Prometheus + node_exporter, ops scheduler + state cache,
|
||||||
|
drift detectors, signal engine, decision classifier, approval engine
|
||||||
|
(Matrix ✅/❌), daily brief.
|
||||||
|
- **Week 4**: Oikos Console (oikos.hubris.network, behind Authentik with
|
||||||
|
step-up re-auth on approvals), per-agent age-key-signed approval
|
||||||
|
requests, docs pass, 60/90-day backlog.
|
||||||
@@ -5,6 +5,7 @@ name: apps
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: docker-apps
|
role: docker-apps
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 105
|
pve_id: 105
|
||||||
lan_ip: 192.168.8.205
|
lan_ip: 192.168.8.205
|
||||||
@@ -33,21 +34,29 @@ services_hosted:
|
|||||||
- name: artifacto
|
- name: artifacto
|
||||||
backend: apps
|
backend: apps
|
||||||
url: https://artifacto.hubris.network
|
url: https://artifacto.hubris.network
|
||||||
|
doc_page: containers/105-apps.md
|
||||||
|
config_repo: dtoro/Artifacto
|
||||||
- name: homelab_mcp
|
- name: homelab_mcp
|
||||||
backend: apps
|
backend: apps
|
||||||
port: 9810
|
port: 9810
|
||||||
systemd_unit: homelab-mcp
|
systemd_unit: homelab-mcp
|
||||||
public_host: mcp.hubris.network
|
public_host: mcp.hubris.network
|
||||||
endpoint: https://mcp.hubris.network/mcp
|
endpoint: https://mcp.hubris.network/mcp
|
||||||
|
doc_page: infrastructure/homelab-context.md
|
||||||
|
config_repo: dtoro/Homelab-Docs
|
||||||
note: MCP server. Read-only context + management. Reachable on the LAN via Caddy and from off-LAN via
|
note: MCP server. Read-only context + management. Reachable on the LAN via Caddy and from off-LAN via
|
||||||
Netbird (192.168.8.0/24 is a network resource routed through hubris).
|
Netbird (192.168.8.0/24 is a network resource routed through hubris).
|
||||||
|
risk_notes: "agents' primary read surface \u2014 outage degrades every agent to grepping the clone"
|
||||||
- name: secrets_issuance
|
- name: secrets_issuance
|
||||||
backend: apps
|
backend: apps
|
||||||
port: 9820
|
port: 9820
|
||||||
systemd_unit: secrets-issuance
|
systemd_unit: secrets-issuance
|
||||||
public_host: secrets.hubris.network
|
public_host: secrets.hubris.network
|
||||||
endpoint: https://secrets.hubris.network/issue
|
endpoint: https://secrets.hubris.network/issue
|
||||||
|
doc_page: operations/agent-enrollment.md
|
||||||
|
config_repo: dtoro/Homelab-Docs
|
||||||
note: Issues per-client age private keys. Gated at source-IP layer (mesh + LAN subnets in MESH_SUBNETS).
|
note: Issues per-client age private keys. Gated at source-IP layer (mesh + LAN subnets in MESH_SUBNETS).
|
||||||
|
risk_notes: "identity issuance \u2014 any change is security-sensitive; key operations are destructive-class"
|
||||||
age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
|
age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
|
||||||
see_also:
|
see_also:
|
||||||
- containers/105-apps.md
|
- containers/105-apps.md
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: arriman
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: arr-stack
|
role: arr-stack
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 122
|
pve_id: 122
|
||||||
lan_ip: 192.168.8.245
|
lan_ip: 192.168.8.245
|
||||||
@@ -28,6 +29,7 @@ services_hosted:
|
|||||||
- name: arr_stack
|
- name: arr_stack
|
||||||
backend: arriman
|
backend: arriman
|
||||||
note: jellyseerr / qbit / sab on docker compose
|
note: jellyseerr / qbit / sab on docker compose
|
||||||
|
doc_page: containers/122-arriman.md
|
||||||
notes:
|
notes:
|
||||||
- Migrated from hubris to strong 2026-07-05 (Phase 2). Library on ludo-lvm.
|
- Migrated from hubris to strong 2026-07-05 (Phase 2). Library on ludo-lvm.
|
||||||
- Contains homarr, radarr, sonarr, lidarr, sabnzbd, qbittorrent, bazarr, flaresolverr, prowlarr, jellyseerr
|
- Contains homarr, radarr, sonarr, lidarr, sabnzbd, qbittorrent, bazarr, flaresolverr, prowlarr, jellyseerr
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: auth-outpost
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: authentik-gateway
|
role: authentik-gateway
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 106
|
pve_id: 106
|
||||||
lan_ip: 192.168.8.6
|
lan_ip: 192.168.8.6
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: caddy
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: reverse-proxy
|
role: reverse-proxy
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 121
|
pve_id: 121
|
||||||
lan_ip: 192.168.8.175
|
lan_ip: 192.168.8.175
|
||||||
@@ -23,6 +24,10 @@ services_hosted:
|
|||||||
backend: caddy
|
backend: caddy
|
||||||
role: reverse-proxy
|
role: reverse-proxy
|
||||||
note: terminates all *.hubris.network
|
note: terminates all *.hubris.network
|
||||||
|
doc_page: containers/121-caddy.md
|
||||||
|
config_repo: dtoro/caddy-conf
|
||||||
|
risk_notes: "wide blast radius \u2014 every *.hubris.network route rides on it (see oikos/policy.yaml\
|
||||||
|
\ service_overrides)"
|
||||||
notes:
|
notes:
|
||||||
- Terminates all *.hubris.network
|
- Terminates all *.hubris.network
|
||||||
- /etc/caddy is a git checkout of dtoro/caddy-conf
|
- /etc/caddy is a git checkout of dtoro/caddy-conf
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: dns
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: dns-server
|
role: dns-server
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 107
|
pve_id: 107
|
||||||
lan_ip: 192.168.8.2
|
lan_ip: 192.168.8.2
|
||||||
@@ -14,13 +15,13 @@ mesh_globals:
|
|||||||
- netbird
|
- netbird
|
||||||
- tailscale
|
- tailscale
|
||||||
runs:
|
runs:
|
||||||
- authentik
|
- dns
|
||||||
services_hosted:
|
services_hosted:
|
||||||
- name: authentik
|
- name: dns
|
||||||
url: https://auth.hubris.network
|
|
||||||
backend: dns
|
backend: dns
|
||||||
dns: null
|
|
||||||
note: Technitium DNS, split-horizon zone
|
note: Technitium DNS, split-horizon zone
|
||||||
|
doc_page: containers/107-dns.md
|
||||||
|
risk_notes: "LAN-wide resolver \u2014 misconfig breaks name resolution for every client"
|
||||||
notes:
|
notes:
|
||||||
- Technitium DNS, split-horizon zone for *.hubris.network
|
- Technitium DNS, split-horizon zone for *.hubris.network
|
||||||
- Primary DNS for 192.168.8.0/24 LAN (inventory.services.dns references this)
|
- Primary DNS for 192.168.8.0/24 LAN (inventory.services.dns references this)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: elementsynapse
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: matrix-server
|
role: matrix-server
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 118
|
pve_id: 118
|
||||||
lan_ip: 192.168.8.242
|
lan_ip: 192.168.8.242
|
||||||
@@ -22,6 +23,8 @@ services_hosted:
|
|||||||
- name: matrix
|
- name: matrix
|
||||||
url: https://matrix.hubris.network
|
url: https://matrix.hubris.network
|
||||||
backend: elementsynapse
|
backend: elementsynapse
|
||||||
|
doc_page: containers/118-elementsynapse.md
|
||||||
|
risk_notes: "alert/approval channel for Oikos \u2014 outage silences agent escalation"
|
||||||
notes:
|
notes:
|
||||||
- Migrated from hubris to strong 2026-07-05 (Phase 1 of strong migration plan).
|
- Migrated from hubris to strong 2026-07-05 (Phase 1 of strong migration plan).
|
||||||
see_also:
|
see_also:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: gitea
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: git-server
|
role: git-server
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 104
|
pve_id: 104
|
||||||
lan_ip: 192.168.8.121
|
lan_ip: 192.168.8.121
|
||||||
@@ -26,6 +27,9 @@ services_hosted:
|
|||||||
url: https://git.hubris.network
|
url: https://git.hubris.network
|
||||||
backend: gitea
|
backend: gitea
|
||||||
backend_url: http://192.168.8.121:3000
|
backend_url: http://192.168.8.121:3000
|
||||||
|
doc_page: containers/104-gitea.md
|
||||||
|
config_repo: dtoro/gitea-customizations
|
||||||
|
risk_notes: hosts all config repos + deploy webhooks; outage blocks auto-deploy and sync
|
||||||
notes:
|
notes:
|
||||||
- Bare repos live at /mnt/library/repos/dtoro/*.git
|
- Bare repos live at /mnt/library/repos/dtoro/*.git
|
||||||
see_also:
|
see_also:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: grimmory
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: book-library
|
role: book-library
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 130
|
pve_id: 130
|
||||||
lan_ip: 192.168.8.247
|
lan_ip: 192.168.8.247
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: haos
|
|||||||
kind: vm
|
kind: vm
|
||||||
os: linux
|
os: linux
|
||||||
role: home-automation
|
role: home-automation
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 108
|
pve_id: 108
|
||||||
lan_ip: 192.168.8.101
|
lan_ip: 192.168.8.101
|
||||||
@@ -21,6 +22,7 @@ runs:
|
|||||||
services_hosted:
|
services_hosted:
|
||||||
- name: haos
|
- name: haos
|
||||||
backend: haos
|
backend: haos
|
||||||
|
doc_page: vms/108-haos.md
|
||||||
see_also:
|
see_also:
|
||||||
- vms/108-haos.md
|
- vms/108-haos.md
|
||||||
mcp_endpoint: https://mcp.hubris.network/mcp
|
mcp_endpoint: https://mcp.hubris.network/mcp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: house
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: family-planner
|
role: family-planner
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 129
|
pve_id: 129
|
||||||
lan_ip: 192.168.8.244
|
lan_ip: 192.168.8.244
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: hubris
|
|||||||
kind: proxmox-host
|
kind: proxmox-host
|
||||||
os: linux
|
os: linux
|
||||||
role: hypervisor
|
role: hypervisor
|
||||||
|
state: active
|
||||||
lan_ip: 192.168.8.77
|
lan_ip: 192.168.8.77
|
||||||
mesh:
|
mesh:
|
||||||
netbird:
|
netbird:
|
||||||
@@ -28,6 +29,8 @@ services_hosted:
|
|||||||
url: https://proxmox.hubris.network
|
url: https://proxmox.hubris.network
|
||||||
backend: hubris
|
backend: hubris
|
||||||
port: 8006
|
port: 8006
|
||||||
|
doc_page: hosts/hubris.md
|
||||||
|
risk_notes: "hypervisor UI \u2014 changes here affect every guest on the node"
|
||||||
age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
|
age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
|
||||||
see_also:
|
see_also:
|
||||||
- hosts/hubris.md
|
- hosts/hubris.md
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: jellyfin
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: media-server
|
role: media-server
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 101
|
pve_id: 101
|
||||||
lan_ip: 192.168.8.246
|
lan_ip: 192.168.8.246
|
||||||
@@ -25,6 +26,9 @@ services_hosted:
|
|||||||
- name: jellyfin
|
- name: jellyfin
|
||||||
url: https://media.hubris.network
|
url: https://media.hubris.network
|
||||||
backend: jellyfin
|
backend: jellyfin
|
||||||
|
doc_page: containers/101-jellyfin.md
|
||||||
|
risk_notes: native Authentik OIDC via SSO-Auth plugin, no Caddy forward-auth gate; VAAPI transcode depends
|
||||||
|
on GPU passthrough on strong
|
||||||
notes:
|
notes:
|
||||||
- Jellyfin 10.11.11 with VAAPI hardware acceleration (Radeon 680M iGPU on strong)
|
- Jellyfin 10.11.11 with VAAPI hardware acceleration (Radeon 680M iGPU on strong)
|
||||||
- 4 cores / 8 GiB RAM / 1 GiB swap
|
- 4 cores / 8 GiB RAM / 1 GiB swap
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: mac-mini
|
|||||||
kind: workstation
|
kind: workstation
|
||||||
os: macos
|
os: macos
|
||||||
role: dev
|
role: dev
|
||||||
|
state: active
|
||||||
lan_ip: 192.168.178.182
|
lan_ip: 192.168.178.182
|
||||||
mesh:
|
mesh:
|
||||||
netbird:
|
netbird:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: mule-images
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: photo-management
|
role: photo-management
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 120
|
pve_id: 120
|
||||||
lan_ip: 192.168.8.136
|
lan_ip: 192.168.8.136
|
||||||
@@ -25,6 +26,8 @@ services_hosted:
|
|||||||
- name: photos
|
- name: photos
|
||||||
url: https://photos.hubris.network
|
url: https://photos.hubris.network
|
||||||
backend: mule-images
|
backend: mule-images
|
||||||
|
doc_page: containers/120-mule-images.md
|
||||||
|
config_repo: dtoro/mule-image
|
||||||
see_also:
|
see_also:
|
||||||
- containers/120-mule-images.md
|
- containers/120-mule-images.md
|
||||||
mcp_endpoint: https://mcp.hubris.network/mcp
|
mcp_endpoint: https://mcp.hubris.network/mcp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: netbird-vps
|
|||||||
kind: external
|
kind: external
|
||||||
os: linux
|
os: linux
|
||||||
role: netbird-mgmt
|
role: netbird-mgmt
|
||||||
|
state: active
|
||||||
mesh:
|
mesh:
|
||||||
netbird:
|
netbird:
|
||||||
ip: 100.122.165.149
|
ip: 100.122.165.149
|
||||||
@@ -16,6 +17,16 @@ mesh_globals:
|
|||||||
- tailscale
|
- tailscale
|
||||||
ssh:
|
ssh:
|
||||||
user: root
|
user: root
|
||||||
|
runs:
|
||||||
|
- authentik
|
||||||
|
services_hosted:
|
||||||
|
- name: authentik
|
||||||
|
url: https://auth.hubris.network
|
||||||
|
backend: netbird-vps
|
||||||
|
doc_page: containers/106-auth-outpost.md
|
||||||
|
note: core runs on the VPS since 2026-05-31; LAN forward-auth outpost is auth-outpost (LXC 106) at 192.168.8.6:9000.
|
||||||
|
Previous backend value "authentik" referenced the retired embedded-outpost host (LXC 124).
|
||||||
|
risk_notes: "SSO provider \u2014 outage locks login to OIDC/forward-auth services"
|
||||||
notes:
|
notes:
|
||||||
- "Public IONOS VPS \u2014 hosts the vanilla netbird mgmt+signal+relay+dashboard stack + host coturn (see\
|
- "Public IONOS VPS \u2014 hosts the vanilla netbird mgmt+signal+relay+dashboard stack + host coturn (see\
|
||||||
\ infrastructure/vps-hardening.md + infrastructure/mesh.md changelog 2026-05-21)."
|
\ infrastructure/vps-hardening.md + infrastructure/mesh.md changelog 2026-05-21)."
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: nextcloud
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: file-sync
|
role: file-sync
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 114
|
pve_id: 114
|
||||||
lan_ip: 192.168.8.224
|
lan_ip: 192.168.8.224
|
||||||
@@ -25,6 +26,7 @@ services_hosted:
|
|||||||
- name: nextcloud
|
- name: nextcloud
|
||||||
url: https://cloud.hubris.network
|
url: https://cloud.hubris.network
|
||||||
backend: nextcloud
|
backend: nextcloud
|
||||||
|
doc_page: containers/114-nextcloud.md
|
||||||
see_also:
|
see_also:
|
||||||
- containers/114-nextcloud.md
|
- containers/114-nextcloud.md
|
||||||
mcp_endpoint: https://mcp.hubris.network/mcp
|
mcp_endpoint: https://mcp.hubris.network/mcp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: nfs-export
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: storage-export
|
role: storage-export
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 102
|
pve_id: 102
|
||||||
lan_ip: 192.168.8.200
|
lan_ip: 192.168.8.200
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: paperless
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: document-archive
|
role: document-archive
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 103
|
pve_id: 103
|
||||||
lan_ip: 192.168.8.130
|
lan_ip: 192.168.8.130
|
||||||
@@ -25,6 +26,8 @@ services_hosted:
|
|||||||
- name: paperless
|
- name: paperless
|
||||||
url: https://paperless.hubris.network
|
url: https://paperless.hubris.network
|
||||||
backend: paperless
|
backend: paperless
|
||||||
|
doc_page: containers/103-paperless.md
|
||||||
|
risk_notes: "document archive \u2014 treat data as irreplaceable; DB operations are destructive-class"
|
||||||
see_also:
|
see_also:
|
||||||
- containers/103-paperless.md
|
- containers/103-paperless.md
|
||||||
mcp_endpoint: https://mcp.hubris.network/mcp
|
mcp_endpoint: https://mcp.hubris.network/mcp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: rclone
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: backup
|
role: backup
|
||||||
|
state: active
|
||||||
mesh:
|
mesh:
|
||||||
netbird:
|
netbird:
|
||||||
fqdn: rclone.netbird.selfhosted
|
fqdn: rclone.netbird.selfhosted
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: republic-laptop
|
|||||||
kind: workstation
|
kind: workstation
|
||||||
os: linux
|
os: linux
|
||||||
role: primary-dev
|
role: primary-dev
|
||||||
|
state: active
|
||||||
mesh:
|
mesh:
|
||||||
netbird:
|
netbird:
|
||||||
fqdn: republic-laptop.netbird.selfhosted
|
fqdn: republic-laptop.netbird.selfhosted
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: romm
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: rom-manager
|
role: rom-manager
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 134
|
pve_id: 134
|
||||||
lan_ip: 192.168.8.249
|
lan_ip: 192.168.8.249
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: seanime
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: anime-media-server
|
role: anime-media-server
|
||||||
|
state: active
|
||||||
host: strong
|
host: strong
|
||||||
pve_id: 133
|
pve_id: 133
|
||||||
lan_ip: 192.168.8.248
|
lan_ip: 192.168.8.248
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: sophia
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: workshop
|
role: workshop
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 119
|
pve_id: 119
|
||||||
lan_ip: 192.168.8.109
|
lan_ip: 192.168.8.109
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: strong
|
|||||||
kind: proxmox-host
|
kind: proxmox-host
|
||||||
os: linux
|
os: linux
|
||||||
role: hypervisor
|
role: hypervisor
|
||||||
|
state: active
|
||||||
lan_ip: 192.168.178.181
|
lan_ip: 192.168.178.181
|
||||||
mesh_globals:
|
mesh_globals:
|
||||||
primary: netbird
|
primary: netbird
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: trmnl
|
|||||||
kind: lxc
|
kind: lxc
|
||||||
os: linux
|
os: linux
|
||||||
role: trmnl-middleware
|
role: trmnl-middleware
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 128
|
pve_id: 128
|
||||||
lan_ip: 192.168.8.211
|
lan_ip: 192.168.8.211
|
||||||
@@ -21,6 +22,8 @@ services_hosted:
|
|||||||
backend: trmnl
|
backend: trmnl
|
||||||
url: https://trmnl.hubris.network
|
url: https://trmnl.hubris.network
|
||||||
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
|
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
|
||||||
|
doc_page: containers/128-trmnl.md
|
||||||
|
config_repo: dtoro/terminalito
|
||||||
see_also:
|
see_also:
|
||||||
- containers/128-trmnl.md
|
- containers/128-trmnl.md
|
||||||
mcp_endpoint: https://mcp.hubris.network/mcp
|
mcp_endpoint: https://mcp.hubris.network/mcp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ name: zimaos
|
|||||||
kind: vm
|
kind: vm
|
||||||
os: linux
|
os: linux
|
||||||
role: nas-frontend-eval
|
role: nas-frontend-eval
|
||||||
|
state: active
|
||||||
host: hubris
|
host: hubris
|
||||||
pve_id: 100
|
pve_id: 100
|
||||||
lan_ip: 192.168.8.195
|
lan_ip: 192.168.8.195
|
||||||
@@ -20,6 +21,7 @@ services_hosted:
|
|||||||
- name: zimaos
|
- name: zimaos
|
||||||
url: https://zimaos.hubris.network
|
url: https://zimaos.hubris.network
|
||||||
backend: zimaos
|
backend: zimaos
|
||||||
|
doc_page: vms/100-zimaos.md
|
||||||
see_also:
|
see_also:
|
||||||
- vms/100-zimaos.md
|
- vms/100-zimaos.md
|
||||||
mcp_endpoint: https://mcp.hubris.network/mcp
|
mcp_endpoint: https://mcp.hubris.network/mcp
|
||||||
|
|||||||
93
infrastructure/topology.md
Normal file
93
infrastructure/topology.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<!-- Generated by oikos/gen-topology.py from inventory.yaml. -->
|
||||||
|
<!-- Do NOT edit by hand - your changes will be overwritten. -->
|
||||||
|
|
||||||
|
# Topology (generated)
|
||||||
|
|
||||||
|
Source: [inventory.yaml](../inventory.yaml) — 2 hypervisors, 19 LXCs, 2 VMs, 2 workstations, 17 services.
|
||||||
|
Edge semantics: [oikos/ontology.yaml](../oikos/ontology.yaml). Operating model: [OIKOS.md](../OIKOS.md).
|
||||||
|
|
||||||
|
## Compute & ingress
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph hubris_sub["hubris (Proxmox)"]
|
||||||
|
trmnl["trmnl<br/>LXC 128<br/>trmnl-middleware<br/>192.168.8.211"]
|
||||||
|
nfs_export["nfs-export<br/>LXC 102<br/>storage-export<br/>192.168.8.200"]
|
||||||
|
paperless["paperless<br/>LXC 103<br/>document-archive<br/>192.168.8.130"]
|
||||||
|
gitea["gitea<br/>LXC 104<br/>git-server<br/>192.168.8.121"]
|
||||||
|
apps["apps<br/>LXC 105<br/>docker-apps<br/>192.168.8.205"]
|
||||||
|
auth_outpost["auth-outpost<br/>LXC 106<br/>authentik-gateway<br/>192.168.8.6"]
|
||||||
|
dns["dns<br/>LXC 107<br/>dns-server<br/>192.168.8.2"]
|
||||||
|
nextcloud["nextcloud<br/>LXC 114<br/>file-sync<br/>192.168.8.224"]
|
||||||
|
sophia["sophia<br/>LXC 119<br/>workshop<br/>192.168.8.109"]
|
||||||
|
mule_images["mule-images<br/>LXC 120<br/>photo-management<br/>192.168.8.136"]
|
||||||
|
caddy["caddy<br/>LXC 121<br/>reverse-proxy<br/>192.168.8.175"]
|
||||||
|
zimaos["zimaos<br/>VM 100<br/>nas-frontend-eval<br/>192.168.8.195"]
|
||||||
|
haos["haos<br/>VM 108<br/>home-automation<br/>192.168.8.101"]
|
||||||
|
end
|
||||||
|
subgraph strong_sub["strong (Proxmox)"]
|
||||||
|
house["house<br/>LXC 129<br/>family-planner<br/>192.168.8.244"]
|
||||||
|
jellyfin["jellyfin<br/>LXC 101<br/>media-server<br/>192.168.8.246"]
|
||||||
|
elementsynapse["elementsynapse<br/>LXC 118<br/>matrix-server<br/>192.168.8.242"]
|
||||||
|
arriman["arriman<br/>LXC 122<br/>arr-stack<br/>192.168.8.245"]
|
||||||
|
grimmory["grimmory<br/>LXC 130<br/>book-library<br/>192.168.8.247"]
|
||||||
|
seanime["seanime<br/>LXC 133<br/>anime-media-server<br/>192.168.8.248"]
|
||||||
|
romm["romm<br/>LXC 134<br/>rom-manager<br/>192.168.8.249"]
|
||||||
|
end
|
||||||
|
rclone["rclone<br/>lxc<br/>backup"]
|
||||||
|
republic_laptop([republic-laptop<br/>workstation<br/>primary-dev])
|
||||||
|
mac_mini([mac-mini<br/>workstation<br/>dev<br/>192.168.178.182])
|
||||||
|
netbird_vps[[netbird-vps<br/>external<br/>netbird-mgmt]]
|
||||||
|
url_artifacto(["artifacto.hubris.network"]) -->|routes-to| apps
|
||||||
|
url_authentik(["auth.hubris.network"]) -->|routes-to| netbird_vps
|
||||||
|
url_gitea(["git.hubris.network"]) -->|routes-to| gitea
|
||||||
|
url_homelab_mcp(["mcp.hubris.network"]) -->|routes-to| apps
|
||||||
|
url_jellyfin(["media.hubris.network"]) -->|routes-to| jellyfin
|
||||||
|
url_matrix(["matrix.hubris.network"]) -->|routes-to| elementsynapse
|
||||||
|
url_nextcloud(["cloud.hubris.network"]) -->|routes-to| nextcloud
|
||||||
|
url_paperless(["paperless.hubris.network"]) -->|routes-to| paperless
|
||||||
|
url_photos(["photos.hubris.network"]) -->|routes-to| mule_images
|
||||||
|
url_proxmox_ui(["proxmox.hubris.network"]) -->|routes-to| hubris_sub
|
||||||
|
url_secrets_issuance(["secrets.hubris.network"]) -->|routes-to| apps
|
||||||
|
url_trmnl(["trmnl.hubris.network"]) -->|routes-to| trmnl
|
||||||
|
url_zimaos(["zimaos.hubris.network"]) -->|routes-to| zimaos
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage (mounts)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
mnt_library[("/mnt/library")]
|
||||||
|
mnt_media_local[("/mnt/media_local")]
|
||||||
|
mnt_media_local_anime[("/mnt/media_local/anime")]
|
||||||
|
apps["apps"] -->|mounts| mnt_library
|
||||||
|
arriman["arriman"] -->|mounts| mnt_media_local
|
||||||
|
gitea["gitea"] -->|mounts| mnt_library
|
||||||
|
grimmory["grimmory"] -->|mounts| mnt_media_local
|
||||||
|
hubris["hubris"] -->|mounts| mnt_library
|
||||||
|
jellyfin["jellyfin"] -->|mounts| mnt_media_local
|
||||||
|
mule_images["mule-images"] -->|mounts| mnt_library
|
||||||
|
nextcloud["nextcloud"] -->|mounts| mnt_library
|
||||||
|
paperless["paperless"] -->|mounts| mnt_library
|
||||||
|
romm["romm"] -->|mounts| mnt_media_local
|
||||||
|
seanime["seanime"] -->|mounts| mnt_media_local_anime
|
||||||
|
sophia["sophia"] -->|mounts| mnt_library
|
||||||
|
```
|
||||||
|
|
||||||
|
## Archaeology (destroyed nodes)
|
||||||
|
|
||||||
|
| Node | ID | Destroyed | Reason |
|
||||||
|
|---|---|---|---|
|
||||||
|
| plato | 126 | 2026-06-28 | notes workspace decommissioned; data retained at /mnt/library/documents/plato |
|
||||||
|
| claudio-bot | 123 | 2026-06-04 | replaced by Hermes Agent on mac-mini; monitoring moved to homelab-health-watchdog cron |
|
||||||
|
| mule-photos-new | 127 | 2026-05-22 | PhotoPrism test stack promoted to LXC 120 (Mulimage 2.0 merge) |
|
||||||
|
| heaper | 116 | 2026-05-14 | decommissioned; data retained at /mnt/library/heaper |
|
||||||
|
| syncthing | 109 | 2026-05-14 | decommissioned; library subtree was empty |
|
||||||
|
| seafile | 125 | 2026-05-13 | Seafile Pro evaluation rejected; files.hubris.network removed from caddy + dns |
|
||||||
|
| arr-yunohost | 100 | 2026-04-28 | migrated to docker stack on arriman (LXC 122) |
|
||||||
|
| flaresolverr | 106 | 2026-04-28 | folded into the arriman docker compose |
|
||||||
|
| marimo | 107 | 2026-04-28 | decommissioned |
|
||||||
|
| photoprism | 110 | 2026-04-28 | replaced by mule-images (LXC 120) |
|
||||||
|
| karakeep | 111 | 2026-04-28 | decommissioned |
|
||||||
|
| immich | 112 | 2026-04-28 | replaced by mule-images (LXC 120) |
|
||||||
|
| reticulum | 115 | 2026-04-28 | decommissioned |
|
||||||
121
inventory.yaml
121
inventory.yaml
@@ -35,72 +35,118 @@ mesh:
|
|||||||
- tailscale
|
- tailscale
|
||||||
netbird_subnet: 100.122.0.0/16
|
netbird_subnet: 100.122.0.0/16
|
||||||
netbird_domain: netbird.selfhosted
|
netbird_domain: netbird.selfhosted
|
||||||
|
# Service contract (Oikos, 2026-07-05): each service should carry
|
||||||
|
# backend host/container that runs it (required)
|
||||||
|
# url public URL if ingress-exposed
|
||||||
|
# doc_page owning wiki page
|
||||||
|
# config_repo tracked config repo, if any (mutations go commit+push)
|
||||||
|
# health health-check URL if it differs from `url`
|
||||||
|
# risk_notes what an agent must know before touching it
|
||||||
|
# See oikos/ontology.yaml + oikos/policy.yaml.
|
||||||
services:
|
services:
|
||||||
proxmox_ui:
|
proxmox_ui:
|
||||||
url: https://proxmox.hubris.network
|
url: https://proxmox.hubris.network
|
||||||
backend: hubris
|
backend: hubris
|
||||||
port: 8006
|
port: 8006
|
||||||
|
doc_page: hosts/hubris.md
|
||||||
|
risk_notes: hypervisor UI — changes here affect every guest on the node
|
||||||
gitea:
|
gitea:
|
||||||
url: https://git.hubris.network
|
url: https://git.hubris.network
|
||||||
backend: gitea
|
backend: gitea
|
||||||
backend_url: http://192.168.8.121:3000
|
backend_url: http://192.168.8.121:3000
|
||||||
|
doc_page: containers/104-gitea.md
|
||||||
|
config_repo: dtoro/gitea-customizations
|
||||||
|
risk_notes: hosts all config repos + deploy webhooks; outage blocks auto-deploy and sync
|
||||||
caddy:
|
caddy:
|
||||||
backend: caddy
|
backend: caddy
|
||||||
role: reverse-proxy
|
role: reverse-proxy
|
||||||
note: terminates all *.hubris.network
|
note: terminates all *.hubris.network
|
||||||
|
doc_page: containers/121-caddy.md
|
||||||
|
config_repo: dtoro/caddy-conf
|
||||||
|
risk_notes: wide blast radius — every *.hubris.network route rides on it (see oikos/policy.yaml service_overrides)
|
||||||
authentik:
|
authentik:
|
||||||
url: https://auth.hubris.network
|
url: https://auth.hubris.network
|
||||||
backend: authentik
|
backend: netbird-vps
|
||||||
|
doc_page: containers/106-auth-outpost.md
|
||||||
|
note: >-
|
||||||
|
core runs on the VPS since 2026-05-31; LAN forward-auth outpost is
|
||||||
|
auth-outpost (LXC 106) at 192.168.8.6:9000. Previous backend value
|
||||||
|
"authentik" referenced the retired embedded-outpost host (LXC 124).
|
||||||
|
risk_notes: SSO provider — outage locks login to OIDC/forward-auth services
|
||||||
dns:
|
dns:
|
||||||
backend: dns
|
backend: dns
|
||||||
note: Technitium DNS, split-horizon zone
|
note: Technitium DNS, split-horizon zone
|
||||||
|
doc_page: containers/107-dns.md
|
||||||
|
risk_notes: LAN-wide resolver — misconfig breaks name resolution for every client
|
||||||
jellyfin:
|
jellyfin:
|
||||||
url: https://media.hubris.network
|
url: https://media.hubris.network
|
||||||
backend: jellyfin
|
backend: jellyfin
|
||||||
|
doc_page: containers/101-jellyfin.md
|
||||||
|
risk_notes: native Authentik OIDC via SSO-Auth plugin, no Caddy forward-auth gate; VAAPI transcode depends on GPU passthrough on strong
|
||||||
nextcloud:
|
nextcloud:
|
||||||
url: https://cloud.hubris.network
|
url: https://cloud.hubris.network
|
||||||
backend: nextcloud
|
backend: nextcloud
|
||||||
|
doc_page: containers/114-nextcloud.md
|
||||||
paperless:
|
paperless:
|
||||||
url: https://paperless.hubris.network
|
url: https://paperless.hubris.network
|
||||||
backend: paperless
|
backend: paperless
|
||||||
|
doc_page: containers/103-paperless.md
|
||||||
|
risk_notes: document archive — treat data as irreplaceable; DB operations are destructive-class
|
||||||
matrix:
|
matrix:
|
||||||
url: https://matrix.hubris.network
|
url: https://matrix.hubris.network
|
||||||
backend: elementsynapse
|
backend: elementsynapse
|
||||||
|
doc_page: containers/118-elementsynapse.md
|
||||||
|
risk_notes: alert/approval channel for Oikos — outage silences agent escalation
|
||||||
photos:
|
photos:
|
||||||
url: https://photos.hubris.network
|
url: https://photos.hubris.network
|
||||||
backend: mule-images
|
backend: mule-images
|
||||||
|
doc_page: containers/120-mule-images.md
|
||||||
|
config_repo: dtoro/mule-image
|
||||||
arr_stack:
|
arr_stack:
|
||||||
backend: arriman
|
backend: arriman
|
||||||
note: jellyseerr / qbit / sab on docker compose
|
note: jellyseerr / qbit / sab on docker compose
|
||||||
|
doc_page: containers/122-arriman.md
|
||||||
artifacto:
|
artifacto:
|
||||||
backend: apps
|
backend: apps
|
||||||
url: https://artifacto.hubris.network
|
url: https://artifacto.hubris.network
|
||||||
|
doc_page: containers/105-apps.md
|
||||||
|
config_repo: dtoro/Artifacto
|
||||||
trmnl:
|
trmnl:
|
||||||
backend: trmnl
|
backend: trmnl
|
||||||
url: https://trmnl.hubris.network
|
url: https://trmnl.hubris.network
|
||||||
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
|
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
|
||||||
|
doc_page: containers/128-trmnl.md
|
||||||
|
config_repo: dtoro/terminalito
|
||||||
zimaos:
|
zimaos:
|
||||||
url: https://zimaos.hubris.network
|
url: https://zimaos.hubris.network
|
||||||
backend: zimaos
|
backend: zimaos
|
||||||
|
doc_page: vms/100-zimaos.md
|
||||||
haos:
|
haos:
|
||||||
backend: haos
|
backend: haos
|
||||||
|
doc_page: vms/108-haos.md
|
||||||
homelab_mcp:
|
homelab_mcp:
|
||||||
backend: apps
|
backend: apps
|
||||||
port: 9810
|
port: 9810
|
||||||
systemd_unit: homelab-mcp
|
systemd_unit: homelab-mcp
|
||||||
public_host: mcp.hubris.network
|
public_host: mcp.hubris.network
|
||||||
endpoint: https://mcp.hubris.network/mcp
|
endpoint: https://mcp.hubris.network/mcp
|
||||||
|
doc_page: infrastructure/homelab-context.md
|
||||||
|
config_repo: dtoro/Homelab-Docs
|
||||||
note: MCP server. Read-only context + management. Reachable on the LAN via Caddy
|
note: MCP server. Read-only context + management. Reachable on the LAN via Caddy
|
||||||
and from off-LAN via Netbird (192.168.8.0/24 is a network resource routed through
|
and from off-LAN via Netbird (192.168.8.0/24 is a network resource routed through
|
||||||
hubris).
|
hubris).
|
||||||
|
risk_notes: agents' primary read surface — outage degrades every agent to grepping the clone
|
||||||
secrets_issuance:
|
secrets_issuance:
|
||||||
backend: apps
|
backend: apps
|
||||||
port: 9820
|
port: 9820
|
||||||
systemd_unit: secrets-issuance
|
systemd_unit: secrets-issuance
|
||||||
public_host: secrets.hubris.network
|
public_host: secrets.hubris.network
|
||||||
endpoint: https://secrets.hubris.network/issue
|
endpoint: https://secrets.hubris.network/issue
|
||||||
|
doc_page: operations/agent-enrollment.md
|
||||||
|
config_repo: dtoro/Homelab-Docs
|
||||||
note: Issues per-client age private keys. Gated at source-IP layer (mesh + LAN
|
note: Issues per-client age private keys. Gated at source-IP layer (mesh + LAN
|
||||||
subnets in MESH_SUBNETS).
|
subnets in MESH_SUBNETS).
|
||||||
|
risk_notes: identity issuance — any change is security-sensitive; key operations are destructive-class
|
||||||
hosts:
|
hosts:
|
||||||
hubris:
|
hubris:
|
||||||
kind: proxmox-host
|
kind: proxmox-host
|
||||||
@@ -374,8 +420,6 @@ hosts:
|
|||||||
- MariaDB sidecar at /opt/romm/docker-compose.yml.
|
- MariaDB sidecar at /opt/romm/docker-compose.yml.
|
||||||
- ROMs on ludo-lvm media volume at /mnt/media_local/roms.
|
- ROMs on ludo-lvm media volume at /mnt/media_local/roms.
|
||||||
- 1 core / 2 GiB RAM / 16 GiB rootfs (ludo-lvm).
|
- 1 core / 2 GiB RAM / 16 GiB rootfs (ludo-lvm).
|
||||||
# 123 (claudio-bot) — destroyed 2026-06-04, replaced by Hermes Agent
|
|
||||||
# 126 (plato) — destroyed 2026-06-28, notes workspace decommissioned
|
|
||||||
zimaos:
|
zimaos:
|
||||||
kind: vm
|
kind: vm
|
||||||
pve_id: 100
|
pve_id: 100
|
||||||
@@ -470,3 +514,74 @@ hosts:
|
|||||||
netbird:
|
netbird:
|
||||||
fqdn: rclone.netbird.selfhosted
|
fqdn: rclone.netbird.selfhosted
|
||||||
age_pubkey: age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x
|
age_pubkey: age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x
|
||||||
|
|
||||||
|
# Destroyed nodes (lifecycle state: destroyed — see oikos/ontology.yaml).
|
||||||
|
# Kept so agents can answer "what happened to X?" from structured data and
|
||||||
|
# so drift detectors can flag anything still referencing them.
|
||||||
|
# Full narrative table: containers/index.md "Recently destroyed".
|
||||||
|
archaeology:
|
||||||
|
claudio-bot:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 123
|
||||||
|
destroyed: 2026-06-04
|
||||||
|
reason: replaced by Hermes Agent on mac-mini; monitoring moved to homelab-health-watchdog cron
|
||||||
|
plato:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 126
|
||||||
|
destroyed: 2026-06-28
|
||||||
|
reason: notes workspace decommissioned; data retained at /mnt/library/documents/plato
|
||||||
|
mule-photos-new:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 127
|
||||||
|
destroyed: 2026-05-22
|
||||||
|
reason: PhotoPrism test stack promoted to LXC 120 (Mulimage 2.0 merge)
|
||||||
|
heaper:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 116
|
||||||
|
destroyed: 2026-05-14
|
||||||
|
reason: decommissioned; data retained at /mnt/library/heaper
|
||||||
|
syncthing:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 109
|
||||||
|
destroyed: 2026-05-14
|
||||||
|
reason: decommissioned; library subtree was empty
|
||||||
|
seafile:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 125
|
||||||
|
destroyed: 2026-05-13
|
||||||
|
reason: Seafile Pro evaluation rejected; files.hubris.network removed from caddy + dns
|
||||||
|
arr-yunohost:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 100
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: migrated to docker stack on arriman (LXC 122)
|
||||||
|
flaresolverr:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 106
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: folded into the arriman docker compose
|
||||||
|
marimo:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 107
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: decommissioned
|
||||||
|
photoprism:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 110
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: replaced by mule-images (LXC 120)
|
||||||
|
karakeep:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 111
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: decommissioned
|
||||||
|
immich:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 112
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: replaced by mule-images (LXC 120)
|
||||||
|
reticulum:
|
||||||
|
kind: lxc
|
||||||
|
pve_id: 115
|
||||||
|
destroyed: 2026-04-28
|
||||||
|
reason: decommissioned
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ def build_one(name: str, entry: dict, inventory: dict) -> dict:
|
|||||||
"kind": entry.get("kind"),
|
"kind": entry.get("kind"),
|
||||||
"os": entry.get("os"),
|
"os": entry.get("os"),
|
||||||
"role": entry.get("role"),
|
"role": entry.get("role"),
|
||||||
|
# Oikos lifecycle (oikos/ontology.yaml); absent in inventory = active
|
||||||
|
"state": entry.get("state", "active"),
|
||||||
"host": entry.get("host"),
|
"host": entry.get("host"),
|
||||||
"pve_id": pve_id,
|
"pve_id": pve_id,
|
||||||
|
"storage": entry.get("storage"),
|
||||||
|
"depends_on": entry.get("depends_on", []),
|
||||||
"lan_ip": entry.get("lan_ip"),
|
"lan_ip": entry.get("lan_ip"),
|
||||||
"mesh": entry.get("mesh", {}),
|
"mesh": entry.get("mesh", {}),
|
||||||
"mesh_globals": {
|
"mesh_globals": {
|
||||||
|
|||||||
178
oikos/gen-topology.py
Normal file
178
oikos/gen-topology.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate infrastructure/topology.md (Mermaid views) from inventory.yaml.
|
||||||
|
|
||||||
|
Views:
|
||||||
|
1. Compute & ingress — hypervisors → guests → services → public URLs
|
||||||
|
2. Storage — mounts and pools per guest
|
||||||
|
|
||||||
|
Run from the repo root:
|
||||||
|
python3 oikos/gen-topology.py # writes infrastructure/topology.md
|
||||||
|
python3 oikos/gen-topology.py --check # exit 1 if output would change
|
||||||
|
|
||||||
|
Wired into the same regeneration path as mcp/build_host_files.py so the
|
||||||
|
diagrams never drift from inventory. Edges follow oikos/ontology.yaml
|
||||||
|
(hosts, provides, routes-to, mounts, stores-on).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
print("PyYAML is required: pip install pyyaml", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
INVENTORY = REPO / "inventory.yaml"
|
||||||
|
OUTPUT = REPO / "infrastructure" / "topology.md"
|
||||||
|
|
||||||
|
BANNER = (
|
||||||
|
"<!-- Generated by oikos/gen-topology.py from inventory.yaml. -->\n"
|
||||||
|
"<!-- Do NOT edit by hand - your changes will be overwritten. -->\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def node_id(name: str) -> str:
|
||||||
|
"""Mermaid-safe node id."""
|
||||||
|
return name.replace("-", "_").replace(".", "_").replace("/", "_").strip("_")
|
||||||
|
|
||||||
|
|
||||||
|
def guest_label(name: str, entry: dict) -> str:
|
||||||
|
pve = entry.get("pve_id")
|
||||||
|
role = entry.get("role", "")
|
||||||
|
tag = f"LXC {pve}" if entry.get("kind") == "lxc" and pve else \
|
||||||
|
f"VM {pve}" if entry.get("kind") == "vm" and pve else entry.get("kind", "")
|
||||||
|
ip = entry.get("lan_ip", "")
|
||||||
|
parts = [name, tag, role, ip]
|
||||||
|
return "<br/>".join(str(p) for p in parts if p)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_view(inv: dict) -> list[str]:
|
||||||
|
hosts = inv.get("hosts", {})
|
||||||
|
services = inv.get("services", {})
|
||||||
|
lines = ["```mermaid", "flowchart LR"]
|
||||||
|
|
||||||
|
hypervisors = {n: e for n, e in hosts.items() if e.get("kind") == "proxmox-host"}
|
||||||
|
guests = {n: e for n, e in hosts.items() if e.get("kind") in ("lxc", "vm")}
|
||||||
|
others = {n: e for n, e in hosts.items()
|
||||||
|
if e.get("kind") in ("workstation", "external")}
|
||||||
|
|
||||||
|
for hv in hypervisors:
|
||||||
|
lines.append(f' subgraph {node_id(hv)}_sub["{hv} (Proxmox)"]')
|
||||||
|
for g, e in guests.items():
|
||||||
|
if e.get("host") == hv:
|
||||||
|
lines.append(f' {node_id(g)}["{guest_label(g, e)}"]')
|
||||||
|
lines.append(" end")
|
||||||
|
|
||||||
|
# guests without a parent hypervisor recorded (e.g. rclone)
|
||||||
|
for g, e in guests.items():
|
||||||
|
if e.get("host") not in hypervisors:
|
||||||
|
lines.append(f' {node_id(g)}["{guest_label(g, e)}"]')
|
||||||
|
|
||||||
|
for n, e in others.items():
|
||||||
|
shape = "([{}])" if e.get("kind") == "workstation" else "[[{}]]"
|
||||||
|
lines.append(f' {node_id(n)}{shape.format(guest_label(n, e))}')
|
||||||
|
|
||||||
|
# ingress: public URL -> backend (routes-to)
|
||||||
|
for svc, e in sorted(services.items()):
|
||||||
|
if not isinstance(e, dict):
|
||||||
|
continue
|
||||||
|
backend = e.get("backend")
|
||||||
|
url = e.get("url") or (
|
||||||
|
f'https://{e["public_host"]}' if e.get("public_host") else None)
|
||||||
|
if backend and url and backend in hosts:
|
||||||
|
host = url.removeprefix("https://").removeprefix("http://")
|
||||||
|
# hypervisors are rendered as subgraphs; point edges at the subgraph id
|
||||||
|
target = node_id(backend) + ("_sub" if backend in hypervisors else "")
|
||||||
|
lines.append(
|
||||||
|
f' {node_id("url_" + svc)}(["{host}"]) -->|routes-to| {target}')
|
||||||
|
|
||||||
|
lines.append("```")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def storage_view(inv: dict) -> list[str]:
|
||||||
|
hosts = inv.get("hosts", {})
|
||||||
|
lines = ["```mermaid", "flowchart LR"]
|
||||||
|
pools: set[str] = set()
|
||||||
|
edges: list[str] = []
|
||||||
|
|
||||||
|
for name, e in hosts.items():
|
||||||
|
for mount in e.get("mounts", []):
|
||||||
|
pools.add(mount)
|
||||||
|
edges.append(f' {node_id(name)}["{name}"] -->|mounts| {node_id(mount)}')
|
||||||
|
|
||||||
|
for pool in sorted(pools):
|
||||||
|
lines.append(f' {node_id(pool)}[("{pool}")]')
|
||||||
|
lines.extend(sorted(set(edges)))
|
||||||
|
lines.append("```")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def archaeology_table(inv: dict) -> list[str]:
|
||||||
|
arch = inv.get("archaeology", {})
|
||||||
|
if not arch:
|
||||||
|
return []
|
||||||
|
lines = ["| Node | ID | Destroyed | Reason |", "|---|---|---|---|"]
|
||||||
|
entries = sorted(arch.items(), key=lambda kv: str(kv[1].get("destroyed", "")),
|
||||||
|
reverse=True)
|
||||||
|
for name, e in entries:
|
||||||
|
lines.append(
|
||||||
|
f'| {name} | {e.get("pve_id", "")} | {e.get("destroyed", "")} '
|
||||||
|
f'| {e.get("reason", "")} |')
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def render(inv: dict) -> str:
|
||||||
|
hosts = inv.get("hosts", {})
|
||||||
|
services = inv.get("services", {})
|
||||||
|
counts = (
|
||||||
|
f"{sum(1 for e in hosts.values() if e.get('kind') == 'proxmox-host')} hypervisors, "
|
||||||
|
f"{sum(1 for e in hosts.values() if e.get('kind') == 'lxc')} LXCs, "
|
||||||
|
f"{sum(1 for e in hosts.values() if e.get('kind') == 'vm')} VMs, "
|
||||||
|
f"{sum(1 for e in hosts.values() if e.get('kind') == 'workstation')} workstations, "
|
||||||
|
f"{len(services)} services"
|
||||||
|
)
|
||||||
|
parts = [
|
||||||
|
BANNER,
|
||||||
|
"# Topology (generated)\n",
|
||||||
|
f"Source: [inventory.yaml](../inventory.yaml) — {counts}.",
|
||||||
|
"Edge semantics: [oikos/ontology.yaml](../oikos/ontology.yaml). "
|
||||||
|
"Operating model: [OIKOS.md](../OIKOS.md).\n",
|
||||||
|
"## Compute & ingress\n",
|
||||||
|
"\n".join(compute_view(inv)) + "\n",
|
||||||
|
"## Storage (mounts)\n",
|
||||||
|
"\n".join(storage_view(inv)) + "\n",
|
||||||
|
]
|
||||||
|
arch = archaeology_table(inv)
|
||||||
|
if arch:
|
||||||
|
parts += ["## Archaeology (destroyed nodes)\n", "\n".join(arch) + "\n"]
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--check", action="store_true",
|
||||||
|
help="exit 1 if output would change (don't write)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
inv = yaml.safe_load(INVENTORY.read_text())
|
||||||
|
content = render(inv)
|
||||||
|
existing = OUTPUT.read_text() if OUTPUT.exists() else ""
|
||||||
|
if existing == content:
|
||||||
|
return 0
|
||||||
|
if args.check:
|
||||||
|
print(f"{OUTPUT.relative_to(REPO)} would change", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
OUTPUT.write_text(content)
|
||||||
|
print(f"wrote {OUTPUT.relative_to(REPO)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
137
oikos/ontology.yaml
Normal file
137
oikos/ontology.yaml
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Oikos ontology — the systems model of the homelab.
|
||||||
|
#
|
||||||
|
# This file defines the closed vocabulary Oikos reasons with: entity
|
||||||
|
# types (grouped into eight domains), typed relationships (with inverses),
|
||||||
|
# and the node lifecycle. inventory.yaml holds the *instances*; this file
|
||||||
|
# defines what those instances and their fields MEAN, so agents, the
|
||||||
|
# decision classifier, and the topology generator interpret them
|
||||||
|
# identically. See OIKOS.md for the operating model.
|
||||||
|
#
|
||||||
|
# Rule of completeness: if something can break, be changed, or hold data,
|
||||||
|
# it has an entity type here and edges to the things it touches.
|
||||||
|
|
||||||
|
domains:
|
||||||
|
physical:
|
||||||
|
description: Hardware and environment.
|
||||||
|
entity_types: [site, machine, ups, sensor, peripheral]
|
||||||
|
compute:
|
||||||
|
description: Things that execute workloads.
|
||||||
|
entity_types: [proxmox-host, lxc, vm, workstation, external-host, device]
|
||||||
|
network:
|
||||||
|
description: How things reach each other.
|
||||||
|
entity_types: [lan, mesh, dns-zone, dns-record, ingress-route, certificate, firewall-rule]
|
||||||
|
storage:
|
||||||
|
description: Where data lives and how it survives.
|
||||||
|
entity_types: [storage-pool, volume, mount, backup-target, dataset]
|
||||||
|
software:
|
||||||
|
description: What runs and how it is configured and shipped.
|
||||||
|
entity_types: [service, application, config-repo, package-set, deploy-pipeline]
|
||||||
|
identity_access:
|
||||||
|
description: Who and what may do which things.
|
||||||
|
entity_types: [person, identity-provider, account, secret, key, access-grant]
|
||||||
|
operations:
|
||||||
|
description: The OS's own working objects.
|
||||||
|
entity_types: [agent, runbook, plan, change, incident, signal, approval, report]
|
||||||
|
external:
|
||||||
|
description: Dependencies outside the lab's control.
|
||||||
|
entity_types: [domain-registration, cloud-service, isp-link, vendor-dependency]
|
||||||
|
|
||||||
|
# Relationships. `source:` says which inventory/repo data expresses the edge
|
||||||
|
# today (thin = not yet structured, derive from docs until backfilled).
|
||||||
|
relationships:
|
||||||
|
hosts:
|
||||||
|
inverse: runs-on
|
||||||
|
example: hubris hosts lxc:apps
|
||||||
|
source: hosts.<lxc>.host + pve_id
|
||||||
|
provides:
|
||||||
|
inverse: provided-by
|
||||||
|
example: lxc:apps provides service:homelab_mcp
|
||||||
|
source: hosts.<name>.runs + services.<svc>.backend
|
||||||
|
mounts:
|
||||||
|
inverse: mounted-by
|
||||||
|
example: lxc:jellyfin mounts /mnt/media_local from strong
|
||||||
|
source: hosts.<name>.mounts (extend with from:)
|
||||||
|
stores-on:
|
||||||
|
inverse: stores-for
|
||||||
|
example: lxc:jellyfin rootfs stores-on storage-pool:ludo-lvm
|
||||||
|
source: hosts.<name>.storage (new field)
|
||||||
|
routes-to:
|
||||||
|
inverse: routed-via
|
||||||
|
example: ingress-route:media.hubris.network routes-to service:jellyfin
|
||||||
|
source: services.<svc>.url/public_host + dtoro/caddy-conf
|
||||||
|
resolves-to:
|
||||||
|
inverse: resolved-from
|
||||||
|
example: dns-record:media.hubris.network resolves-to caddy lan_ip
|
||||||
|
source: Technitium split-horizon zone (LXC 107) + dns-sync job
|
||||||
|
secured-by:
|
||||||
|
inverse: secures
|
||||||
|
example: ingress-route:paperless secured-by identity-provider:authentik
|
||||||
|
source: caddy-conf forward-auth blocks + service auth notes
|
||||||
|
authenticates-via:
|
||||||
|
inverse: authenticates
|
||||||
|
example: service:jellyfin authenticates-via authentik (native OIDC)
|
||||||
|
source: services.<svc>.auth (new field, from risk_notes/docs)
|
||||||
|
connects-via:
|
||||||
|
inverse: connects
|
||||||
|
example: workstation:mac-mini connects-via mesh:netbird
|
||||||
|
source: hosts.<name>.mesh
|
||||||
|
can-decrypt:
|
||||||
|
inverse: readable-by
|
||||||
|
example: lxc:apps can-decrypt secret:gitea-pat
|
||||||
|
source: .sops.yaml path rules + hosts.<name>.age_pubkey
|
||||||
|
configured-by:
|
||||||
|
inverse: configures
|
||||||
|
example: lxc:caddy configured-by config-repo:dtoro/caddy-conf
|
||||||
|
source: services.<svc>.config_repo (new field)
|
||||||
|
deploys-to:
|
||||||
|
inverse: deployed-from
|
||||||
|
example: deploy-pipeline:webhook-10 deploys-to /opt/homelab-mcp on lxc:apps
|
||||||
|
source: infrastructure/auto-deploy.md table
|
||||||
|
monitors:
|
||||||
|
inverse: monitored-by
|
||||||
|
example: agent:scheduler monitors service:* (Week 3)
|
||||||
|
source: oikos/scheduler config
|
||||||
|
depends-on:
|
||||||
|
inverse: dependency-of
|
||||||
|
example: service:paperless depends-on service:authentik
|
||||||
|
source: hosts/services depends_on (new field)
|
||||||
|
backs-up-to:
|
||||||
|
inverse: backup-of
|
||||||
|
example: dataset:nextcloud-data backs-up-to backup-target:proton-drive
|
||||||
|
source: infrastructure backups docs → structured field (thin)
|
||||||
|
documents:
|
||||||
|
inverse: documented-by
|
||||||
|
example: containers/101-jellyfin.md documents lxc:jellyfin
|
||||||
|
source: generated see_also / services.<svc>.doc_page
|
||||||
|
powered-by:
|
||||||
|
inverse: powers
|
||||||
|
example: machine:hubris powered-by ups (future, thin record)
|
||||||
|
source: physical domain (thin)
|
||||||
|
registered-with:
|
||||||
|
inverse: registrar-of
|
||||||
|
example: domain-registration:hubris.network registered-with registrar
|
||||||
|
source: external domain (thin)
|
||||||
|
|
||||||
|
# Node lifecycle. Stored as `state:` on each inventory host entry
|
||||||
|
# (absent = active, for backward compatibility). Transitions are runbooks
|
||||||
|
# (Week 2); drift detectors (Week 3) verify declared state matches reality.
|
||||||
|
lifecycle:
|
||||||
|
states: [planned, provisioning, active, migrating, deprecated, destroyed]
|
||||||
|
default: active
|
||||||
|
transitions:
|
||||||
|
planned->provisioning:
|
||||||
|
requires: [inventory-entry, ip-reserved, storage-pool-chosen, doc-page-stub]
|
||||||
|
provisioning->active:
|
||||||
|
requires: [age-key-enrolled-if-needed, mesh-joined-if-needed,
|
||||||
|
ingress-live-if-public, health-check-answering,
|
||||||
|
doc-page-complete, ledger-entry]
|
||||||
|
active->migrating:
|
||||||
|
requires: [preflight, backup-verified]
|
||||||
|
migrating->active:
|
||||||
|
requires: [post-verify, caddy-backends-checked, mounts-checked, docs-updated]
|
||||||
|
active->deprecated:
|
||||||
|
requires: [replacement-live-or-role-retired]
|
||||||
|
complete_when: no inbound depends-on / routes-to edges remain
|
||||||
|
deprecated->destroyed:
|
||||||
|
requires: [backups-verified, secrets-revoked-and-rekeyed,
|
||||||
|
ingress-and-dns-removed, archaeology-entry, ledger-entry]
|
||||||
117
oikos/policy.yaml
Normal file
117
oikos/policy.yaml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Oikos risk & approval policy — machine-readable safety model.
|
||||||
|
#
|
||||||
|
# Every operation an agent can perform maps to exactly one risk class.
|
||||||
|
# The decision classifier (oikos/decide.py, Week 3) and the homelab CLI
|
||||||
|
# consult this file before executing; agents consult it before proposing.
|
||||||
|
# See OIKOS.md for the operating model.
|
||||||
|
#
|
||||||
|
# Autonomy default (operator decision 2026-07-05): unattended agents may
|
||||||
|
# perform read_only and reversible_low actions; config_mutation and
|
||||||
|
# destructive always require operator approval.
|
||||||
|
|
||||||
|
risk_classes:
|
||||||
|
read_only:
|
||||||
|
description: Observes state; cannot change anything.
|
||||||
|
approval: none
|
||||||
|
ledger: false
|
||||||
|
reversible_low:
|
||||||
|
description: >-
|
||||||
|
Changes runtime state in a way a single follow-up command undoes
|
||||||
|
(restart, cache clear, sync pull). No config or data changes.
|
||||||
|
approval: none
|
||||||
|
ledger: true # every mutation leaves a ledger entry
|
||||||
|
config_mutation:
|
||||||
|
description: >-
|
||||||
|
Changes tracked configuration or deployed software: repo edit + push,
|
||||||
|
deploy pipeline trigger, Caddy/Gitea/app config, package upgrades.
|
||||||
|
Reversible via git, but affects other consumers.
|
||||||
|
approval: operator # Matrix ✅/❌ reaction (Week 3 approval engine)
|
||||||
|
ledger: true
|
||||||
|
destructive:
|
||||||
|
description: >-
|
||||||
|
Destroys or irreversibly alters data/entities: container destroy,
|
||||||
|
disk format, DB wipe, secret rotation, client revocation.
|
||||||
|
approval: operator_confirmed # approval + typed confirmation phrase
|
||||||
|
ledger: true
|
||||||
|
|
||||||
|
# Lifecycle gates (see ontology.yaml lifecycle):
|
||||||
|
# provisioning: config_mutation downgraded to reversible_low (no dependents yet)
|
||||||
|
# deprecated: adding new inbound edges (depends-on/routes-to) is refused
|
||||||
|
# destroyed: any action targeting the entity raises a drift signal
|
||||||
|
lifecycle_overrides:
|
||||||
|
provisioning:
|
||||||
|
config_mutation: reversible_low
|
||||||
|
deprecated:
|
||||||
|
refuse: [new-inbound-edges]
|
||||||
|
destroyed:
|
||||||
|
refuse: [all]
|
||||||
|
|
||||||
|
# homelab CLI subcommands → risk class
|
||||||
|
commands:
|
||||||
|
whoami: read_only
|
||||||
|
list: read_only
|
||||||
|
status: read_only
|
||||||
|
logs: read_only
|
||||||
|
open: read_only
|
||||||
|
ssh-keyscan: read_only
|
||||||
|
apt-audit: read_only
|
||||||
|
mcp: read_only # MCP tools are individually classified below
|
||||||
|
secret: read_only # decrypt-to-stdout; never write secrets to files/docs
|
||||||
|
ssh: read_only # interactive shell itself; actions inside it carry
|
||||||
|
# their own class — agents must not use raw ssh to
|
||||||
|
# bypass policy (HERMES.md convention)
|
||||||
|
sync: reversible_low
|
||||||
|
refresh-creds: reversible_low
|
||||||
|
ssh-config: reversible_low # rewrites ~/.ssh/config, regenerable
|
||||||
|
apt-upgrade: config_mutation
|
||||||
|
render-vps-configs: config_mutation
|
||||||
|
client-add: config_mutation
|
||||||
|
client-remove: destructive # revokes key + re-keys all secrets
|
||||||
|
|
||||||
|
# MCP tools → risk class (all currently read-only by design)
|
||||||
|
mcp_tools:
|
||||||
|
get_host: read_only
|
||||||
|
list_services: read_only
|
||||||
|
find_service: read_only
|
||||||
|
get_topology: read_only
|
||||||
|
search_docs: read_only
|
||||||
|
get_page: read_only
|
||||||
|
get_changelog: read_only
|
||||||
|
whoami: read_only
|
||||||
|
get_service_status: read_only
|
||||||
|
tail_log: read_only
|
||||||
|
list_lxcs: read_only
|
||||||
|
get_lxc_state: read_only
|
||||||
|
ping_service: read_only
|
||||||
|
list_my_secrets: read_only
|
||||||
|
|
||||||
|
# Common operational actions (not yet CLI subcommands) → risk class.
|
||||||
|
# Used by agents to classify ad-hoc work until Week 2/3 wraps them in
|
||||||
|
# `homelab service` / runbooks.
|
||||||
|
actions:
|
||||||
|
service-restart: reversible_low
|
||||||
|
cache-clear: reversible_low
|
||||||
|
docker-compose-restart: reversible_low
|
||||||
|
tracked-config-edit: config_mutation # commit+push to config repo, never local edit
|
||||||
|
deploy-webhook-trigger: config_mutation
|
||||||
|
lxc-create: config_mutation # new entity, state: provisioning
|
||||||
|
lxc-migrate: config_mutation
|
||||||
|
dns-record-change: config_mutation
|
||||||
|
ingress-route-change: config_mutation
|
||||||
|
secret-rotate: destructive
|
||||||
|
lxc-destroy: destructive
|
||||||
|
disk-format: destructive
|
||||||
|
db-wipe: destructive
|
||||||
|
storage-pool-change: destructive
|
||||||
|
|
||||||
|
# Per-service overrides (schema ready; populate as needs emerge).
|
||||||
|
# Example:
|
||||||
|
# jellyfin:
|
||||||
|
# service-restart: reversible_low # default anyway
|
||||||
|
# caddy:
|
||||||
|
# service-restart: config_mutation # wide blast radius: all ingress
|
||||||
|
service_overrides:
|
||||||
|
caddy:
|
||||||
|
service-restart: config_mutation # everything *.hubris.network rides on it
|
||||||
|
dns:
|
||||||
|
service-restart: config_mutation # LAN-wide resolver
|
||||||
Reference in New Issue
Block a user