remove hosts/ directory — single source of truth is inventory.yaml

- Delete cmd/oikos/build_hosts.go (generator no longer needed)
- Remove build-hosts subcommand from main.go
- Fix oikos homelab whoami: read from inventory.yaml instead of hosts/
- Update bootstrap.sh: identity check uses inventory.yaml
- Remove all 27 generated hosts/*.yaml files
- Update AGENTS.md and OIKOS.md to reference inventory.yaml only
This commit is contained in:
2026-07-07 20:48:51 +02:00
parent f04e0dc0d4
commit 5009a335bb
33 changed files with 23 additions and 1003 deletions

View File

@@ -38,7 +38,7 @@ one pass through **Observe → Orient → Decide → Act**:
| Primitive | What it is | Lives in | | Primitive | What it is | Lives in |
|---|---|---| |---|---|---|
| Host / Service | topology entities | `inventory.yaml` (+ generated `hosts/*.yaml`) | | Host / Service | topology entities | `inventory.yaml` |
| Secret | SOPS+age encrypted value, per-client recipients | `secrets/` + `.sops.yaml` | | Secret | SOPS+age encrypted value, per-client recipients | `secrets/` + `.sops.yaml` |
| Runbook | executable workflow with risk class + verification | `.agents/skills/<name>/SKILL.md` | | Runbook | executable workflow with risk class + verification | `.agents/skills/<name>/SKILL.md` |
| Signal | something needing attention, with lifecycle | `signals/` ledger (Week 3) | | Signal | something needing attention, with lifecycle | `signals/` ledger (Week 3) |

View File

@@ -16,13 +16,13 @@ Agent-facing instruction is separated from human content under `.agents/`:
`.agents/domains/` holds the per-domain schemas `.agents/domains/` holds the per-domain schemas
([knowledge](.agents/domains/knowledge/schema.md), [operations](.agents/domains/operations/schema.md)). ([knowledge](.agents/domains/knowledge/schema.md), [operations](.agents/domains/operations/schema.md)).
The narrative wiki lives under `knowledge/wiki/`; the machine-readable substrate The narrative wiki lives under `knowledge/wiki/`; the machine-readable substrate
(`inventory.yaml`, `hosts/*.yaml`, `oikos/`) stays at the repo root. (`inventory.yaml` stays at the repo root.
## 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:
/opt/homelab-context/hosts/<your-hostname>.yaml /opt/homelab-context/inventory.yaml
That file tells you your role, your peers, what's mounted, and what services That file tells you your role, your peers, what's mounted, and what services
you host. If it does not exist, this client was not enrolled — stop and tell you host. If it does not exist, this client was not enrolled — stop and tell

View File

@@ -314,10 +314,10 @@ else
fi fi
# -------- identity check -------- # -------- identity check --------
HOST_YAML="$CLONE_DIR/hosts/$HNAME.yaml" INVENTORY="$CLONE_DIR/inventory.yaml"
if [ ! -f "$HOST_YAML" ]; then if [ ! -f "$INVENTORY" ] || ! grep -q "^ $HNAME:" "$INVENTORY" 2>/dev/null; then
cat >&2 <<EOF cat >&2 <<EOF
[bootstrap] no hosts/$HNAME.yaml in the repo. [bootstrap] no entry for '$HNAME' in inventory.yaml.
This client has not been enrolled yet. From any existing client, run: This client has not been enrolled yet. From any existing client, run:
homelab client add $HNAME homelab client add $HNAME
@@ -669,7 +669,7 @@ cat <<EOF
[bootstrap] done. [bootstrap] done.
Identity: $CLONE_DIR/hosts/$HNAME.yaml Identity: $CLONE_DIR/inventory.yaml
Sync: 5-minute interval ($([ "$OS" = "Darwin" ] && echo launchd || echo systemd)) Sync: 5-minute interval ($([ "$OS" = "Darwin" ] && echo launchd || echo systemd))
Manual pull: homelab sync (or 'systemctl start homelab-context-sync' / 'launchctl kickstart') Manual pull: homelab sync (or 'systemctl start homelab-context-sync' / 'launchctl kickstart')
CLI: /usr/local/bin/homelab (try 'homelab whoami') CLI: /usr/local/bin/homelab (try 'homelab whoami')

View File

@@ -1,187 +0,0 @@
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
func runBuildHosts() error {
repoDir := "."
if d := os.Getenv("HOMELAB_CONTEXT_DIR"); d != "" {
repoDir = d
}
data, err := os.ReadFile(filepath.Join(repoDir, "inventory.yaml"))
if err != nil {
return fmt.Errorf("read inventory: %w", err)
}
var inventory map[string]any
if err := yaml.Unmarshal(data, &inventory); err != nil {
return fmt.Errorf("parse inventory: %w", err)
}
hosts, _ := inventory["hosts"].(map[string]any)
services, _ := inventory["services"].(map[string]any)
mesh, _ := inventory["mesh"].(map[string]any)
hostsDir := filepath.Join(repoDir, "hosts")
os.MkdirAll(hostsDir, 0755)
desired := make(map[string]string)
for name, entry := range hosts {
entryMap, _ := entry.(map[string]any)
record := buildHostRecord(name, entryMap, services, mesh)
content := fmt.Sprintf("# Generated by oikos build-hosts from inventory.yaml.\n# Do NOT edit by hand.\n\n%s", asYAML(record))
desired[name+".yaml"] = content
}
for name, content := range desired {
path := filepath.Join(hostsDir, name)
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
fmt.Printf("wrote %s\n", name)
}
// Clean orphans
entries, _ := os.ReadDir(hostsDir)
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".yaml") && desired[e.Name()] == "" {
os.Remove(filepath.Join(hostsDir, e.Name()))
fmt.Printf("deleted orphan %s\n", e.Name())
}
}
return nil
}
func buildHostRecord(name string, entry map[string]any, services map[string]any, mesh map[string]any) map[string]any {
kind, _ := entry["kind"].(string)
var runsServices []string
for svc, v := range services {
if svcMap, ok := v.(map[string]any); ok {
if backend, ok := svcMap["backend"].(string); ok && backend == name {
runsServices = append(runsServices, svc)
}
}
}
sort.Strings(runsServices)
var hosted []map[string]any
for _, svc := range runsServices {
if svcMap, ok := services[svc].(map[string]any); ok {
entry := make(map[string]any)
for k, v := range svcMap {
entry[k] = v
}
entry["name"] = svc
hosted = append(hosted, entry)
}
}
record := map[string]any{
"name": name,
"kind": kind,
"os": entry["os"],
"role": entry["role"],
"state": stringOr(entry["state"], "active"),
"host": entry["host"],
"pve_id": entry["pve_id"],
"storage": entry["storage"],
"depends_on": entry["depends_on"],
"lan_ip": entry["lan_ip"],
"mesh": entry["mesh"],
"peers": entry["peers"],
"mounts": entry["mounts"],
"public_host": entry["public_host"],
"ssh": entry["ssh"],
"runs": append(stringSlice(entry["runs"]), runsServices...),
"services_hosted": hosted,
"notes": entry["notes"],
"age_pubkey": entry["age_pubkey"],
}
if mesh != nil {
record["mesh_globals"] = map[string]any{
"primary": mesh["primary"],
"accepted": mesh["accepted"],
}
}
if mcp, ok := services["homelab_mcp"].(map[string]any); ok {
record["mcp_endpoint"] = mcp["endpoint"]
}
if si, ok := services["secrets_issuance"].(map[string]any); ok {
record["secrets_issuance_endpoint"] = si["endpoint"]
}
cleaned := make(map[string]any)
for k, v := range record {
switch val := v.(type) {
case nil:
continue
case string:
if val == "" {
continue
}
case []string:
if len(val) == 0 {
continue
}
case []map[string]any:
if len(val) == 0 {
continue
}
case map[string]any:
if len(val) == 0 {
continue
}
case int:
if val == 0 {
continue
}
}
cleaned[k] = v
}
return cleaned
}
func stringOr(v any, def string) string {
if s, ok := v.(string); ok && s != "" {
return s
}
return def
}
func stringSlice(v any) []string {
switch val := v.(type) {
case []any:
var out []string
for _, item := range val {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
case []string:
return val
}
return nil
}
func asYAML(v any) string {
var buf bytes.Buffer
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
enc.Encode(v)
return buf.String()
}

View File

@@ -47,15 +47,24 @@ func runHomelabSubcommand() {
case "whoami": case "whoami":
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
hostFile := filepath.Join(ctxDir, "hosts", hostname+".yaml") // Try short hostname and full hostname
data, err := os.ReadFile(hostFile) hostnameShort := strings.Split(hostname, ".")[0]
if err != nil { inv := loadInventory(ctxDir)
fmt.Printf("host: %s (not enrolled — no hosts/%s.yaml)\n", hostname, hostname) h, ok := inv.Hosts[hostnameShort]
if !ok {
// Try with full hostname
h, ok = inv.Hosts[hostname]
}
if !ok {
fmt.Printf("host: %s (not enrolled — no entry in inventory.yaml)\n", hostname)
return return
} }
var h cliHost state := h.State
yaml.Unmarshal(data, &h) if state == "" {
fmt.Printf("host: %s kind: %s role: %s lan_ip: %s\n", h.Name, h.Kind, h.Role, h.LanIP) state = "active"
}
fmt.Printf("host: %s kind: %s role: %s lan_ip: %s state: %s\n",
hostnameShort, h.Kind, h.Role, h.LanIP, state)
for _, svc := range h.Services { for _, svc := range h.Services {
fmt.Printf(" service: %v\n", svc["name"]) fmt.Printf(" service: %v\n", svc["name"])
} }

View File

@@ -60,11 +60,6 @@ func main() {
slog.Error("export failed", "error", err) slog.Error("export failed", "error", err)
os.Exit(1) os.Exit(1)
} }
case "build-hosts":
if err := runBuildHosts(); err != nil {
slog.Error("build-hosts failed", "error", err)
os.Exit(1)
}
case "homelab": case "homelab":
runHomelabSubcommand() runHomelabSubcommand()
case "api": case "api":
@@ -119,7 +114,6 @@ Roles:
migrate Run database migrations (forward-only, idempotent) migrate Run database migrations (forward-only, idempotent)
seed Ingest seed YAML files into the database seed Ingest seed YAML files into the database
export Export DB state back to seed YAMLs (DR / version control) export Export DB state back to seed YAMLs (DR / version control)
build-hosts Generate hosts/*.yaml from inventory.yaml
homelab Operator CLI: list, whoami, ssh, secret homelab Operator CLI: list, whoami, ssh, secret
api Run the REST + MCP API server (Phase 2) api Run the REST + MCP API server (Phase 2)
scheduler Run the observe + act loop (Phase 3) scheduler Run the observe + act loop (Phase 3)

View File

@@ -1,59 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
host: hubris
kind: lxc
lan_ip: 192.168.8.205
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: apps
ip: 100.121.171.122
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: apps
os: linux
pve_id: 105
role: docker-apps
runs:
- artifacto
- plantuml
- homelab-mcp
- secrets-issuance
- artifacto
- homelab_mcp
- secrets_issuance
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: apps
config_repo: dtoro/Artifacto
doc_page: knowledge/wiki/containers/105-apps.md
name: artifacto
url: https://artifacto.hubris.network
- backend: apps
config_repo: dtoro/Homelab-Docs
doc_page: knowledge/wiki/infrastructure/homelab-context.md
endpoint: https://mcp.hubris.network/mcp
name: homelab_mcp
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).
port: 9810
public_host: mcp.hubris.network
risk_notes: agents' primary read surface — outage degrades every agent to grepping the clone
systemd_unit: homelab-mcp
- backend: apps
config_repo: dtoro/Homelab-Docs
doc_page: .agents/operations/agent-enrollment.md
endpoint: https://secrets.hubris.network/issue
name: secrets_issuance
note: Issues per-client age private keys. Gated at source-IP layer (mesh + LAN subnets in MESH_SUBNETS).
port: 9820
public_host: secrets.hubris.network
risk_notes: identity issuance — any change is security-sensitive; key operations are destructive-class
systemd_unit: secrets-issuance
state: active

View File

@@ -1,34 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: strong
kind: lxc
lan_ip: 192.168.8.245
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: arr
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/media_local
name: arriman
notes:
- 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
- qBittorrent auth subnet whitelist expanded to 192.168.8.0/24 (for seanime + Caddy access)
os: linux
pve_id: 122
role: arr-stack
runs:
- arr_stack
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: arriman
doc_page: knowledge/wiki/containers/122-arriman.md
name: arr_stack
note: jellyseerr / qbit / sab on docker compose
state: active

View File

@@ -1,20 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.6
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: auth-outpost
notes:
- Runs Authentik outpost (reverse-proxy/SSO enforcement) for protected services
os: linux
pve_id: 106
role: authentik-gateway
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,34 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.175
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: caddy
notes:
- Terminates all *.hubris.network
- /etc/caddy is a git checkout of dtoro/caddy-conf
os: linux
peers:
- authentik
- gitea
pve_id: 121
role: reverse-proxy
runs:
- caddy
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: caddy
config_repo: dtoro/caddy-conf
doc_page: knowledge/wiki/containers/121-caddy.md
name: caddy
note: terminates all *.hubris.network
risk_notes: wide blast radius — every *.hubris.network route rides on it (see oikos/policy.yaml service_overrides)
role: reverse-proxy
state: active

View File

@@ -1,29 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.2
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: dns
notes:
- Technitium DNS, split-horizon zone for *.hubris.network
- Primary DNS for 192.168.8.0/24 LAN (inventory.services.dns references this)
os: linux
pve_id: 107
role: dns-server
runs:
- dns
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: dns
doc_page: knowledge/wiki/containers/107-dns.md
name: dns
note: Technitium DNS, split-horizon zone
risk_notes: LAN-wide resolver — misconfig breaks name resolution for every client
state: active

View File

@@ -1,31 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: strong
kind: lxc
lan_ip: 192.168.8.242
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale: {}
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: elementsynapse
notes:
- Migrated from hubris to strong 2026-07-05 (Phase 1 of strong migration plan).
os: linux
public_host: matrix.hubris.network
pve_id: 118
role: matrix-server
runs:
- matrix
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: elementsynapse
doc_page: knowledge/wiki/containers/118-elementsynapse.md
name: matrix
risk_notes: alert/approval channel for Oikos — outage silences agent escalation
url: https://matrix.hubris.network
state: active

View File

@@ -1,36 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.121
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: gitea
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: gitea
notes:
- Bare repos live at /mnt/library/repos/dtoro/*.git
os: linux
public_host: git.hubris.network
pve_id: 104
role: git-server
runs:
- gitea
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: gitea
backend_url: http://192.168.8.121:3000
config_repo: dtoro/gitea-customizations
doc_page: knowledge/wiki/containers/104-gitea.md
name: gitea
risk_notes: hosts all config repos + deploy webhooks; outage blocks auto-deploy and sync
url: https://git.hubris.network
state: active

View File

@@ -1,25 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1uellsemnjrzgfg9fxw4jefpy05laxzggwnwhh6ny3wl7alyp6v8q0muxet
host: strong
kind: lxc
lan_ip: 192.168.8.247
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/media_local
name: grimmory
notes:
- Docker host for Grimmory (community fork of Booklore). Created 2026-06-29.
- Migrated from hubris to strong 2026-07-05 (Phase 2d). Books on ludo-lvm.
os: linux
public_host: books.hubris.network
pve_id: 130
role: book-library
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,27 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: vm
lan_ip: 192.168.8.101
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: homeassistant
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: haos
os: linux
pve_id: 108
role: home-automation
runs:
- haos
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: haos
doc_page: knowledge/wiki/vms/108-haos.md
name: haos
state: active

View File

@@ -1,25 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1s07zs83ehtlg8jtwvr75ltc3c4cdlemfwjuxrwjtwkqxkl9tpggsyrzn2h
host: strong
kind: lxc
lan_ip: 192.168.8.244
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: house
notes:
- Docker host for Yuvomi (family planner). Created 2026-06-26.
- Migrated from hubris to strong 2026-07-05 (Phase 1 of strong migration plan).
- Runs Yuvomi container + WebDAV doc bridge to paperless
- 192.168.8.212 was the hubris IP before migration (briefly picked up by teddycloud via DHCP; teddycloud has since been given a static IP, see hosts.teddycloud)
os: linux
public_host: house.hubris.network
pve_id: 129
role: family-planner
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,36 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
kind: proxmox-host
lan_ip: 192.168.8.77
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
netbird:
fqdn: proxmox-server.netbird.selfhosted
ip: 100.122.38.109
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: hubris
os: linux
role: hypervisor
runs:
- proxmox_ui
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: hubris
doc_page: knowledge/wiki/hosts/hubris.md
name: proxmox_ui
port: 8006
risk_notes: hypervisor UI — changes here affect every guest on the node
url: https://proxmox.hubris.network
ssh:
netbird_port: 22022
port: 22
user: root
state: active

View File

@@ -1,38 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: strong
kind: lxc
lan_ip: 192.168.8.246
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: jellyfin
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/media_local
name: jellyfin
notes:
- Jellyfin 10.11.11 with VAAPI hardware acceleration (Radeon 680M iGPU on strong)
- 4 cores / 8 GiB RAM / 1 GiB swap
- SSO-Auth plugin v4.0.0.4 with Authentik OIDC (no Caddy forward-auth gate)
- GPU passed via dev0+dev1: /dev/dri/renderD128 + card0
- Migrated from hubris to strong 2026-07-05 (Phase 2). Library on ludo-lvm.
os: linux
public_host: media.hubris.network
pve_id: 101
role: media-server
runs:
- jellyfin
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: jellyfin
doc_page: knowledge/wiki/containers/101-jellyfin.md
name: jellyfin
risk_notes: native Authentik OIDC via SSO-Auth plugin, no Caddy forward-auth gate; VAAPI transcode depends on GPU passthrough on strong
url: https://media.hubris.network
state: active

View File

@@ -1,24 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs
kind: workstation
lan_ip: 192.168.178.182
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
netbird:
fqdn: mac-mini-234-17.netbird.selfhosted
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: mac-mini
notes:
- Only macOS in the fleet. Bootstrap uses launchd.
os: macos
role: dev
secrets_issuance_endpoint: https://secrets.hubris.network/issue
ssh:
user: dtoro
state: active

View File

@@ -1,32 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.136
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: muleimage
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: mule-images
os: linux
public_host: photos.hubris.network
pve_id: 120
role: photo-management
runs:
- photos
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: mule-images
config_repo: dtoro/mule-image
doc_page: knowledge/wiki/containers/120-mule-images.md
name: photos
url: https://photos.hubris.network
state: active

View File

@@ -1,35 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
kind: external
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
netbird:
fqdn: netbird-ionos.netbird.selfhosted
ip: 100.122.165.149
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: netbird-vps
notes:
- Public IONOS VPS — hosts the vanilla netbird mgmt+signal+relay+dashboard stack + host coturn (see infrastructure/vps-hardening.md + infrastructure/mesh.md changelog 2026-05-21).
- NOT a homelab client. No /etc/age/key.txt, no /opt/homelab-context clone. Managed via ssh from hubris; sshd is locked to hubris's pubkey.
- Public IPv4 82.165.190.79. Auto-patching via unattended-upgrades.
- Configs rendered by `homelab render-vps-configs` from vps/turnserver.conf.tmpl + vps/management.json.tmpl, with secrets decrypted from secrets/turn-shared-secret.yaml + secrets/netbird-authentik-oidc.yaml on hubris.
os: linux
role: netbird-mgmt
runs:
- authentik
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: netbird-vps
doc_page: knowledge/wiki/containers/106-auth-outpost.md
name: authentik
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
url: https://auth.hubris.network
ssh:
user: root
state: active

View File

@@ -1,31 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.224
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: nextcloud
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: nextcloud
os: linux
public_host: cloud.hubris.network
pve_id: 114
role: file-sync
runs:
- nextcloud
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: nextcloud
doc_page: knowledge/wiki/containers/114-nextcloud.md
name: nextcloud
url: https://cloud.hubris.network
state: active

View File

@@ -1,18 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.200
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: nfs-export
os: linux
pve_id: 102
role: storage-export
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,32 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.130
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: paperless
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: paperless
os: linux
public_host: paperless.hubris.network
pve_id: 103
role: document-archive
runs:
- paperless
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: paperless
doc_page: knowledge/wiki/containers/103-paperless.md
name: paperless
risk_notes: document archive — treat data as irreplaceable; DB operations are destructive-class
url: https://paperless.hubris.network
state: active

View File

@@ -1,19 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1pwtdws2thdh7vzp2dzttl3zxgcs2tgpcsjsqgw3q04nyml4kvuqq467u4x
kind: lxc
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
netbird:
fqdn: rclone.netbird.selfhosted
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: rclone
os: linux
role: backup
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,20 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
kind: workstation
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
netbird:
fqdn: republic-laptop.netbird.selfhosted
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: republic-laptop
os: linux
role: primary-dev
secrets_issuance_endpoint: https://secrets.hubris.network/issue
ssh:
user: dtoro
state: active

View File

@@ -1,26 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: strong
kind: lxc
lan_ip: 192.168.8.249
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/media_local
name: romm
notes:
- Docker host for RomM (romm.app) self-hosted ROM manager. Created 2026-07-05.
- MariaDB sidecar at /opt/romm/docker-compose.yml.
- ROMs on ludo-lvm media volume at /mnt/media_local/roms.
- 1 core / 2 GiB RAM / 16 GiB rootfs (ludo-lvm).
os: linux
public_host: roms.hubris.network
pve_id: 134
role: rom-manager
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,29 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: strong
kind: lxc
lan_ip: 192.168.8.248
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/media_local/anime
name: seanime
notes:
- Seanime anime media server for online streaming + local library scanning
- Created 2026-07-05. Binary at /opt/seanime/bin/seanime, systemd service.
- Connected to qBittorrent on arriman (192.168.8.245:8080)
- 8 online streaming extensions installed (HiAnime, AniWatch, KickAssAnime, etc.)
- /anime mounted from strong ludo-lvm (/mnt/media_local/anime)
- Caddy: https://seanime.hubris.network → 192.168.8.248:43211
- qBittorrent auth subnet whitelist expanded to 192.168.8.0/24 for seanime access
os: linux
public_host: seanime.hubris.network
pve_id: 133
role: anime-media-server
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,23 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.109
mcp_endpoint: https://mcp.hubris.network/mcp
mesh:
tailscale:
fqdn: sophia
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: sophia
os: linux
pve_id: 119
role: workshop
secrets_issuance_endpoint: https://secrets.hubris.network/issue
state: active

View File

@@ -1,24 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
age_pubkey: age1rtwvdct6avjkr3cyxv3vue3vqx4d524fjfr3vk7xrnvyrylnry5sm54sn4
kind: proxmox-host
lan_ip: 192.168.178.181
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: strong
notes:
- Reformatted from Linux workstation ("ludo-mini" in this wiki, still the machine's nickname) to Proxmox VE 9.2.3 on 2026-07-01. Renamed the inventory/wiki identity from ludo-mini to strong on the same day so it matches the OS/cluster hostname everywhere (bootstrap looks up hosts/$(hostname).yaml, so a mismatch would break enrollment).
- Joined hubris's "Homelab" cluster same day. 2-node, no QDevice tiebreaker yet — see hosts/hubris.md quorum note.
- Netbird not yet installed (fresh OS wiped prior enrollment); reachable today only via the household LAN / existing Fritz static route to 192.168.8.0/24. Re-enroll in mesh as a follow-up if off-LAN access to this host itself (not just its future guests) is needed.
- First step of the planned library-SSD migration — see .hermes/plans/2026-06-03_110000-library-ssd-migration-to-ludo-mini.md (filename kept as-is, it's a historical planning doc). Only Phase 1 (Proxmox install + cluster join) is done; no physical drive move, service migration, or GPU passthrough has happened yet.
os: linux
role: hypervisor
secrets_issuance_endpoint: https://secrets.hubris.network/issue
ssh:
user: root
state: active

View File

@@ -1,35 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.150
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
mounts:
- /mnt/library
name: teddycloud
notes:
- Docker host for TeddyCloud (ghcr.io/toniebox-reverse-engineering/teddycloud), a self-hosted reimplementation of the Toniebox cloud backend. Debian 12 (bookworm).
- 1 core / 1 GiB RAM / 512 MiB swap / 16 GiB rootfs (local-lvm).
- Predates the client-enrollment convention — undocumented in inventory.yaml until 2026-07-06, when Oikos's drift detector (oikos/drift.py) caught pve_id 131 live on hubris (`pct list`) with no inventory entry. Static IP assigned 2026-07-05 during the strong migration (was picking up 192.168.8.243 via DHCP before that — see hosts/strong.md's 2026-07-05 changelog).
- No age_pubkey / homelab-context enrollment — not a homelab CLI client, just a docker-compose app container. Not a required follow-up unless it needs secrets.
os: linux
public_host: teddy.hubris.network
pve_id: 131
role: teddycloud
runs:
- teddycloud
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: teddycloud
doc_page: knowledge/wiki/containers/131-teddycloud.md
name: teddycloud
note: self-hosted TeddyCloud (Toniebox cloud reimplementation), docker compose
risk_notes: no Caddy forward-auth gate (unlike sab.hubris.network on the same Caddyfile) — reachable to anyone on the LAN/mesh who can resolve teddy.hubris.network; undocumented in inventory.yaml until 2026-07-06 (drift-caught)
url: https://teddy.hubris.network
state: active

View File

@@ -1,28 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: lxc
lan_ip: 192.168.8.211
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: trmnl
os: linux
public_host: trmnl.hubris.network
pve_id: 128
role: trmnl-middleware
runs:
- trmnl
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: trmnl
config_repo: dtoro/terminalito
doc_page: knowledge/wiki/containers/128-trmnl.md
name: trmnl
note: self-hosted middleware for TRMNL e-ink plugins (polled by TRMNL cloud)
url: https://trmnl.hubris.network
state: active

View File

@@ -1,26 +0,0 @@
# Generated by oikos build-hosts from inventory.yaml.
# Do NOT edit by hand.
host: hubris
kind: vm
lan_ip: 192.168.8.195
mcp_endpoint: https://mcp.hubris.network/mcp
mesh_globals:
accepted:
- netbird
- tailscale
primary: netbird
name: zimaos
os: linux
public_host: zimaos.hubris.network
pve_id: 100
role: nas-frontend-eval
runs:
- zimaos
secrets_issuance_endpoint: https://secrets.hubris.network/issue
services_hosted:
- backend: zimaos
doc_page: knowledge/wiki/vms/100-zimaos.md
name: zimaos
url: https://zimaos.hubris.network
state: active