Problem: node and cross-cutting narratives lived at the repo root
(containers/, vms/, infrastructure/, host .md files), interleaved with the
machine-readable substrate.
Change:
- Move containers/ -> knowledge/wiki/containers/, vms/ -> knowledge/wiki/vms/,
infrastructure/ -> knowledge/wiki/infrastructure/, hosts/{hubris,strong}.md ->
knowledge/wiki/hosts/, infrastructure/references/ -> knowledge/sources/references/,
GLOSSARY.md -> knowledge/GLOSSARY.md.
- Add knowledge/{index.md,log.md,sources/index.md} scaffolding.
- Rewrite all relative links repo-wide via a path-resolving mapper (inbound +
outbound + between-moved-files), including .hermes/, runbooks, operations,
investigations, plans, README, AGENTS.
- Repoint inventory.yaml doc_page fields and regenerate hosts/*.yaml (which
embed doc_page); update oikos/gen-topology.py output path, candidate doc
paths, and footer links; update code-comment doc paths.
Substrate untouched in place: inventory.yaml, hosts/*.yaml (regenerated,
idempotent), oikos/ code, mcp/, secrets/, bin/.
Verification:
- Logical broken-link set identical to pre-move baseline (net 128 -> 127; the
topology regen fixed one, introduced none). Remaining are pre-existing refs
to destroyed/archived nodes, out of scope for this move.
- gen-topology.py --check exit 0 (in sync); cards carry knowledge/wiki/ doc paths.
- build_host_files.py idempotent; all inventory doc_page targets resolve.
- MCP contract verified: get_page/search_docs/get_changelog resolve moved pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.9 KiB
Homelab context distribution
The cross-client context-and-secrets system that makes every agent (Claude Code, Hermes Agent, future MCP-capable clients) on every machine in the lab self-locating and able to read the same source of truth.
Operational walkthrough for enrolling a new client lives in operations/agent-enrollment.md; this page is the architecture reference.
What's where
| Piece | Host | Path | Role |
|---|---|---|---|
| Source of truth | gitea (104) | dtoro/Homelab-Docs.git |
Inventory + wiki + service code |
| Per-client clone | every enrolled client | /opt/homelab-context/ |
Read by homelab CLI, MCP server, Hermes Agent |
homelab CLI |
every enrolled client | /usr/local/bin/homelab → /opt/homelab-context/bin/homelab (symlink) |
Operator surface for enroll/secret/ssh/pct |
| Per-client age key | every enrolled client | /etc/age/key.txt (0600 root) |
Decrypts SOPS-encrypted secrets the client is a recipient on |
| MCP server | apps (105) | homelab-mcp.service on port 9810 (https://mcp.hubris.network/mcp) |
14 tools: 8 context (get_host, search_docs, …) + 5 read-only management (get_service_status, tail_log, …) + list_my_secrets |
| Secrets-issuance | apps (105) | secrets-issuance.service on port 9820 (https://secrets.hubris.network/issue) |
Generates per-client age keypair on first bootstrap; idempotent; admin-token-gated /revoke |
| Sync timer | every enrolled client | homelab-context-sync.timer (Linux) / network.hubris.homelab-context-sync.plist (macOS) |
git pull --ff-only every 5 min |
| Encrypted secrets | dtoro/Homelab-Docs |
secrets/*.yaml (SOPS+age) |
Recipients declared in .sops.yaml |
| Read-only context PAT | dtoro Gitea user, scope read:repository |
given to operators out-of-band | Bootstrap-only — for the initial clone before SOPS works |
| Write-scoped PAT | secrets/gitea-pat.yaml (SOPS) |
homelab refresh-creds swaps it into /etc/homelab-context/git-credentials |
All post-bootstrap pushes (client lifecycle, wiki edits) |
Data flow
dtoro/Homelab-Docs (gitea)
│
┌────────── push ────────┤ ◀── git push (write PAT or SSH)
│ │
│ ┌────── push ──────┘
│ │ │
│ │ ▼ webhook (push event)
│ │ ┌─── homelab-mcp-deploy ──── (LXC 105:9811)
│ │ └─── secrets-issuance-deploy (LXC 105:9821)
│ │ │
│ │ ▼
│ │ git pull → deploy.sh → restart service
│ │
│ └── on every client:
│ timer (5 min) → git pull --ff-only into /opt/homelab-context
│
▼
homelab CLI / MCP server reads /opt/homelab-context for everything
Why two clones on LXC 105
The MCP server and secrets-issuance each have their own clone
(/opt/homelab-mcp, /opt/secrets-issuance) in addition to
/opt/homelab-context. Reasons:
- The deploy webhook for each service updates its own clone, runs
deploy.shfrom there, and re-installs the systemd unit. Mixing this with the client-context clone would create a circular dependency (deploy reinstalls the unit that pulled it). - The MCP server reads its data from
/opt/homelab-context(the same path every client uses) so changes to inventory propagate identically. Code changes live in/opt/homelab-mcpand trigger a service restart.
Mesh / network gates
- Both services bind
0.0.0.0:<port>. The trust boundary isMESH_SUBNETSin the service's environment + nftables (planned). TodayMESH_SUBNETS=100.122.0.0/16,100.64.0.0/10,192.168.8.0/24— Netbird + Tailscale + the homelab LAN. Adjust if the LAN ever has untrusted devices. - Caddy fronts both with Let's Encrypt certs via the IONOS DNS challenge:
mcp.hubris.network→192.168.8.205:9810,secrets.hubris.network→192.168.8.205:9820. Off-LAN clients on Netbird reach them via the192.168.8.0/24network resource routed through the PVE peer (mesh.md). - Clients with default-public DNS (workstations not on Netbird, LXCs
using router DNS) need a
/etc/hostsoverride pointingmcp.hubris.networkandsecrets.hubris.networkat the caddy LXC (192.168.8.175) — same caveat as every other*.hubris.networkservice, see dns.md.
Secrets model
- Each enrolled client gets one age private key issued by
secrets-issuance on first bootstrap. The key file stays root-only on
the client; the public key is committed to
inventory.yaml(and becomes a recipient on SOPS-encrypted files via.sops.yaml). secrets/*.yamlare SOPS+age. Recipients per file are pinned in.sops.yamlcreation_rulesbypath_regex. Re-encrypting a file issops updatekeys -y secrets/<name>.yaml.- The MCP server's
list_my_secrets(caller_pubkey)tool returns only secret names a given pubkey can decrypt — the server never sees plaintext. Decryption is local-on-client (homelab secret <name>shells out tosops -dwith the client's key). - The "all-clients" secrets (
hello.yamlfor the bootstrap decrypt test,gitea-pat.yamlfor the write-scoped PAT) are auto-granted to every newly enrolled client byhomelab client add --finalize-pubkey(which appends the pubkey to the matching.sops.yamlrule and runssops updatekeys). - Removal does not erase past disclosure. Revoking a client via
homelab client removeshreds the issuance-side key, denylists the hostname, removes them from the recipient list, and re-keys all shared secrets — but anything they already decrypted to disk is out of your control. Rotate the underlying credential if compromise is suspected.
Why this design
- One source of truth keeps inventory, code, secrets, and docs
versioned together. A
git logofinventory.yamlis the history of the homelab. - Per-client age keys scale better than a shared admin secret — removing a client is a real revocation (for new ciphertext), not just removing them from a wiki page.
- MCP layer over the same clone gives MCP-capable agents structured
query (
find_service,search_docs) without forcing non-MCP tools to go without — anything can stillcatthe markdown. - Sync timer rather than push fan-out keeps the failure mode contained: one client's webhook outage doesn't block a push from landing on the others. Sub-5-min staleness is fine for docs and rare enough for secrets that we don't need lower latency.
Related
- Operations: agent enrollment — the step-by-step for adding a new client
- Auto-deploy — the
homelab-mcp+secrets-issuancepipelines (and the rest of the lab's webhook pipelines) - Mesh — Netbird / Tailscale paths and the
192.168.8.0/24network resource - Apps (105) — where both services run
- Gitea (104) — the source of truth
Changelog
2026-05-20 — system live across hubris, apps, republic-laptop
Phase 1 of the cross-client context plan merged. Three
clients enrolled end-to-end: PAT-based bootstrap, age-key issuance, SOPS
decrypt verified on each. Webhook auto-deploy for both LXC 105 services
wired (hook ids 10 + 11). homelab refresh-creds + atomic
client add --finalize-pubkey grant flow live so new clients are one
ceremony instead of four manual steps. Outstanding: bootstrap mac-mini
(macOS, exercises launchd) + ludo-mini + the remaining LXCs;
Hermes Agent integration so the agent uses inventory at chat-time.